本节所有的代码基于1.13.4版本。
启动分析
Kubelet的启动参数有两种,kubeletFlags
和kubeletConfig
。其中,kubeletFlags
与我们使用的kubelet的--参数
命令保持一致;kubeletConfig
通过解析特定的配置文件完成参数的配置,它们共同构成kubelet启动参数的配置。如图
Run
方法。Kubelet启动的Run方法代较长,如下
Run: func(cmd *cobra.Command, args []string) {
// initial flag parse, since we disable cobra's flag parsing
if err := cleanFlagSet.Parse(args); err != nil {
cmd.Usage()
klog.Fatal(err)
}
// check if there are non-flag arguments in the command line
cmds := cleanFlagSet.Args()
if len(cmds) > 0 {
cmd.Usage()
klog.Fatalf("unknown command: %s", cmds[0])
}
// short-circuit on help
help, err := cleanFlagSet.GetBool("help")
if err != nil {
klog.Fatal(`"help" flag is non-bool, programmer error, please correct`)
}
if help {
cmd.Help()
return
}
// short-circuit on verflag
verflag.PrintAndExitIfRequested()
utilflag.PrintFlags(cleanFlagSet)
// set feature gates from initial flags-based config
if err := utilfeature.DefaultFeatureGate.SetFromMap(kubeletConfig.FeatureGates); err != nil {
klog.Fatal(err)
}
// validate the initial KubeletFlags
if err := options.ValidateKubeletFlags(kubeletFlags); err != nil {
klog.Fatal(err)
}
if kubeletFlags.ContainerRuntime == "remote" && cleanFlagSet.Changed("pod-infra-container-image") {
klog.Warning("Warning: For remote container runtime, --pod-infra-container-image is ignored in kubelet, which should be set in that remote runtime instead")
}
// load kubelet config file, if provided
if configFile := kubeletFlags.KubeletConfigFile; len(configFile) > 0 {
kubeletConfig, err = loadConfigFile(configFile)
if err != nil {
klog.Fatal(err)
}
// We must enforce flag precedence by re-parsing the command line into the new object.
// This is necessary to preserve backwards-compatibility across binary upgrades.
// See issue #56171 for more details.
if err := kubeletConfigFlagPrecedence(kubeletConfig, args); err != nil {
klog.Fatal(err)
}
// update feature gates based on new config
if err := utilfeature.DefaultFeatureGate.SetFromMap(kubeletConfig.FeatureGates); err != nil {
klog.Fatal(err)
}
}
// We always validate the local configuration (command line + config file).
// This is the default "last-known-good" config for dynamic config, and must always remain valid.
if err := kubeletconfigvalidation.ValidateKubeletConfiguration(kubeletConfig); err != nil {
klog.Fatal(err)
}
// use dynamic kubelet config, if enabled
var kubeletConfigController *dynamickubeletconfig.Controller
if dynamicConfigDir := kubeletFlags.DynamicConfigDir.Value(); len(dynamicConfigDir) > 0 {
var dynamicKubeletConfig *kubeletconfiginternal.KubeletConfiguration
dynamicKubeletConfig, kubeletConfigController, err = BootstrapKubeletConfigController(dynamicConfigDir,
func(kc *kubeletconfiginternal.KubeletConfiguration) error {
// Here, we enforce flag precedence inside the controller, prior to the controller's validation sequence,
// so that we get a complete validation at the same point where we can decide to reject dynamic config.
// This fixes the flag-precedence component of issue #63305.
// See issue #56171 for general details on flag precedence.
return kubeletConfigFlagPrecedence(kc, args)
})
if err != nil {
klog.Fatal(err)
}
// If we should just use our existing, local config, the controller will return a nil config
if dynamicKubeletConfig != nil {
kubeletConfig = dynamicKubeletConfig
// Note: flag precedence was already enforced in the controller, prior to validation,
// by our above transform function. Now we simply update feature gates from the new config.
if err := utilfeature.DefaultFeatureGate.SetFromMap(kubeletConfig.FeatureGates); err != nil {
klog.Fatal(err)
}
}
}
// construct a KubeletServer from kubeletFlags and kubeletConfig
kubeletServer := &options.KubeletServer{
KubeletFlags: *kubeletFlags,
KubeletConfiguration: *kubeletConfig,
}
// use kubeletServer to construct the default KubeletDeps
kubeletDeps, err := UnsecuredDependencies(kubeletServer)
if err != nil {
klog.Fatal(err)
}
// add the kubelet config controller to kubeletDeps
kubeletDeps.KubeletConfigController = kubeletConfigController
// start the experimental docker shim, if enabled
if kubeletServer.KubeletFlags.ExperimentalDockershim {
if err := RunDockershim(&kubeletServer.KubeletFlags, kubeletConfig, stopCh); err != nil {
klog.Fatal(err)
}
return
}
// run the kubelet
klog.V(5).Infof("KubeletConfiguration: %#v", kubeletServer.KubeletConfiguration)
if err := Run(kubeletServer, kubeletDeps, stopCh); err != nil {
klog.Fatal(err)
}
复制代码
主要包括以下步骤:
1、解析参数,对参数的合法性进行判断;
2、根据kubeletConfig
解析一些特殊的特性所需要配置的参数;
3、配置kubeletServer
,包括KubeletFlags
和KubeletConfiguration
两个参数;
4、构造kubeletDeps
结构体;
5、启动最终的Run
方法。
除了最终的Run
方法,其余的步骤还是为kubelet的启动构建初始化的参数,无非就是换一个名称,换一个不同的结构体,并配置相依赖的参数。
启动
启动调用的是Run
方法,如图
initForOS
通过对操作系统的判断,如果是windows系统需要做一些预先的特殊处理;
run
方法即通过传入的
kubeDeps
参数开始执行启动操作。
进入
run
方法,开始主要执行对参数的再一次验证,以及新的结构体的初始化。后续开始构建一些重要的客户端,包括
eventClient
主要处理事件的上报,与apiserver打交道;
heartbeatClient
主要处理心跳操作,与之后的PLEG相关;
csiClient
主要与CSI接口相关。配置完成之后,最终进入
RunKubelet
方法。
RunKubelet
方法最重要的方法有两个:
CreateAndInitKubelet
和
startKubelet
,可以理解为
CreateAndInitKubelet
为参数的配置,
startKubelet
为最终的启动(说来说去还是把参数封装一遍,重新构造新的结构体)。
CreateAndInitKubelet
方法通过调用
NewMainKubelet
返回
Kubelet
结构体。在
NewMainKubelet
中,主要的配置有:
1、PodConfig。通过
makePodSourceConfig
可以发现kubelet获取Pod的来源有以下途径:
静态Pod、
静态Pod的URL地址以及
kube-apiserver;
2、容器与镜像的GC参数。
3、驱逐Pod策略。
最终通过参数填充
Kubelet
结构体,完成kubelet结构体参数的最终配置。
接下来就是启动了,不过在启动之前会有一个判断 判断是以后台daemon进程一直运行还是只启动一次,即runOnce,基本上都是以后台daemon启动的方式,所以大部分调用的是
startKubelet
方法。
startKubelet
方法内部调用了最终的
Run
方法,如下
func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) {
if kl.logServer == nil {
kl.logServer = http.StripPrefix("/logs/", http.FileServer(http.Dir("/var/log/")))
}
if kl.kubeClient == nil {
klog.Warning("No api server defined - no node status update will be sent.")
}
// Start the cloud provider sync manager
if kl.cloudResourceSyncManager != nil {
go kl.cloudResourceSyncManager.Run(wait.NeverStop)
}
if err := kl.initializeModules(); err != nil {
kl.recorder.Eventf(kl.nodeRef, v1.EventTypeWarning, events.KubeletSetupFailed, err.Error())
klog.Fatal(err)
}
// Start volume manager
go kl.volumeManager.Run(kl.sourcesReady, wait.NeverStop)
if kl.kubeClient != nil {
// Start syncing node status immediately, this may set up things the runtime needs to run.
go wait.Until(kl.syncNodeStatus, kl.nodeStatusUpdateFrequency, wait.NeverStop)
go kl.fastStatusUpdateOnce()
// start syncing lease
if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) {
go kl.nodeLeaseController.Run(wait.NeverStop)
}
}
go wait.Until(kl.updateRuntimeUp, 5*time.Second, wait.NeverStop)
// Start loop to sync iptables util rules
if kl.makeIPTablesUtilChains {
go wait.Until(kl.syncNetworkUtil, 1*time.Minute, wait.NeverStop)
}
// Start a goroutine responsible for killing pods (that are not properly
// handled by pod workers).
go wait.Until(kl.podKiller, 1*time.Second, wait.NeverStop)
// Start component sync loops.
kl.statusManager.Start()
kl.probeManager.Start()
// Start syncing RuntimeClasses if enabled.
if kl.runtimeClassManager != nil {
go kl.runtimeClassManager.Run(wait.NeverStop)
}
// Start the pod lifecycle event generator.
kl.pleg.Start()
kl.syncLoop(updates, kl)
}
复制代码
可以看到,在该方法内,完成的就是最终的kubelet的任务,通过多个goroutine完成。包括以下系列:
1、volumeManager,volume相关管理;
2、syncNodeStatus,定时同步Node状态;
3、updateRuntimeUp,定时更新Runtime状态;
4、syncNetworkUtil,定时同步网络状态;
5、podKiller,定时清理死亡的pod;
6、statusManager,pod状态管理;
7、probeManager,pod探针管理;
8、启动PLEG;
9、syncLoop,最重要的主进程,不停监听外部数据的变化执行pod的相应操作。
至此,kubelet启动过程完成。启动主要完成的任务就是参数的配置和多个任务的启动,通过构造一个循环进程不停监听外部事件的变化,执行对应的pod处理工作,这也就是kubelet所需要负责的任务。
Pod启动流程
Pod的启动在syncLoop
方法下调用的syncLoopIteration
方法开始。在syncLoopIteration
方法内,有5个重要的参数
2、handler:处理Pod的handler;
3、syncCh:同步所有等待同步的Pod;
4、houseKeepingCh:清理Pod的channel;
5、plegCh:获取PLEG信息,同步Pod。
每个参数都是一个channel,通过select判断某个channel获取到信息,处理相应的操作。Pod的启动显然与configCh相关。
通过获取configCh信息,获取Pod整个生命周期中的多种状态 相对应的,每个状态对应相应的处理方法 其中, ADD操作对应Pod的创建,其对应的处理方法为 HandlePodAdditions。
进入
HandlePodAdditions
方法,主要以下几个步骤:
1、根据Pod的创建时间对Pod进行排序;
2、podManager添加Pod;(对Pod的管理依赖于podManager)
3、处理mirrorPod,即静态Pod的处理;
4、通过
dispatchWork
方法分发任务,处理Pod的创建;
5、probeManager添加Pod。(readiness和liveness探针)
dispatchWork
方法内,最核心的是调用了
kl.podWorkers.UpdatePod
方法对Pod进行创建。
UpdatePod
方法通过
podUpdates
的map类型获取相对应的Pod,map的key为Pod的UID,value为
UpdatePodOptions
的结构体channel。通过获取到需要创建的Pod之后,单独起一个goroutine调用
managePodLoop
方法完成Pod的创建,
managePodLoop
方法最终调用
syncPodFn
完成Pod的创建,
syncPodFn
对应的就是Kubelet的
syncPod
方法,位于
kubernetes/pkg/kubelet/kubelet.go
下。经过层层环绕,
syncPod
就是最终处理Pod创建的方法。
syncPod
主要的工作流如注释
1、更新Pod的状态,对应
generateAPIPodStatus
和
statusManager.SetPodStatus
方法;
2、创建Pod存储的目录,对应
makePodDataDirs
方法;
3、挂载对应的volume,对应
volumeManager.WaitForAttachAndMount
方法;
4、获取ImagePullSecrets,对应
getPullSecretsForPod
方法;
5、创建容器,对应
containerRuntime.SyncPod
方法,如下
至此,Pod的启动到创建过程完成。通过
kubectl describe pod
命令可以查看Pod创建的整个生命周期。
PLEG分析
PLEG,即PodLifecycleEventGenerator,用来记录Pod生命周期中对应的各种事件。在kubelet中,启动主进程的syncLoop
之前,先启动pleg,如图
Start
方法通过启动一个定时的任务执行
relist
方法
relist
主要的工作就是通过比对Pod的原始状态和现在的状态,判断Pod当前所处的生命周期,核心代码如下
对每一个Pod,比对Pod内的容器,通过
computeEvents-->generateEvents
生成事件。在
generateEvents
内,生成以下事件:
1、newState为 plegContainerRunning,对应 ContainerStarted事件;
2、newState为 plegContainerExited,对应 ContainerDied事件;
3、newState为 plegContainerUnknown,对应 ContainerChanged事件;
4、newState为 plegContainerNonExistent,查找oldState,如果对应 plegContainerExited,则生成的事件为 ContainerRemoved,否则事件为 ContainerDied和 ContainerRemoved;
生成完事件之后,将事件一一通知到 eventChannel,该channel对应的就是
syncLoopIteration
方法下的plegCh
在
syncLoopIteration
方法下,接收到plegCh channel传输过来的消息之后,执行
HandlePodSyncs
同步方法,最终调用到
dispatchWork
这个Pod的处理方法,对Pod的生命进行管理。
GC管理
Kubelet会定时去清理多余的container和image,完成ContainerGC和ImageGC。Kubelet在启动的Run
方法里,会先去调用imageManager的Start方法,代码位于kubernetes/pkg/kubelet/kubelet.go
下,调用了initializeModules
方法。imageManager.Start
方法主要执行两个步骤:
1、detectImages:主要用来监控images,判断镜像是可被发现的;
2、ListImages:主要用来获取镜像信息,写入到缓存imageCache中。
在启动的CreateAndInitKubelet
方法中,开始执行镜像与容器的回收
StartGarbageCollection
方法启用两个goroutine,一个用来做ContainerGC,一个用来做ImageGC,代码如下
func (kl *Kubelet) StartGarbageCollection() {
loggedContainerGCFailure := false
go wait.Until(func() {
if err := kl.containerGC.GarbageCollect(); err != nil {
klog.Errorf("Container garbage collection failed: %v", err)
kl.recorder.Eventf(kl.nodeRef, v1.EventTypeWarning, events.ContainerGCFailed, err.Error())
loggedContainerGCFailure = true
} else {
var vLevel klog.Level = 4
if loggedContainerGCFailure {
vLevel = 1
loggedContainerGCFailure = false
}
klog.V(vLevel).Infof("Container garbage collection succeeded")
}
}, ContainerGCPeriod, wait.NeverStop)
// when the high threshold is set to 100, stub the image GC manager
if kl.kubeletConfiguration.ImageGCHighThresholdPercent == 100 {
klog.V(2).Infof("ImageGCHighThresholdPercent is set 100, Disable image GC")
return
}
prevImageGCFailed := false
go wait.Until(func() {
if err := kl.imageManager.GarbageCollect(); err != nil {
if prevImageGCFailed {
klog.Errorf("Image garbage collection failed multiple times in a row: %v", err)
// Only create an event for repeated failures
kl.recorder.Eventf(kl.nodeRef, v1.EventTypeWarning, events.ImageGCFailed, err.Error())
} else {
klog.Errorf("Image garbage collection failed once. Stats initialization may not have completed yet: %v", err)
}
prevImageGCFailed = true
} else {
var vLevel klog.Level = 4
if prevImageGCFailed {
vLevel = 1
prevImageGCFailed = false
}
klog.V(vLevel).Infof("Image garbage collection succeeded")
}
}, ImageGCPeriod, wait.NeverStop)
}
复制代码
可以看到容器的GC默认是每分钟执行一次,镜像的GC默认是每5分钟执行一次,通过定时执行GC的清理完成容器与镜像的回收。
容器的GC主要完成的任务包括删除被驱除的容器、删除sandboxes以及清理Pod的sandbox的日志目录,代码位于kubernetes/pkg/kubelet/kuberuntime/kuberuntime_gc.go
下,调用了GarbageCollect
方法;镜像的GC主要完成多余镜像的删除和存储空间的释放,代码位于kubernetes/pkg/kubelet/images/image_gc_manager.go
下,调用了GarbageCollect
方法。