本来这篇文章讲述 kubelet 中的主要模块,由于网友反馈能不能先从 kubelet 的启动流程开始,kubelet 的启动流程在很久之前基于 v1.12 写过一篇文章,对比了 v1.16 中的启动流程变化不大,但之前的文章写的比较简洁,本文会重新分析 kubelet 的启动流程。
第
1
则
Kubelet 启动流程
kubelet 的启动比较复杂,首先还是把 kubelet 的启动流程图放在此处,便于在后文中清楚各种调用的流程:
NewKubeletCommand
首先从 kubelet 的 main 函数开始,其中调用的 NewKubeletCommand 方法主要负责获取配置文件中的参数,校验参数以及为参数设置默认值。主要逻辑为:
解析命令行参数;
为 kubelet 初始化 feature gates 参数;
加载 kubelet 配置文件;
校验配置文件中的参数;
检查 kubelet 是否启用动态配置功能;
初始化 kubeletDeps,kubeletDeps 包含 kubelet 运行所必须的配置,是为了实现 dependency injection,其目的是为了把 kubelet 依赖的组件对象作为参数传进来,这样可以控制 kubelet 的行为;
调用 Run 方法;
k8s.io/kubernetes/cmd/kubelet/app/server.go:111
Run
该方法中仅仅调用 run 方法执行后面的启动逻辑。
k8s.io/kubernetes/cmd/kubelet/app/server.go:408
run
run 方法中主要是为 kubelet 的启动做一些基本的配置及检查工作,主要逻辑为:
为 kubelet 设置默认的 FeatureGates,kubelet 所有的 FeatureGates 可以通过命令参数查看,k8s 中处于 Alpha 状态的 FeatureGates 在组件启动时默认关闭,处于 Beta 和 GA 状态的默认开启;
校验 kubelet 的参数;
尝试获取 kubelet 的 lock file,需要在 kubelet 启动时指定 --exit-on-lock-contention 和 --lock-file,该功能处于 Alpha 版本默认为关闭状态;
将当前的配置文件注册到 http server /configz URL 中;
检查 kubelet 启动模式是否为 standalone 模式,此模式下不会和 apiserver 交互,主要用于 kubelet 的调试;
初始化 kubeDeps,kubeDeps 中包含 kubelet 的一些依赖,主要有 KubeClient、EventClient、HeartbeatClient、Auth、cadvisor、ContainerManager;
检查是否以 root 用户启动;
为进程设置 oom 分数,默认为 -999,分数范围为 [-1000, 1000],越小越不容易被 kill 掉;
调用 RunKubelet 方法;
检查 kubelet 是否启动了动态配置功能;
启动 Healthz http server;
如果使用 systemd 启动,通知 systemd kubelet 已经启动;
k8s.io/kubernetes/cmd/kubelet/app/server.go:472
RunKubelet
RunKubelet 中主要调用了 createAndInitKubelet 方法执行 kubelet 组件的初始化,然后调用 startKubelet 启动 kubelet 中的组件。
k8s.io/kubernetes/cmd/kubelet/app/server.go:989
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
func createAndInitKubelet(......) {
k, err = kubelet.NewMainKubelet(
......
)
if err != nil {
return nil, err
}
k.BirthCry()
k.StartGarbageCollection()
return k, nil
}
kubelet.NewMainKubelet
NewMainKubelet 是初始化 kubelet 的一个方法,主要逻辑为:
初始化 PodConfig 即监听 pod 元数据的来源(file,http,apiserver),将不同 source 的 pod configuration 合并到一个结构中;
初始化 containerGCPolicy、imageGCPolicy、evictionConfig 配置;
启动 serviceInformer 和 nodeInformer;
初始化 containerRefManager、oomWatcher;
初始化 kubelet 对象;
初始化 secretManager、configMapManager;
初始化 livenessManager、podManager、statusManager、resourceAnalyzer;
调用 kuberuntime.NewKubeGeneric
RuntimeManager 初始化 containerRuntime;
初始化 pleg;
初始化 containerGC、containerDeletor、imageManager、containerLogManager;
初始化 serverCertificateManager、probeManager、tokenManager、volumePluginMgr、pluginManager、volumeManager;
初始化 workQueue、podWorkers、evictionManager;
最后注册相关模块的 handler;
NewMainKubelet 中对 kubelet 依赖的所有模块进行了初始化,每个模块对应的功能在上篇文章“kubelet 架构浅析”有介绍,至于每个模块初始化的流程以及功能会在后面的文章中进行详细分析。
k8s.io/kubernetes/pkg/kubelet/kubelet.go:335
startKubelet
在startKubelet 中通过调用 k.Run 来启动 kubelet 中的所有模块以及主流程,然后启动 kubelet 所需要的 http server,在 v1.16 中,kubelet 默认仅启动健康检查端口 10248 和 kubelet server 的端口 10250。
k8s.io/kubernetes/cmd/kubelet/app/server.go:1070
至此,kubelet 对象以及其依赖模块在上面的几个方法中已经初始化完成了,除了单独启动了 gc 模块外其余的模块以及主逻辑最后都会在 Run 方法启动,Run 方法的主要逻辑在下文中会进行解释,此处总结一下 kubelet 启动逻辑中的调用关系如下所示:
第
2
则
Run
Run 方法是启动 kubelet 的核心方法,其中会启动 kubelet 的依赖模块以及主循环逻辑,该方法的主要逻辑为:
注册 logServer;
判断是否需要启动 cloud provider sync manager;
调用 kl.initializeModules 首先启动不依赖 container runtime 的一些模块;
启动 volume manager;
执行 kl.syncNodeStatus 定时同步 Node 状态;
调用 kl.fastStatusUpdateOnce 更新容器运行时启动时间以及执行首次状态同步;
判断是否启用 NodeLease 机制;
执行 kl.updateRuntimeUp 定时更新 Runtime 状态;
执行 kl.syncNetworkUtil 定时同步 iptables 规则;
执行 kl.podKiller 定时清理异常 pod,当 pod 没有被 podworker 正确处理的时候,启动一个goroutine 负责 kill 掉 pod;
启动 statusManager;
启动 probeManager;
启动 runtimeClassManager;
启动 pleg;
调用 kl.syncLoop 监听 pod 变化;
在 Run 方法中主要调用了两个方法 kl.initializeModules 和 kl.fastStatusUpdateOnce 来完成启动前的一些初始化,在初始化完所有的模块后会启动主循环。
k8s.io/kubernetes/pkg/kubelet/kubelet.go:1398
initializeModules
initializeModules 中启动的模块是不依赖于 container runtime 的,并且不依赖于尚未初始化的模块,其主要逻辑为:
调用 kl.setupDataDirs 创建 kubelet 所需要的文件目录;
创建 ContainerLogsDir /var/log/containers;
启动 imageManager,image gc 的功能已经在 RunKubelet 中启动了,此处主要是监控 image 的变化;
启动 certificateManager,负责证书更新;
启动 oomWatcher,监听 oom 并记录事件;
启动 resourceAnalyzer;
k8s.io/kubernetes/pkg/kubelet/kubelet.go:1319
func (kl *Kubelet) initializeModules() error {
metrics.Register(
kl.runtimeCache,
collectors.NewVolumeStatsCollector(kl),
collectors.NewLogMetricsCollector(kl.StatsProvider.ListPodStats),
)
metrics.SetNodeName(kl.nodeName)
servermetrics.Register()
// 1、创建文件目录
if err := kl.setupDataDirs(); err != nil {
return err
}
// 2、创建 ContainerLogsDir
if _, err := os.Stat(ContainerLogsDir); err != nil {
if err := kl.os.MkdirAll(ContainerLogsDir, 0755); err != nil {
klog.Errorf("Failed to create directory %q: %v", ContainerLogsDir, err)
}
}
// 3、启动 imageManager
kl.imageManager.Start()
// 4、启动 certificate manager
if kl.serverCertificateManager != nil {
kl.serverCertificateManager.Start()
}
// 5、启动 oomWatcher.
if err := kl.oomWatcher.Start(kl.nodeRef); err != nil {
return fmt.Errorf("failed to start OOM watcher %v", err)
}
// 6、启动 resource analyzer
kl.resourceAnalyzer.Start()
return nil
}
fastStatusUpdateOnce
fastStatusUpdateOnce 会不断尝试更新 pod CIDR,一旦更新成功会立即执行updateRuntimeUp和syncNodeStatus来进行运行时的更新和节点状态更新。此方法只在 kubelet 启动时执行一次,目的是为了通过更新 pod CIDR,减少节点达到 ready 状态的时延,尽可能快的进行 runtime update 和 node status update。
k8s.io/kubernetes/pkg/kubelet/kubelet.go:2262
func (kl *Kubelet) fastStatusUpdateOnce() {
for {
time.Sleep(100 * time.Millisecond)
node, err := kl.GetNode()
if err != nil {
klog.Errorf(err.Error())
continue
}
if len(node.Spec.PodCIDRs) != 0 {
podCIDRs := strings.Join(node.Spec.PodCIDRs, ",")
if _, err := kl.updatePodCIDR(podCIDRs); err != nil {
klog.Errorf("Pod CIDR update to %v failed %v", podCIDRs, err)
continue
}
kl.updateRuntimeUp()
kl.syncNodeStatus()
return
}
}
}
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
initializeRuntimeDependentModules
该方法的主要逻辑为:
启动 cadvisor;
获取 CgroupStats;
启动 containerManager、evictionManager、containerLogManager;
将 CSI Driver 和 Device Manager 注册到 pluginManager,然后启动 pluginManager;
k8s.io/kubernetes/pkg/kubelet/kubelet.go:1361
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
syncLoopIteration
syncLoopIteration 方法会监听多个 channel,当发现任何一个 channel 有数据就交给 handler 去处理,在 handler 中通过调用 dispatchWork 分发任务。它会从以下几个 channel 中获取消息:
configCh:该信息源由 kubeDeps 对象中的 PodConfig 子模块提供,该模块将同时 watch 3 个不同来源的 pod 信息的变化(file,http,apiserver),一旦某个来源的 pod 信息发生了更新(创建/更新/删除),这个 channel 中就会出现被更新的 pod 信息和更新的具体操作;
syncCh:定时器,每隔一秒去同步最新保存的 pod 状态;
houseKeepingCh:housekeeping 事件的通道,做 pod 清理工作;
plegCh:该信息源由 kubelet 对象中的 pleg 子模块提供,该模块主要用于周期性地向 container runtime 查询当前所有容器的状态,如果状态发生变化,则这个 channel 产生事件;
liveness Manager:健康检查模块发现某个 pod 异常时,kubelet 将根据 pod 的 restartPolicy 自动执行正确的操作;
k8s.io/kubernetes/pkg/kubelet/kubelet.go:1888
最后再总结一下启动 kubelet 以及其依赖模块 Run 方法中的调用流程:
本文主要介绍了 kubelet 的启动流程,可以看到 kubelet 启动流程中的环节非常多,也包含了众多的模块,后续在分享 kubelet 源码的文章中会先以 Run 方法中启动的所有模块为主,各个击破。
作者:田飞雨
原文地址:http://rrd.me/fKges
END
云原生套餐钜惠
K8s集群免费领
华为云开年采购季·云原生专场特惠四大福利来袭
1、Kubernetes集群免费领
2、容器平台软件免费领
3、开发者集群套餐低至4.5折
4、更多优惠套餐低至 2 折
点击下方阅读原文或识别二维码
即可领取免费集群动手实验