kubelet 源码分析:从 syncLoop 到容器创建的全链路

kubelet 源码分析:从 syncLoop 到容器创建的全链路

9 分钟阅读

这是一篇 kubelet 源码阅读笔记,主要回答一个问题:一个 Pod 从被调度到节点上,到容器跑起来,kubelet 内部到底走了哪些代码路径? 顺着这条主线,把 kubelet 的核心模块、syncLoop 调度、对 CRI 的调用方式串起来。代码版本基于 kubernetes 1.22 前后。


kubelet 的核心模块

kubelet 是 K8s 节点上的"代理人",承载了一堆并行的子系统。从代码组织上,最核心的几个 manager 是:

  • cloud provider sync manager — 与底层云平台同步节点信息
  • volume manager — 管理 Pod 的 volume 挂载/卸载
  • node status sync — 上报节点心跳/状态
  • node lease sync — 通过 Lease 对象做轻量心跳(替代旧的 NodeStatus 上报)
  • runtime update sync — 同步运行时(CRI)状态
  • status manager — 维护 Pod 状态
  • Pod Lifecycle Event Generator (PLEG) — 周期性 relist 容器状态,把变化推到 syncLoop
  • main loop sync (syncLoop) — 所有事件最终汇集到这里处理

这些模块大部分都是 goroutine 跑在后台,通过 channel 和共享状态把信息汇到 syncLoop

syncLoop:一切的入口

syncLoop 是 kubelet 的"事件循环",所有的处理逻辑都从这里发起。它通过 select 监听多个事件源:

事件源 含义
pod 更新事件 apiserver/file/http 三个 source 推过来的 Pod 变更
pleg 事件 PLEG 检测到的容器状态变化
liveness 事件 存活探针失败
readiness 事件 就绪探针变化
sync 事件 周期性全量 sync(兜底)
startup 事件 启动探针
housekeeping 事件 清理孤儿 volume、过期容器等

Pod 更新通道的容量是写死的 50:

updates := make(chan kubetypes.PodUpdate, 50)

事件源有三种:

  • file — 文件系统监听(用于 static pod)
  • apiserver — 主路径,从 apiserver list/watch
  • http — 通过 HTTP 接口推送

从 syncLoop 到 syncPod

任何一个事件最终都会调用 syncPod,这是 Pod 创建/更新的主入口:

syncPod
├── create cgroup and apply resources       (CgroupManager)
├── create mirror pod for static pod        (用于 static pod 在 apiserver 留个影像)
├── make data directory for pod             (/var/lib/kubelet/pods/<uid>)
├── wait for volume attach                  (VolumeManager.WaitForAttachAndMount)
├── fetch pull secrets for pod              (从 secret manager 取镜像凭证)
└── invoke runtime's syncPod                (CRI 调用主路径)

对应的,Pod 退出时走的是另外两个函数:

  • syncTerminatingPod — Pod 正在终止(已发 SIGTERM,等待 graceful shutdown)
  • syncTerminatedPod — Pod 已经完全终止(清理资源、上报状态)

runtime syncPod:sandbox 与 container 的编排

进入 runtime 层后,kubelet 会做一次"差异计算",决定要创建哪些 sandbox、起哪些容器:

runtime syncPod
├── Compute sandbox and container changes (生成 change actions)
├── Kill pod sandbox if necessary
├── Kill any containers that should not be running
├── Create sandbox if necessary               → 拿到 podSandboxID
├── Create ephemeral containers              使用 podSandboxID
├── Create init containers                   使用 podSandboxID
└── Create normal containers                 使用 podSandboxID

每一个容器的启动走 startContainer

startContainer
├── pull the image                         (CRI: PullImage)
├── create the container                   (CRI: CreateContainer)
├── start the container                    (CRI: StartContainer)
└── run the post-start lifecycle hooks

切到 CRI 一侧:以 containerd 实现为例

kubelet 通过 CRI gRPC 把请求转给具体的运行时,主流实现是 containerd。下面看 containerd 这边怎么响应。

RunPodSandbox

cri.RunPodSandbox
├── 生成 id,生成 sandbox name,放入 index 避免并发请求
├── 创建 sandbox 对象,state 默认 unknown
├── 确保 sandbox 镜像存在 (pause 镜像)
├── 取 OCI runtime (runc/kata/firecracker/gvisor),默认 runc
├── (如果需要:!windows && !hostNetwork) 网络初始化
│   ├── 创建网络命名空间
│   │   ├── unix.Unshare(unix.CLONE_NEWNET)
│   │   ├── unix.Setns
│   │   └── unix.Mount
│   └── setupPodNetwork: invoke CNI plugins
└── create sandbox container
    ├── create OCI spec and generate opts
    ├── invoke c.client.NewContainer
    ├── create root directory for sandbox
    ├── setup files: /dev/shm, /etc/hosts, /etc/resolv.conf, /etc/hostname
    └── create sandbox task in containerd, wait & start task, update status

值得注意的是网络命名空间的创建 — 用 unshare(CLONE_NEWNET) 创建一个新 netns,然后 mount 到一个 bind path 上让它持久存在,最后调用 CNI 插件在这个 netns 里配置网络。

CreateContainer

cri.CreateContainer
├── 根据 sandbox id 从 sandboxStore 拿到 sandbox 对象
├── generate container name
├── 取得 image 镜像
├── 创建 container root directory
├── 创建 image volumes mounts
├── 根据 sandbox 拿到 OCI runtime,生成 container spec 和 opts
└── containerstore.NewContainer & Add container into container store

这一步只是把容器"准备好",并没有真正起。

StartContainer

cri.StartContainer
├── 从 containerstore 根据 container id 取出 container 对象
├── 从 sandboxStore 根据 sandbox id 取出 sandbox 对象
├── 检查 sandbox 是否是 ready 状态
└── task: new → wait → start

CRI 往下调用 containerd 内部的 services(containers、tasks…),最终通过 shim 调用到 runtime/v2 的 runc 实现:

runtime/v2/runc/container.go: NewContainer
├── newInit return process (runc.Init)
├── p.Create invoke runc.Create, return pid
└── set cgroup

runc.Create 本质就是对 runc 二进制命令的封装。

Cgroup 的设置

容器创建时的 cgroup 资源限制设置走这条路:

  • 取得标准资源值:cpu、memory、ephemeral storage、hugepage/* 等
  • 从 node config 上取得 pidLimit
  • 调用 runc/libcontainer/cgroup 设置

Pod 关闭:killPod 链路

Pod 的删除路径走 killPod

runtime killPod
├── killContainersWithSyncResult (按顺序 kill 每个容器)
│   └── killContainer
└── runtimeService.StopPodSandbox

killContainer 里有一个值得注意的细节:gracePeriod 的计算优先级

gracePeriod 优先级(从高到低):
1. pod.DeletionGracePeriodSeconds                          (kubectl delete --grace-period)
2. probe.TerminationGracePeriodSeconds
     ├── StartupProbe.TerminationGracePeriodSeconds
     └── LivenessProbe.TerminationGracePeriodSeconds
3. pod.Spec.TerminationGracePeriodSeconds                  (Pod YAML 里写的)
4. minimumGracePeriodInSeconds: 2s                         (硬下限)

调用流程:

killContainer
├── pre-stop lifecycle hook (gracePeriod 减去 hook 执行的时间,不能小于 minimum)
└── runtimeService.StopContainer

到 containerd 一侧:

cri.StopContainer → stopContainer
├── 根据 container id 从 containerstore 中取得 container
├── only kill state: running and unknown
├── 判断 timeout > 0
│   └── stopSignal = container.StopSignal || "SIGTERM"
├── task.Kill and waitContainerStop
└── 如果关闭失败:再次 task.Kill with syscall.SIGKILL and waitContainerStop

最终的 task.Kill 走 shim 调用,落到 runc.process.kill 杀掉容器进程。

全链路一张图

把上面的链路串起来:

kubectl ──→ apiserver ──→ kubelet (syncLoop) ──→ CRI ──→ containerd ──→ containerd-shim ──→ OCI ──→ runc/kata

containerd architecture

Linux Namespace 复习

最后顺手列一下容器隔离用到的 namespace 类型:

Namespace 隔离
pid 进程号
network 网络栈、网络设备、路由表、防火墙
mount 挂载点
user 用户和组
ipc System V IPC、POSIX 消息队列
uts 主机名、域名
cgroup cgroup 根目录视图

小结

读完 kubelet 这一圈最大的感受是:Kubernetes 的复杂度有相当一部分被 kubelet 单点承载了。它要协调 CRI、CNI、CSI、cgroup、volume、网络命名空间、各种探针、容器状态……几乎是节点上的"小操作系统"。

而 syncLoop 这个简单的事件循环加上 PLEG 的状态机,是把这一切粘起来的关键设计。后来我们在 PLEG NotReady 问题上反复踩坑,根因都能追到 syncLoop 这条主线被某个 RPC 卡住时的连锁反应——所以读懂这条路径,是排查 kubelet 故障的基础功。

后续会继续展开 PLEG 内部、volume manager、status manager 这几个模块的细节,与之前发布的 短任务场景下 PLEG NotReady 排查 形成互补阅读。

Zoe

Written by

Zoe

AI Infra Engineer · LLM Serving · GPU/RDMA · 造工具的偏执狂

评论