本来这篇文章会继续讲述 kubelet 中的主要模块,但由于网友反馈能不能先从 kubelet 的启动流程开始,kubelet 的启动流程在很久之前基于 v1.12 写过一篇文章,对比了 v1.16 中的启动流程变化不大,但之前的文章写的比较简洁,本文会重新分析 kubelet 的启动流程。
Kubelet 启动流程
kubernetes 版本:v1.16
kubelet 的启动比较复杂,首先还是把 kubelet 的启动流程图放在此处,便于在后文中清楚各种调用的流程:
NewKubeletCommand
首先从 kubelet 的 main
函数开始,其中调用的 NewKubeletCommand
方法主要负责获取配置文件中的参数,校验参数以及为参数设置默认值。主要逻辑为:
- 1、解析命令行参数;
- 2、为 kubelet 初始化 feature gates 参数;
- 3、加载 kubelet 配置文件;
- 4、校验配置文件中的参数;
- 5、检查 kubelet 是否启用动态配置功能;
- 6、初始化 kubeletDeps,kubeletDeps 包含 kubelet 运行所必须的配置,是为了实现 dependency injection,其目的是为了把 kubelet 依赖的组件对象作为参数传进来,这样可以控制 kubelet 的行为;
- 7、调用
Run
方法;
k8s.io/kubernetes/cmd/kubelet/app/server.go:111
1 | func NewKubeletCommand() *cobra.Command { |
Run
该方法中仅仅调用 run
方法执行后面的启动逻辑。
k8s.io/kubernetes/cmd/kubelet/app/server.go:408
1 | func Run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-chan struct{}) error { |
run
run
方法中主要是为 kubelet 的启动做一些基本的配置及检查工作,主要逻辑为:
- 1、为 kubelet 设置默认的 FeatureGates,kubelet 所有的 FeatureGates 可以通过命令参数查看,k8s 中处于
Alpha
状态的 FeatureGates 在组件启动时默认关闭,处于Beta
和 GA 状态的默认开启; - 2、校验 kubelet 的参数;
- 3、尝试获取 kubelet 的
lock file
,需要在 kubelet 启动时指定--exit-on-lock-contention
和--lock-file
,该功能处于Alpha
版本默认为关闭状态; - 4、将当前的配置文件注册到 http server
/configz
URL 中; - 5、检查 kubelet 启动模式是否为 standalone 模式,此模式下不会和 apiserver 交互,主要用于 kubelet 的调试;
- 6、初始化 kubeDeps,kubeDeps 中包含 kubelet 的一些依赖,主要有
KubeClient
、EventClient
、HeartbeatClient
、Auth
、cadvisor
、ContainerManager
; - 7、检查是否以 root 用户启动;
- 8、为进程设置 oom 分数,默认为 -999,分数范围为 [-1000, 1000],越小越不容易被 kill 掉;
- 9、调用
RunKubelet
方法; - 10、检查 kubelet 是否启动了动态配置功能;
- 11、启动 Healthz http server;
- 12、如果使用 systemd 启动,通知 systemd kubelet 已经启动;
k8s.io/kubernetes/cmd/kubelet/app/server.go:472
1 | func run(s *options.KubeletServer, kubeDeps *kubelet.Dependencies, stopCh <-chan struct{}) (err error) { |
RunKubelet
RunKubelet
中主要调用了 createAndInitKubelet
方法执行 kubelet 组件的初始化,然后调用 startKubelet
启动 kubelet 中的组件。
k8s.io/kubernetes/cmd/kubelet/app/server.go:989
1 | func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencies, runOnce bool) error { |
createAndInitKubelet
createAndInitKubelet
中主要调用了三个方法来完成 kubelet 的初始化:
kubelet.NewMainKubelet
:实例化 kubelet 对象,并对 kubelet 依赖的所有模块进行初始化;k.BirthCry
:向 apiserver 发送一条 kubelet 启动了的 event;k.StartGarbageCollection
:启动垃圾回收服务,回收 container 和 images;
k8s.io/kubernetes/cmd/kubelet/app/server.go:1089
1 | func createAndInitKubelet(......) { |
kubelet.NewMainKubelet
NewMainKubelet
是初始化 kubelet 的一个方法,主要逻辑为:
- 1、初始化 PodConfig 即监听 pod 元数据的来源(file,http,apiserver),将不同 source 的 pod configuration 合并到一个结构中;
- 2、初始化 containerGCPolicy、imageGCPolicy、evictionConfig 配置;
- 3、启动 serviceInformer 和 nodeInformer;
- 4、初始化
containerRefManager
、oomWatcher
; - 5、初始化 kubelet 对象;
- 6、初始化
secretManager
、configMapManager
; - 7、初始化
livenessManager
、podManager
、statusManager
、resourceAnalyzer
; - 8、调用
kuberuntime.NewKubeGenericRuntimeManager
初始化containerRuntime
; - 9、初始化
pleg
; - 10、初始化
containerGC
、containerDeletor
、imageManager
、containerLogManager
; - 11、初始化
serverCertificateManager
、probeManager
、tokenManager
、volumePluginMgr
、pluginManager
、volumeManager
; - 12、初始化
workQueue
、podWorkers
、evictionManager
; - 13、最后注册相关模块的 handler;
NewMainKubelet
中对 kubelet 依赖的所有模块进行了初始化,每个模块对应的功能在上篇文章“kubelet 架构浅析”有介绍,至于每个模块初始化的流程以及功能会在后面的文章中进行详细分析。
k8s.io/kubernetes/pkg/kubelet/kubelet.go:335
1 | func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,) { |
startKubelet
在startKubelet
中通过调用 k.Run
来启动 kubelet 中的所有模块以及主流程,然后启动 kubelet 所需要的 http server,在 v1.16 中,kubelet 默认仅启动健康检查端口 10248 和 kubelet server 的端口 10250。
k8s.io/kubernetes/cmd/kubelet/app/server.go:1070
1 | func startKubelet(k kubelet.Bootstrap, podCfg *config.PodConfig, kubeCfg *kubeletconfiginternal.KubeletConfiguration, kubeDeps *kubelet.Dependencies, enableCAdvisorJSONEndpoints, enableServer bool) { |
至此,kubelet 对象以及其依赖模块在上面的几个方法中已经初始化完成了,除了单独启动了 gc 模块外其余的模块以及主逻辑最后都会在 Run
方法启动,Run
方法的主要逻辑在下文中会进行解释,此处总结一下 kubelet 启动逻辑中的调用关系如下所示:
1 | |--> NewMainKubelet |
Run
Run
方法是启动 kubelet 的核心方法,其中会启动 kubelet 的依赖模块以及主循环逻辑,该方法的主要逻辑为:
- 1、注册 logServer;
- 2、判断是否需要启动 cloud provider sync manager;
- 3、调用
kl.initializeModules
首先启动不依赖 container runtime 的一些模块; - 4、启动
volume manager
; - 5、执行
kl.syncNodeStatus
定时同步 Node 状态; - 6、调用
kl.fastStatusUpdateOnce
更新容器运行时启动时间以及执行首次状态同步; - 7、判断是否启用
NodeLease
机制; - 8、执行
kl.updateRuntimeUp
定时更新 Runtime 状态; - 9、执行
kl.syncNetworkUtil
定时同步 iptables 规则; - 10、执行
kl.podKiller
定时清理异常 pod,当 pod 没有被 podworker 正确处理的时候,启动一个goroutine 负责 kill 掉 pod; - 11、启动
statusManager
; - 12、启动
probeManager
; - 13、启动
runtimeClassManager
; - 14、启动
pleg
; - 15、调用
kl.syncLoop
监听 pod 变化;
在 Run
方法中主要调用了两个方法 kl.initializeModules
和 kl.fastStatusUpdateOnce
来完成启动前的一些初始化,在初始化完所有的模块后会启动主循环。
k8s.io/kubernetes/pkg/kubelet/kubelet.go:1398
1 | func (kl *Kubelet) Run(updates <-chan kubetypes.PodUpdate) { |
initializeModules
initializeModules
中启动的模块是不依赖于 container runtime 的,并且不依赖于尚未初始化的模块,其主要逻辑为:
- 1、调用
kl.setupDataDirs
创建 kubelet 所需要的文件目录; - 2、创建 ContainerLogsDir
/var/log/containers
; - 3、启动
imageManager
,image gc 的功能已经在 RunKubelet 中启动了,此处主要是监控 image 的变化; - 4、启动
certificateManager
,负责证书更新; - 5、启动
oomWatcher
,监听 oom 并记录事件; - 6、启动
resourceAnalyzer
;
k8s.io/kubernetes/pkg/kubelet/kubelet.go:1319
1 | func (kl *Kubelet) initializeModules() error { |
fastStatusUpdateOnce
fastStatusUpdateOnce
会不断尝试更新 pod CIDR,一旦更新成功会立即执行updateRuntimeUp
和syncNodeStatus
来进行运行时的更新和节点状态更新。此方法只在 kubelet 启动时执行一次,目的是为了通过更新 pod CIDR,减少节点达到 ready 状态的时延,尽可能快的进行 runtime update 和 node status update。
k8s.io/kubernetes/pkg/kubelet/kubelet.go:2262
1 | func (kl *Kubelet) fastStatusUpdateOnce() { |
updateRuntimeUp
updateRuntimeUp
方法在容器运行时首次启动过程中初始化运行时依赖的模块,并在 kubelet 的runtimeState
中更新容器运行时的启动时间。updateRuntimeUp
方法首先检查 network 以及 runtime 是否处于 ready 状态,如果 network 以及 runtime 都处于 ready 状态,然后调用 initializeRuntimeDependentModules
初始化 runtime 的依赖模块,包括 cadvisor
、containerManager
、evictionManager
、containerLogManager
、pluginManage
等。
k8s.io/kubernetes/pkg/kubelet/kubelet.go:2168
1 | func (kl *Kubelet) updateRuntimeUp() { |
initializeRuntimeDependentModules
该方法的主要逻辑为:
- 1、启动
cadvisor
; - 2、获取 CgroupStats;
- 3、启动
containerManager
、evictionManager
、containerLogManager
; - 4、将 CSI Driver 和 Device Manager 注册到
pluginManager
,然后启动pluginManager
;
k8s.io/kubernetes/pkg/kubelet/kubelet.go:1361
1 | func (kl *Kubelet) initializeRuntimeDependentModules() { |
小结
在 Run
方法中可以看到,会直接调用 kl.syncNodeStatus
和 kl.updateRuntimeUp
,但在 kl.fastStatusUpdateOnce
中也调用了这两个方法,而在 kl.fastStatusUpdateOnce
中仅执行一次,在 Run
方法中会定期执行。在kl.fastStatusUpdateOnce
中调用的目的就是当 kubelet 首次启动时尽可能快的进行 runtime update 和 node status update,减少节点达到 ready 状态的时延。而在 kl.updateRuntimeUp
中调用的初始化 runtime 依赖模块的方法 kl.initializeRuntimeDependentModules
通过 sync.Once 调用仅仅会被执行一次。
syncLoop
syncLoop
是 kubelet 的主循环方法,它从不同的管道(file,http,apiserver)监听 pod 的变化,并把它们汇聚起来。当有新的变化发生时,它会调用对应的函数,保证 pod 处于期望的状态。
syncLoop
中首先定义了一个 syncTicker
和 housekeepingTicker
,即使没有需要更新的 pod 配置,kubelet 也会定时去做同步和清理 pod 的工作。然后在 for 循环中一直调用 syncLoopIteration
,如果在每次循环过程中出现错误时,kubelet 会记录到 runtimeState
中,遇到错误就等待 5 秒中继续循环。
k8s.io/kubernetes/pkg/kubelet/kubelet.go:1821
1 | func (kl *Kubelet) syncLoop(updates <-chan kubetypes.PodUpdate, handler SyncHandler) { |
syncLoopIteration
syncLoopIteration
方法会监听多个 channel,当发现任何一个 channel 有数据就交给 handler 去处理,在 handler 中通过调用 dispatchWork
分发任务。它会从以下几个 channel 中获取消息:
- 1、configCh:该信息源由 kubeDeps 对象中的 PodConfig 子模块提供,该模块将同时 watch 3 个不同来源的 pod 信息的变化(file,http,apiserver),一旦某个来源的 pod 信息发生了更新(创建/更新/删除),这个 channel 中就会出现被更新的 pod 信息和更新的具体操作;
- 2、syncCh:定时器,每隔一秒去同步最新保存的 pod 状态;
- 3、houseKeepingCh:housekeeping 事件的通道,做 pod 清理工作;
- 4、plegCh:该信息源由 kubelet 对象中的 pleg 子模块提供,该模块主要用于周期性地向 container runtime 查询当前所有容器的状态,如果状态发生变化,则这个 channel 产生事件;
- 5、liveness Manager:健康检查模块发现某个 pod 异常时,kubelet 将根据 pod 的 restartPolicy 自动执行正确的操作;
k8s.io/kubernetes/pkg/kubelet/kubelet.go:1888
1 | func (kl *Kubelet) syncLoopIteration(......) bool { |
最后再总结一下启动 kubelet 以及其依赖模块 Run
方法中的调用流程:
1 | |--> kl.cloudResourceSyncManager.Run |
总结
本文主要介绍了 kubelet 的启动流程,可以看到 kubelet 启动流程中的环节非常多,kubelet 中也包含了非常多的模块,后续在分享 kubelet 源码的文章中会先以 Run
方法中启动的所有模块为主,各个击破。