kubelet源码简单分析
kubelet入口在Run()
,包含了大量的子模块:
- 初始化一些插件,如 image manager, ca manager, oom watcher, resource analyzer
- 启动 volume manager
- 定期与apsierver同步节点状态。为了减轻apiserver的负担,上报 node status 的周期相对长一些,默认 5 分钟;使用 lease 进行 10 秒一次的的同步
- 定期检测 runtime 状态
- 每分钟同步 iptables 规则
- 每秒钟进行一次杀死 pod 的操作,当用户发出删除 pod 的指令时,pod 被加入到
podkillingCh
中,pod killer 会读取该 channel 并杀死里面的 pod - 启动 status manager,它负责与 apisever 同步 pod 信息,同时也是 pod 的缓存
- 启动 probes manager
- 启动 PLEG
- 最后进入
syncLoop()
死循环
main loop for processing changes
syncLoop is the main loop for processing changes. It watches for changes from three channels (file, apiserver, and http) and creates a union of them. For any new change seen, will run a sync against desired state and running state. If no changes are seen to the configuration, will synchronize the last known desired state every sync-frequency seconds. Never returns.
这里首先提到一个概念:pod worker。每个 pod worker 负责一个 pod 的状态同步,不同的 pod worker 互不干扰。
syncLoop
中还新建了两个 channel。syncTicker
每秒生成一个事件,用来检查是否有需要同步的 pod worker,housekeepingTicker
每 20 秒触发一次,触发 pod 清理行为。
各个 channel 和其用法总结如下
configCh
:其实这个一个 aggregate 的 channelplegCh
:PLEG 通过 list & watch 机制监听 apiserver 处的 pod 变化,产生 eventsyncCh
:生成于上面的syncTicker
housekeepingCh
:生成于上面的housekeepingTicker
liveness manager
:健康检查
整个 syncLoopIteration
由一组 select - case 语法组成,我们直接分析创建 pod 的处理逻辑,即 handler.HandlePodAdditions
。
Kubernetes 处理 pod 创建的事件时,可能将多个 pod 的创建合并为一个 event,所以虽然名称叫 pod addition,实际可能处理多个 pod。
// start 刚进入处理函数的时间戳,后面记录 metrics 会用到
start := kl.clock.Now()
// ...省略部分
// static pod有一份mirror pod,static pod不由kubelet管理,所以如果是 mirror pod,直接处理
if kubepod.IsMirrorPod(pod) {
kl.handleMirrorPod(pod, start)
continue
}
// 对于没有被删除的pod,额外进行一次 admission 校验
// 该校验独立于 apiserver,校验规则在 NewMainKubelet 中注册
if !kl.podIsTerminated(pod) {
activePods := kl.filterOutTerminatedPods(existingPods)
if ok, reason, message := kl.canAdmitPod(activePods, pod); !ok {
// 如果 pod 没有通过校验,则被置为 Failed
kl.rejectPod(pod, reason, message)
continue
}
}
// 可能该 pod 是一个 static pod,所以需要获取它的 mirror pod。
// 最终 pod 被 dispatch 出去,并且由 probe manager 接管健康检查
mirrorPod, _ := kl.podManager.GetMirrorPodByPod(pod)
kl.dispatchWork(pod, kubetypes.SyncPodCreate, mirrorPod, start)
kl.probeManager.AddPod(pod)
dispatchWork
将交给 pod workers 做异步的处理。
// Run the sync in an async worker.
kl.podWorkers.UpdatePod(&UpdatePodOptions{
// 记录一组 pod 和 mirror pod
Pod: pod,
MirrorPod: mirrorPod,
UpdateType: syncType,
// 处理完毕之后记录 metrics
OnCompleteFunc: func(err error) {
if err != nil {
metrics.PodWorkerDuration.WithLabelValues(syncType.String()).Observe(metrics.SinceInSeconds(start))
metrics.DeprecatedPodWorkerLatency.WithLabelValues(syncType.String()).Observe(metrics.SinceInMicroseconds(start))
}
},
})
pod workers 维护各个 worker(其实是一些 go routine)的状态信息
type podWorkers struct {
// 每次处理一个事件
podLock sync.Mutex
// 通过 channel 通知 pod 更新的配置信息
podUpdates map[types.UID]chan UpdatePodOptions
// 标志 worker 是否处于 running 状态
isWorking map[types.UID]bool
// Tracks the last undelivered work item for this pod - a work item is
// undelivered if it comes in while the worker is working.
lastUndeliveredWorkUpdate map[types.UID]UpdatePodOptions
workQueue queue.WorkQueue
// This function is run to sync the desired stated of pod.
// NOTE: This function has to be thread-safe - it can be called for
// different pods at the same time.
// 这个函数注册为 kubelet.syncPod
syncPodFn syncPodFnType
// The EventRecorder to use
recorder record.EventRecorder
// backOffPeriod is the duration to back off when there is a sync error.
backOffPeriod time.Duration
// resyncInterval is the duration to wait until the next sync.
resyncInterval time.Duration
// podCache stores kubecontainer.PodStatus for all pods.
podCache kubecontainer.Cache
}
进入 UpdatePod
看一下具体实现
// 如果不加锁,一个 pod 的多个事件可能同时处理
p.podLock.Lock()
defer p.podLock.Unlock()
if podUpdates, exists = p.podUpdates[uid]; !exists {
// We need to have a buffer here, because checkForUpdates() method that
// puts an update into channel is called from the same goroutine where
// the channel is consumed. However, it is guaranteed that in such case
// the channel is empty, so buffer of size 1 is enough.
podUpdates = make(chan UpdatePodOptions, 1)
p.podUpdates[uid] = podUpdates
// 我处理我自己
go func() {
defer runtime.HandleCrash()
p.managePodLoop(podUpdates)
}()
}
// 如果该 pod 没有 worker 正在处理,新建一个 worker,向 channel 中传入一个事件
if !p.isWorking[pod.UID] {
p.isWorking[pod.UID] = true
podUpdates <- *options
}
看一下 managePodLoop
的逻辑
for update := range podUpdates {
// 模块化代码,封装为匿名函数
err := func() error {
podUID := update.Pod.UID
// 为了不让 worker 处理可能过时的 pod,只有当缓存中 pod 在上一次 sync 之后有被改动,才会处理
// 如果 pod 一直没有改动,根本不会有 pod update 事件。而可能 pod update 之后,缓存没有及时
// 更新。虽然这个过程是阻塞的,但整个过程只是加锁进行一些判断,猜测不会成为阻塞的瓶颈
status, err := p.podCache.GetNewerThan(podUID, lastSyncTime)
if err != nil {
// This is the legacy event thrown by manage pod loop
// all other events are now dispatched from syncPodFn
p.recorder.Eventf(update.Pod, v1.EventTypeWarning, events.FailedSync, "error determining status: %v", err)
return err
}
// 进入 kubelet.syncPod 函数
err = p.syncPodFn(syncPodOptions{
mirrorPod: update.MirrorPod,
pod: update.Pod,
podStatus: status,
killPodOptions: update.KillPodOptions,
updateType: update.UpdateType,
})
// 处理完毕之后,更新 lastSyncTime
lastSyncTime = time.Now()
return err
}()
// 执行之前注册的记录 prometheus metrics 的操作
if update.OnCompleteFunc != nil {
update.OnCompleteFunc(err)
}
}
最核心的逻辑在 kubelet.syncPod
中,为了简单起见,只分析与创建 pod 相关的逻辑
// Latency measurements for the main workflow are relative to the
// first time the pod was seen by the API server.
// API server 第一次处理 pod 时,会加上一个 annotation
firstSeenTime = kubetypes.ConvertToTimestamp(firstSeenTimeStr).Get()
if updateType == kubetypes.SyncPodCreate {
if !firstSeenTime.IsZero() {
// This is the first time we are syncing the pod. Record the latency
// since kubelet first saw the pod if firstSeenTime is set.
// pod worker start duration 是此时时间减去 firstSeenTime
metrics.PodWorkerStartDuration.Observe(metrics.SinceInSeconds(firstSeenTime))
metrics.DeprecatedPodWorkerStartLatency.Observe(metrics.SinceInMicroseconds(firstSeenTime))
}
//...
}
// ...省略部分
// Record the time it takes for the pod to become running.
existingStatus, ok := kl.statusManager.GetPodStatus(pod.UID)
if !ok || existingStatus.Phase == v1.PodPending && apiPodStatus.Phase == v1.PodRunning &&
!firstSeenTime.IsZero() {
// 只有第一次看到 pod 从 pending 变成 running 才记录 pod startup duration
// pod start duration 顾名思义
metrics.PodStartDuration.Observe(metrics.SinceInSeconds(firstSeenTime))
metrics.DeprecatedPodStartLatency.Observe(metrics.SinceInMicroseconds(firstSeenTime))
}
// ...省略部分
// Create Cgroups for the pod and apply resource parameters
// to them if cgroups-per-qos flag is enabled.
pcm := kl.containerManager.NewPodContainerManager()
if !kl.podIsTerminated(pod) {
// ...省略部分
// 如果 pod 已经被杀掉了,且 restartPolicy 为 Never,这个 pod 不会再被处理
// 所以不需要创建cgroup
if !(podKilled && pod.Spec.RestartPolicy == v1.RestartPolicyNever) {
if !pcm.Exists(pod) {
// 注:[1]
if err := kl.containerManager.UpdateQOSCgroups(); err != nil {
//...
}
// 注:[2]
if err := pcm.EnsureExists(pod); err != nil {
//...
}
}
}
}
// ...省略部分
// Volume manager will not mount volumes for terminated pods
if !kl.podIsTerminated(pod) {
// Wait for volumes to attach/mount
// 注:[3]
if err := kl.volumeManager.WaitForAttachAndMount(pod); err != nil {
// ...
return err
}
}
// ...
// Call the container runtime's SyncPod callback
// 这里就是 CRI 接口,代码在 kuberuntime 包中
result := kl.containerRuntime.SyncPod(pod, podStatus, pullSecrets, kl.backOff)
注:
- 在 update cgroups 的过程中,一路调用到 pkg/kubelet/cm/cgroup_manager_linux.go 中的
func (m *cgroupManagerImpl) Update(cgroupConfig *CgroupConfig) error
函数,在该函数结束之后,会记录 cgroup update 操作的时延,从进入Update()
函数开始,到函数结束。由于 cgroups 可能有多个 update 操作,这里可能会记录多个时延。该操作并没有调用 runc 的 API,而是通过setSupportedSubsystems
进行系统调用完成 - 这里会记录 cgroup create 的时延,这些操作通过 runc 的 libcontainer 库与系统调用完成。疑问:为什么 create 操作在 update 操作之后?这些 cgroup 的操作并不太理解透彻
- volume 相关操作通过 poll 轮询机制实现,每 3 秒轮询一次,超时时间为 2 分钟。这里并没有 metrics 的记录,所以我无从得知这个 poll 过程究竟耗时多少。volume manager 对比 desired status 和 actual status,通过 reconcile 进行状态机的处理。CSI 的逻辑也包含在 volume manager 中。volume manager 并不会显示的调用写 metrics 的操作,而是把它注册为 hook。见 pkg/volume/util/metrics.go 中
OperationCompleteHook()
,它负责storage_operation_duration_seconds
这个 metric。虽然也是 kubelet 的metric,但和 pod worker 没有直接联系。
containerRuntime
的具体实现在 kuberuntime 这个包中
// KubeGenericRuntime 继承了 containerRuntime interface
type KubeGenericRuntime interface {
kubecontainer.Runtime
kubecontainer.StreamingRuntime
kubecontainer.ContainerCommandRunner
}
syncPod
这个函数有比较完整的注释
SyncPod syncs the running pod into the desired pod by executing following steps:
Compute sandbox and container changes.
Kill pod sandbox if necessary.
Kill any containers that should not be running.
Create sandbox if necessary.
Create init containers.
Create normal containers.
对于新创建一个 pod 来说,前三步我们可以跳过不分析,直接从第四步开始。由于我们的 clusterloader2 测试不包含 init container,所以我们也跳过第五步。我们一共需要关注三个步骤:
// 创建 pod 沙箱
podSandboxID, msg, err = m.createPodSandbox(pod, podContainerChanges.Attempt)
// 获取沙箱状态
podSandboxStatus, err := m.runtimeService.PodSandboxStatus(podSandboxID)
// 创建容器
msg, err := m.startContainer(podSandboxID, podSandboxConfig, container, pod, podStatus, pullSecrets, podIP)
- 首先,创建沙箱。沙箱其实就是一个 pause 容器。该过程记录了
kubelet_runtime_operations
中的operation_type="run_podsandbox"
的时延,从进入RunPodSandbox
开始,到函数结束,涉及 runtime 的真正处理,与 grpc 的时延。注意记录的层次在 kubelet runtime 层,而不在 CRI 层。 - 之后,获取沙箱状态。与上面类似
- 第三步,创建并启动容器。注意
startContainer
实际包含了 create 和 start 两个动作。在 create 环节记录kubelet_runtime_operations
中的operation_type="create_container"
的时延;在 start 环节记录kubelet_runtime_operations
中的operation_type="start_container"
的时延。
如果查看 prometheus 中的 metrics,我们会发现除了 kubelet_runtime_operations
之外,还有 kubelet_docker_operations
。它由 CRI 负责记录,我们可以直接看 dockershim 中 RunPodSandbox
的逻辑。
// 创建沙箱容器
createConfig, err := ds.makeSandboxDockerConfig(config, image)
// 注:[1]
createResp, err := ds.client.CreateContainer(*createConfig)
ds.setNetworkReady(createResp.ID, false)
// 创建 checkpoint
ds.checkpointManager.CreateCheckpoint(createResp.ID, constructPodSandboxCheckpoint(config))
// 启动沙箱容器
err = ds.client.StartContainer(createResp.ID)
// 设置沙箱容器的网络,这里调用了 CNI 的接口,注:[2]
ds.network.SetUpPod(//...)
注:
- 这里有个
client
的概念,dockershim 封装了instrumented_client
,而后者封装了真正沟通 docker 的 grpc client。这个instrumented_client
在执行操作的时候会记录docker_operations
。在 create container 操作中记录"create_container"
,在 start container 中记录"start_container"
。 - 调用 CNI 接口的时候会记录
network_plugin_operations
这个 metric,SetUpPod
中记录了"set_up_pod"
这个动作的时延。
metrics整理
kubelet 常用的 metrics 与他们之间的关系整理如下:
-
pod_worker_duration_seconds
和pod_worker_latency_microseconds
:从进入 kubelet 的 handler 开始,到 pod worker 处理结束。它不代表 pod 的启动时间,只代表一个 pod update 事件在 kubelet 中的处理时间 -
pod_worker_start_duration_seconds
和pod_worker_start_latency_microseconds
:从 API server 第一次处理 pod 开始,到 pod worker 在syncPod
中处理SyncPodCreate
事件 -
pod_start_duration_seconds
和pod_start_latency_microseconds
:从 API Server 第一次处理 pod 开始,到 kubelet 处理 pod update 事件时看到 pod 状态为 running -
cgroup_manager_duration_seconds
和cgroup_manager_latency_microseconds
:从进入 cgroup manager 模块中Update()
或Create()
函数开始,到函数结束,是 pod worker 的子集 -
kubelet_runtime_operations
:从instrumentedRuntimeService
开始处理到处理结束,包括 grpc 时延与 runtime 真正处理的时延,是 pod worker 的子集 -
storage_operation_duration_seconds
:volume 中注册的一个 hook 会异步的注册存储操作的时延,和 pod worker 没有直接联系 -
network_plugin_operations
:由 runtime 再去调用。如果是 dockershim 模式,则由 dockershim 去调用;否则由 CRI-O 去调用 runtime。是kubelet_runtime_operations
的子集 -
docker_operations
:由 dockershim 调用,是 kubelet runtime 的子集
我们再结合最简洁的创建 pod 的流程做具体的分析,许多边界条件不做分析。
- 当用户或者 kubernetes 其他组件创建一个 pod 后,首先 API server 接收到创建 pod 的请求,为其打上一个 annotation 记录第一次见到该 pod 的时间戳。
- 调度器为 pod 选择一个节点
- kubelet 接到 apiserver 的 pod 信息更新,经过一层 admission 验证后,开始处理创建 pod 的事件,为这个 pod 分配一个 pod worker,并通过一个 pod update channel 实现异步处理
- pod worker 监听 pod update channel,接收到事件之后,判断 pod 的其中一个时间戳是否在上一轮 sync 之后。这个时间戳记录了 pod 状态变化的最后一次时间,kubelet 通过这个时间戳,防止处理过期的 pod
- 此时记录
pod_worker_start_duration_seconds
,开始对 pod 的真正的处理。至此,kubelet 上层逻辑结束,进入子模块逻辑:- 由 container manager 设置 pod 和容器的 cgroups,记录下
cgroup_manager_duration_seconds
,动作为"create"
和"update"
- 等待 pod 所有要挂载的 volume 准备就绪。这个过程是一个 poll 轮询,不记录 metrics。volume 的状态由单独的 volume manager 保证,它负责记录
storage_operation_duration_seconds
,但这个 metric 和这里 poll 的时延没有任何关系 - 开始容器有关的处理。至此,kubelet 注册给自身的子模块的逻辑结束,进入 **kubelet runtime **层:
- 创建并启动容器沙箱。runtime 层会记录
kubelet_runtime_operations
,动作为"run_podsandbox"
。由于沙箱本身是一个容器,所以该步骤会调用更底层的 CRI 接口:- 创建 pause 容器,记录
docker_operations
,动作为"create_container"
- 启动 puase 容器,记录
docker_operations
,动作为"start_container"
- 设置 pause 容器的网络,调用 CNI 接口,记录
network_plugin_operations
,动作为"set_up_pod"
- 创建 pause 容器,记录
- 检查沙箱状态,这里也会记录 metric,但不重要所以不写了
- 创建容器,记录
kubelet_runtime_operations
,动作为"create_container"
- 在 CRI 层记录
docker_operations
,动作为"create_container"
- 在 CRI 层记录
- 启动容器,记录
kubelet_runtime_operations
,动作为"start_container"
- 在 CRI 层记录
docker_operations
,动作为"start_container"
- 在 CRI 层记录
- 创建并启动容器沙箱。runtime 层会记录
- 由 container manager 设置 pod 和容器的 cgroups,记录下
- 所有处理步骤完成,pod worker 记录下
pod_worker_duration_seconds
独立于上述 pod worker 的处理过程,volume manager 不停地处理 volumes 的 mount 以及 umount,并记录storage_operation_duration_seconds
。
当某个 pod worker 处理 pod update event 时发现 pod status 第一次处于 Running 状态,则记录 pod_start_duration_seconds