kubelet metrics深入解析

1,823 阅读10分钟

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
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 的 channel
  • plegCh:PLEG 通过 list & watch 机制监听 apiserver 处的 pod 变化,产生 event
  • syncCh:生成于上面的 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)

注:

  1. 在 update cgroups 的过程中,一路调用到 pkg/kubelet/cm/cgroup_manager_linux.go 中的 func (m *cgroupManagerImpl) Update(cgroupConfig *CgroupConfig) error 函数,在该函数结束之后,会记录 cgroup update 操作的时延,从进入 Update() 函数开始,到函数结束。由于 cgroups 可能有多个 update 操作,这里可能会记录多个时延。该操作并没有调用 runc 的 API,而是通过 setSupportedSubsystems 进行系统调用完成
  2. 这里会记录 cgroup create 的时延,这些操作通过 runc 的 libcontainer 库与系统调用完成。疑问:为什么 create 操作在 update 操作之后?这些 cgroup 的操作并不太理解透彻
  3. 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:

  1. Compute sandbox and container changes.

  2. Kill pod sandbox if necessary.

  3. Kill any containers that should not be running.

  4. Create sandbox if necessary.

  5. Create init containers.

  6. 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)
  1. 首先,创建沙箱。沙箱其实就是一个 pause 容器。该过程记录了 kubelet_runtime_operations 中的 operation_type="run_podsandbox" 的时延,从进入 RunPodSandbox 开始,到函数结束,涉及 runtime 的真正处理,与 grpc 的时延。注意记录的层次在 kubelet runtime 层,而不在 CRI 层。
  2. 之后,获取沙箱状态。与上面类似
  3. 第三步,创建并启动容器。注意 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(//...)

注:

  1. 这里有个 client 的概念,dockershim 封装了 instrumented_client,而后者封装了真正沟通 docker 的 grpc client。这个 instrumented_client 在执行操作的时候会记录 docker_operations。在 create container 操作中记录 "create_container",在 start container 中记录 "start_container"
  2. 调用 CNI 接口的时候会记录 network_plugin_operations 这个 metric,SetUpPod 中记录了 "set_up_pod" 这个动作的时延。

metrics整理

kubelet 常用的 metrics 与他们之间的关系整理如下:

  • pod_worker_duration_secondspod_worker_latency_microseconds:从进入 kubelet 的 handler 开始,到 pod worker 处理结束。它不代表 pod 的启动时间,只代表一个 pod update 事件在 kubelet 中的处理时间

  • pod_worker_start_duration_secondspod_worker_start_latency_microseconds:从 API server 第一次处理 pod 开始,到 pod worker 在 syncPod 中处理 SyncPodCreate 事件

  • pod_start_duration_secondspod_start_latency_microseconds:从 API Server 第一次处理 pod 开始,到 kubelet 处理 pod update 事件时看到 pod 状态为 running

  • cgroup_manager_duration_secondscgroup_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 的流程做具体的分析,许多边界条件不做分析。

  1. 当用户或者 kubernetes 其他组件创建一个 pod 后,首先 API server 接收到创建 pod 的请求,为其打上一个 annotation 记录第一次见到该 pod 的时间戳。
  2. 调度器为 pod 选择一个节点
  3. kubelet 接到 apiserver 的 pod 信息更新,经过一层 admission 验证后,开始处理创建 pod 的事件,为这个 pod 分配一个 pod worker,并通过一个 pod update channel 实现异步处理
  4. pod worker 监听 pod update channel,接收到事件之后,判断 pod 的其中一个时间戳是否在上一轮 sync 之后。这个时间戳记录了 pod 状态变化的最后一次时间,kubelet 通过这个时间戳,防止处理过期的 pod
  5. 此时记录 pod_worker_start_duration_seconds,开始对 pod 的真正的处理。至此,kubelet 上层逻辑结束,进入子模块逻辑:
    1. 由 container manager 设置 pod 和容器的 cgroups,记录下 cgroup_manager_duration_seconds,动作为 "create""update"
    2. 等待 pod 所有要挂载的 volume 准备就绪。这个过程是一个 poll 轮询,不记录 metrics。volume 的状态由单独的 volume manager 保证,它负责记录 storage_operation_duration_seconds,但这个 metric 和这里 poll 的时延没有任何关系
    3. 开始容器有关的处理。至此,kubelet 注册给自身的子模块的逻辑结束,进入 **kubelet runtime **层:
      1. 创建并启动容器沙箱。runtime 层会记录 kubelet_runtime_operations,动作为 "run_podsandbox"。由于沙箱本身是一个容器,所以该步骤会调用更底层的 CRI 接口:
        1. 创建 pause 容器,记录 docker_operations,动作为 "create_container"
        2. 启动 puase 容器,记录 docker_operations,动作为 "start_container"
        3. 设置 pause 容器的网络,调用 CNI 接口,记录 network_plugin_operations,动作为 "set_up_pod"
      2. 检查沙箱状态,这里也会记录 metric,但不重要所以不写了
      3. 创建容器,记录 kubelet_runtime_operations,动作为 "create_container"
        1. 在 CRI 层记录 docker_operations,动作为 "create_container"
      4. 启动容器,记录 kubelet_runtime_operations,动作为 "start_container"
        1. 在 CRI 层记录 docker_operations,动作为 "start_container"
  6. 所有处理步骤完成,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