Kubelet SyncLoop 工作原理:Pod 从 Watch 到容器创建的完整过程¶
文章摘要¶
上一篇 Worker Node 架构原理 从全局视角讲了 API Server → Scheduler → kubelet → CRI → containerd → Linux 这条六层链路。但 kubelet 内部是怎么运转的?它怎么知道有新 Pod 分配给自己?知道之后又怎么一步步把容器跑起来?
本文往下钻一层——拆解 kubelet 的核心循环 SyncLoop,从 Reflector 的 List + Watch 发现任务,到 PodWorkers 的串行调度,到 SyncPod 的实际执行,再到 PLEG 的持续监控,把 kubelet 内部机制讲透。
Kubelet 的源码不像 API Server 那样有清晰的请求处理链路——它更像一个大循环,不断看、不断比、不断干。社区管这个叫 SyncLoop,是 kubelet 的心脏。
阅读前建议
本文假设你已理解 Worker Node 架构 中讲到的 kubelet → CRI → containerd → runc 执行链路。如果没有,建议先读那篇再看本文。本文以 K8s 1.36.1 源码为基准。
Scheduler 只管选位,活儿都是 kubelet 的¶
回顾上一篇的核心结论:Scheduler 干的事就两件——选一个 Node,把 spec.nodeName 写回 API Server。它不拉镜像、不建容器、不配网络。一个 Pod 从 YAML 变成运行进程,Scheduler 只负责了「去哪里」这一步。
那「谁来干」呢?答案是 kubelet。
当 Scheduler 把绑定结果写回 API Server 之后,Pod 的 spec.nodeName 就变成了具体值(比如 worker01)。但这时候容器还没影——镜像没拉、Sandbox 没建、网络没配。接下来要发生的事情,全靠 worker01 上那个 kubelet。
那 kubelet 怎么知道有活来了?它不会每秒去轮询 API Server——那节点多了 API Server 早就扛不住了。它用的是 Watch 机制。
声明式系统的心脏:Reconcile Loop¶
在讲 Watch 之前,得先说一个更根本的东西。
Kubernetes 是声明式系统。你写一个 YAML 说「我要 3 个 nginx 副本」,API Server 把这个期望状态存进 etcd。然后有一堆组件负责让「实际状态」靠近期望状态。Controller Manager 干这事,kubelet 也干这事。
kubelet 的工作模式可以简化成一句话:不断比较期望状态和实际状态,有差异就去消除差异。
- 期望状态来自 API Server——PodSpec 说要一个 nginx 容器、资源限制多少、挂什么卷
- 实际状态来自容器运行时——containerd 报告容器在跑还是挂了
这个「不断比较、不断消除差异」的循环,社区叫 Reconcile Loop(调谐循环)。它是 Kubernetes 的核心设计思想——不只是 kubelet 在用,每一个 Controller 都有自己的调谐循环。区别在于 kubelet 的调谐对象是本节点的 Pod,粒度更细、实时性要求更高。
理解了这个,后面 SyncLoop 的设计就顺理成章了。
kubelet 的整体架构:一个循环连着两头¶
graph TD
APIServer["API Server"]
APIServer -->|"Watch(Pod 增删改事件)"| SyncLoop
subgraph Kubelet["kubelet"]
PodManager["Pod Manager<br/>维护本节点 Pod 的期望状态"]
SyncLoop["SyncLoop<br/>核心循环,接收各类事件"]
PodWorkers["PodWorkers<br/>按 Pod 维度串行处理"]
SyncPod["SyncPod<br/>单个 Pod 的创建/更新/删除逻辑"]
CR["Container Runtime<br/>通过 CRI 调用 containerd"]
PodManager --> SyncLoop
SyncLoop --> PodWorkers
PodWorkers --> SyncPod
SyncPod --> CR
end
PLEG["PLEG<br/>定期检查容器实际状态<br/>发现差异通知 SyncLoop"]
CR -.->|"容器实际状态"| PLEG
PLEG -.->|"容器状态变化"| SyncLoop
APIServer -.->|"期望状态"| PodManager 两头:上面是 API Server,提供期望状态;下面是 Container Runtime(containerd),提供实际状态。中间的 SyncLoop 不停地把两边对齐。
PLEG 是右下角那个独立模块,全称 Pod Lifecycle Event Generator,专门负责感知容器侧的变化——这个后面会单独讲。
kubelet 怎么发现 Pod:List + Watch¶
kubelet 启动时,会通过一个叫 Reflector 的组件跟 API Server 建立连接。Reflector 这个名字来自 client-go 的 k8s.io/client-go/tools/cache 包,它的工作模式是两步:先 List,后 Watch。
| 步骤 | 操作 | 目的 |
|---|---|---|
| List | kubelet 启动时调一次 List 接口 | 全量拉取本节点已分配的 Pod → 建立基准 |
| Watch | List 之后建立 HTTP/2 长连接 | 从本次 ResourceVersion 开始监听增量变更 |
Watch 推过来的事件类型很直接:
| 事件 | 触发条件 |
|---|---|
| ADD | 新 Pod 被调度到本节点 |
| UPDATE | Pod 配置变了(镜像版本、资源限制等) |
| DELETE | Pod 被删除 |
这些事件被封装成 PodUpdate 结构,通过一个 Channel 传给 SyncLoop。到这一步,「发现任务」的环节就完成了。
为什么不轮询?
如果 kubelet 每秒轮询一次 API Server——100 个节点就是每秒 100 次 HTTP 请求。用 Watch 只需要 100 条长连接,有变化推送,没变化安静等待。省资源得多。
SyncLoop:kubelet 的核心循环¶
这是全文重点。
SyncLoop 的入口在 kubelet 源码的 pkg/kubelet/kubelet.go 里。它是一个 select 循环,同时盯着四路输入:
flowchart LR
subgraph 输入源
PC["① PodConfig Channel<br/>API Server Watch<br/>ADD / UPDATE / DELETE"]
PL["② PLEG Channel<br/>容器运行时<br/>容器挂了 / OOM / 重启"]
RU["③ Runtime Updates<br/>CRI Runtime<br/>containerd 状态变化"]
TS["④ 定时 Sync<br/>--sync-frequency<br/>默认 1 分钟"]
end
PC --> SL
PL --> SL
RU --> SL
TS --> SL
SL["SyncLoop<br/>select 循环"]
SL -->|"统一走 SyncPod"| SP["PodWorkers → SyncPod"] | 输入源 | 来源 | 触发内容 |
|---|---|---|
| ① PodConfig Channel | API Server Watch | ADD / UPDATE / DELETE 事件 |
| ② PLEG Channel | 容器运行时 | 容器挂了、重启了、OOM 了 |
| ③ Runtime Updates | CRI Runtime | containerd 侧状态变化 |
| ④ 定时 Sync | --sync-frequency | 周期性全量同步(默认 1 分钟) |
四路输入汇总到 SyncLoop 的 select 里,谁来了处理谁。不管事件来自 API Server 还是容器运行时,处理逻辑是统一的——都走 SyncPod 这条路。
从 Watch 事件到容器创建:完整链路¶
拿一个新建 Pod 的 ADD 事件走一遍完整流程。
sequenceDiagram
participant AS as API Server
participant R as Reflector
participant SL as SyncLoop
participant HPA as HandlePodAdditions
participant PW as PodWorkers
participant SP as SyncPod
participant CR as CRI / containerd
participant CNI as CNI 插件
participant K as runc / Linux 内核
AS->>R: Watch 推送 Pod ADD 事件
R->>SL: PodConfig Channel → PodUpdate{Op: ADD}
SL->>HPA: HandlePodAdditions
HPA->>HPA: 更新 Pod Manager 期望状态
HPA->>PW: UpdatePod()
PW->>SP: SyncPod()
SP->>CR: RunPodSandbox(创建 Sandbox)
SP->>CNI: 配网络(分 IP、建 veth)
SP->>CR: PullImage → CreateContainer → StartContainer
CR->>K: Namespace / cgroup / 容器进程创建
K-->>CR: 容器进程 Running
CR-->>SP: 返回容器状态
SP-->>PW: SyncPod 完成 实验验证¶
在集群上跑一遍创建 → 删除,看 Events 的完整生命周期:
kubectl run nginx-test --image=nginx:alpine --restart=Never
kubectl delete pod nginx-test
kubectl get events --sort-by='.lastTimestamp' --field-selector involvedObject.name=nginx-test
LAST SEEN TYPE REASON OBJECT MESSAGE
7m7s Normal Scheduled pod/nginx-test Successfully assigned default/nginx-test to worker01
7m6s Normal Pulled pod/nginx-test Container image "..." already present on machine
7m6s Normal Created pod/nginx-test Container created
7m6s Normal Started pod/nginx-test Container started
11s Normal Killing pod/nginx-test Stopping container nginx-test
这五行事件刚好对应了 kubelet 的完整工作流:
Scheduled→ Scheduler 选了 worker01Pulled→ SyncPod 里调了PullImage(镜像本地已有则跳过)Created→CreateContainer完成Started→StartContainer完成Killing→ 删除流程
注意时间戳——Pulled、Created、Started 三步几乎在同一秒完成。
为什么不让 SyncLoop 直接创建容器?¶
核心原因是并发控制。
假设一个 Pod 同时收到 UPDATE 和 DELETE 两个事件——先更新配置再删除。如果 SyncLoop 直接处理,可能 UPDATE 还没创建完新容器,DELETE 就来了,两个操作交叉执行,状态就乱了。
PodWorkers 的设计解决了这个问题:
| 设计 | 说明 |
|---|---|
| 每 Pod 一个 Worker Goroutine | 同一 Pod 的事件按顺序排队处理 |
| 不同 Pod 并行 | Pod A 和 Pod B 的 Worker 互不干扰 |
| 事件去重(Dedup) | 快速更新 10 次镜像 → 只处理最新的那次 |
所以 SyncLoop 和 PodWorkers 的分工是:SyncLoop 负责收事件和派活,PodWorkers 负责按 Pod 维度串行执行。一个调度,一个执行。
SyncPod:真正干活的函数¶
PodWorkers 的 Worker goroutine 拿到事件后,最终调用的就是 SyncPod。这个函数是真正执行 Pod 创建/更新/删除逻辑的地方。
第一步:创建 Pod Sandbox¶
kubelet 通过 CRI 调用 RunPodSandbox,让 containerd 创建基础容器(Pause 容器)。这个 Sandbox 提供了 Pod 级别共享的 Network Namespace、IPC Namespace 和 UTS Namespace。CNI 插件也在这阶段介入——给 Pod 分 IP、建 veth pair、配路由。
第二步:创建 Init Container¶
如果 PodSpec 里定义了 Init 容器,kubelet 会严格按顺序逐个创建和启动。每个 Init 容器必须成功退出(exit code 0),才会创建下一个。任何一个 Init 容器失败,整个流程卡住。
第三步:创建业务容器¶
Init 容器全部就绪后,kubelet 对 containers 数组中的每个容器执行三步:
| 步骤 | CRI 调用 | 说明 |
|---|---|---|
| 1 | PullImage | 拉镜像(本地已有 + imagePullPolicy 非 Always → 跳过) |
| 2 | CreateContainer | 准备 rootfs 和挂载 |
| 3 | StartContainer | 启动容器进程 |
containerd 拿到这些 CRI 请求后,把镜像转换成 OCI Runtime Spec,交给 runc。runc 才是真正调 Linux 系统调用的那一层——创建 Namespace、设 cgroup、启动容器进程。这跟 Worker Node 架构 里讲的链路一致。
容器底层验证¶
在 worker01 上用 crictl 看看跑着的容器:
CONTAINER IMAGE CREATED STATE NAME POD ID
012ced3c3b740 ea51152ef8c48 4 minutes ago Running nginx-test ccf4b42657982
298150763592b 78282b5844742 4 weeks ago Running kube-proxy bc4219b4ba4b6
注意 Container ID 和 Pod ID 不是一回事。crictl pods 能看到 Sandbox 层面:
POD ID STATE NAME NAMESPACE
ccf4b42657982 Ready nginx-test default
bc4219b4ba4b6 Ready kube-proxy-npt2l kube-system
4a0ac310d069a NotReady calico-node-8tp5d kube-system
最后一个 calico-node 的 Sandbox 状态是 NotReady——这是之前重启留下的旧 Sandbox,新的已经 Running 了。kubelet 的 PLEG 循环会发现这种「幽灵 Sandbox」并清理掉。
看 SyncPod 设的 cgroup 限制¶
一个没配 resource limits 的 Pod,kubelet 给了它什么默认设置?
{
"cpu": { "period": 100000, "shares": 2 },
"devices": [{ "access": "rwm", "allow": false }],
"memory": {},
"unified": {
"memory.oom.group": "1",
"memory.swap.max": "0"
}
}
| 字段 | 值 | 含义 |
|---|---|---|
cpu.shares | 2 | 最低权重——CPU 竞争中几乎拿不到资源 |
memory | {} | 没设上限(无 resource limits 时) |
memory.swap.max | 0 | 关掉 swap |
devices.allow | false | 默认禁止访问所有设备 |
如果你在 YAML 里写了 resources.limits.cpu: 500m,这里会变成 cpu.quota: 50000 / cpu.period: 100000(50ms/100ms = 0.5 核)。
到这里,Pod 的容器就算跑起来了。但 kubelet 的工作没完——它还得持续监控这些容器的状态。这就引出了 PLEG。
PLEG:kubelet 的「眼睛」¶
创建完容器之后,kubelet 面临一个问题:怎么知道容器还活着?
API Server 推送的事件只反映「期望状态」的变化。但容器自己挂了、OOM 了、被手动 kill 了——这些是容器运行时那边发生的事,API Server 不会主动通知。
kubelet 需要一双眼睛盯着容器侧。这就是 PLEG(Pod Lifecycle Event Generator)。
sequenceDiagram
participant PLEG as PLEG
participant CR as containerd (CRI)
participant SL as SyncLoop
participant SP as SyncPod
loop 每 1 秒 relist
PLEG->>CR: ListPodSandbox + ListContainers
CR-->>PLEG: 返回当前所有容器真实状态
PLEG->>PLEG: 与上一次状态对比
alt 有差异(容器挂了 / OOM / 重启)
PLEG->>SL: PodLifeCycleEvent(via Channel)
SL->>SP: 触发 SyncPod
SP->>SP: 实际状态对齐期望状态
else 无差异
PLEG->>PLEG: 等待下一秒
end
end PLEG 的工作机制¶
| 阶段 | 操作 |
|---|---|
| ① Relist | 每隔 1 秒(源码硬编码)调用 CRI 的 ListPodSandbox + ListContainers |
| ② 对比 | 与内存中上一次记录的状态做 diff |
| ③ 生成事件 | 有差异 → 生成 PodLifeCycleEvent → 塞进 Channel → SyncLoop 收到通知 → 触发 SyncPod |
例如一个 nginx 容器 OOM 挂了,PLEG 发现它从 Running → Exited,通知 SyncLoop → SyncLoop 触发 SyncPod → SyncPod 发现「期望 = Running,实际 = Exited」→ 重新拉镜像、创建容器、启动。这就是容器自动重启的底层机制——不是 Controller Manager 远程重启的,是本节点 kubelet 通过 PLEG 感知后自己处理的。
为什么看不到 PLEG 日志?¶
默认日志级别(--v=0)下 PLEG 不输出日常操作日志。这很正常——PLEG 每秒 relist 一次,如果每次都输出日志,一天就是 86400 行无意义的日志。只有异常时(如 relist 超时)才会输出 PLEG is not healthy。
PLEG 不健康会怎样?¶
生产环境里你可能见过这种告警:
PLEG 的 relist 循环每秒执行一次,但 containerd 响应 ListContainers 的速度不是 kubelet 能控制的。如果容器数量多、containerd 负载高、底层存储卡顿(containerd 要读 overlayfs 的状态),一次 relist 可能要好几秒甚至几十秒。
| 阈值 | 行为 |
|---|---|
| 单次 relist > 几秒 | 正常延迟——不告警 |
| 连续 3 分钟无成功 relist | kubelet 标记 PLEG 不健康 → Node → NotReady |
Node 一旦 NotReady:
- Scheduler 不再往此节点调度新 Pod
- 已有 Pod 继续运行(容器没挂,只是 kubelet 失去感知能力)
- Pod 手动
crictl ps看起来都正常,但 kubelet 上报的状态已不正常
排查方向:看 kubelet 日志和 containerd 日志,不是去看 API Server。根因通常是容器运行时层面的问题。
Pod 删除时 kubelet 在干什么?¶
删除 nginx-test 后,worker01 上的 kubelet 日志:
I0724 22:38:50.306 scope.go:122] "RemoveContainer" containerID="731ceb..."
I0724 22:38:50.313 scope.go:122] "RemoveContainer" containerID="731ceb..."
E0724 22:38:50.314 remote_runtime.go:569] "ContainerStatus failed"
err="rpc error: code = NotFound desc = ... not found"
I0724 22:38:50.623 kubelet_volumes.go:161] "Cleaned up orphaned pod volumes dir"
sequenceDiagram
participant AS as API Server
participant SL as SyncLoop
participant PW as PodWorkers
participant SP as SyncPod
participant CR as containerd (CRI)
participant FS as 文件系统
AS->>SL: Watch 推送 Pod DELETE 事件
SL->>PW: HandlePodDeletions
PW->>SP: SyncPod(删除模式)
SP->>CR: UnmountVolume(卸载挂载)
SP->>CR: TearDown(拆 Volume)
SP->>CR: RemoveContainer(删容器)
CR-->>SP: 容器已删除
Note over SP,CR: PLEG relist 发现容器消失
SP->>CR: ContainerStatus(再次确认)
CR-->>SP: NotFound(幂等,不再重试)
SP->>FS: Cleaned up orphaned pod volumes dir | 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | UnmountVolume | 卸载 Volume 挂载 |
| 2 | TearDown | 拆除 Volume |
| 3 | RemoveContainer | 删除容器 |
| 4 | ContainerStatus(二次确认) | PLEG relist 发现容器消失 → 再查一次 → 返回 NotFound → 幂等,不再重试 |
| 5 | Clean up orphaned volumes | 清理 Pod 的 Volume 目录 |
注意 RemoveContainer 出现了两次——第二次是 PLEG relist 发现的,但返回了 NotFound,kubelet 知道已经清理完了,不再重试。这不算错误。
从删除到清理完毕通常不到 1 秒。如果 Volume 用的网络存储(NFS、Ceph),这个时间可能拉长,这时候 PLEG relist 就可能超时。
生产环境故障排查¶
Pod 一直卡在 ContainerCreating¶
Pod 卡在 ContainerCreating 意味着 SyncPod 流程里某一步走不下去了。kubectl describe pod 的 Events 部分会告诉你卡在哪:
| Events 中的关键字 | 卡在哪一步 | 常见根因 |
|---|---|---|
Failed to pull image | ImagePull | 镜像仓库不通 / Tag 错误 / 认证失败 |
MountVolume.SetUp failed | Volume 挂载 | PVC 未绑定 / StorageClass 不匹配 / CSI 问题 |
| CNI related error | 网络配置 | CNI 插件异常——需看 Node 上 kubelet 日志 |
kubelet 日志 vs 控制面日志
控制面组件(API Server、Controller Manager、Scheduler)是静态 Pod,不走 systemd,所以 journalctl -u kube-apiserver 查不到日志。但 kubelet 自己是 systemd 服务,journalctl -u kubelet 可以直接用。
Node 突然变 NotReady¶
| 日志关键信息 | 可能原因 | 排查命令 |
|---|---|---|
PLEG is not healthy | 容器运行时响应慢 | systemctl status containerd + containerd 日志 |
container runtime not ready | kubelet 与 containerd 通信异常 | systemctl status containerd |
| 无特殊日志、心跳超时 | 网络问题或 kubelet 挂了 | systemctl status kubelet |
回顾整条链路¶
从 Watch 到容器创建到 PLEG 监控,完整链路串起来:
flowchart TD
AS["API Server<br/>(期望状态)"]
R["Reflector<br/>List + Watch"]
SL["SyncLoop<br/>(select 循环)"]
PW["PodWorkers<br/>(按 Pod 串行)"]
SP["SyncPod<br/>(CRI 调用)"]
CR["containerd<br/>Sandbox / 镜像 / 容器"]
PLEG["PLEG<br/>(每秒 relist)"]
AS -->|"PodUpdate Channel"| R
R --> SL
SL --> PW
PW --> SP
SP -->|"RunPodSandbox<br/>PullImage<br/>CreateContainer<br/>StartContainer"| CR
CR -->|"容器实际状态"| PLEG
PLEG -->|"PodLifeCycleEvent"| SL
SP -.->|"上报状态"| AS - API Server 存了 Pod 的期望状态
- kubelet 通过 Reflector 的 List + Watch 拿到本节点 Pod 事件
- 事件进入 SyncLoop → 派给 PodWorkers
- PodWorkers 按 Pod 维度串行调用 SyncPod
- SyncPod 通过 CRI 让 containerd 创建 Sandbox → 拉镜像 → 建容器
- 容器跑起来后,PLEG 每秒轮询容器状态,发现变化通知 SyncLoop → 触发新一轮 SyncPod
- 循环不停运转,Pod 的实际状态持续向期望状态对齐
总结¶
上一篇 Worker Node 架构 停在六层链路的架构理解层面。本文往下钻了一层——kubelet 内部怎么运转:
- Reflector(List + Watch):发现任务——不轮询,用长连接监听 Pod 变更
- SyncLoop(select 四路输入):核心调度——API Server 事件、PLEG 事件、Runtime 事件、定时 Sync,统一走 SyncPod
- PodWorkers:并发控制——每 Pod 串行、Pod 间并行、事件去重
- SyncPod:真正执行——Sandbox → Init → 容器三步,每步对应 CRI 调用
- PLEG(每秒 relist):容器的眼睛——从 containerd 拉真实状态,发现偏差通知 SyncLoop
再往下就是 CRI 的具体接口实现了——kubelet 调 containerd 的那层 gRPC 接口长什么样,SyncPod 里那些 CRI 调用的参数和返回值是什么意思。
相关阅读¶
| 文章 | 说明 |
|---|---|
| Worker Node 架构原理 | 全局视角——六层执行链路 |
| Kubelet 工作原理详解 | kubelet 组件级深度解析 |
| CRI 容器运行时接口 | kubelet 与 Runtime 之间的标准接口 |
| Kubernetes 架构总览 | 控制面与 Node 全景图 |
| 容器底层原理 | Linux Namespace + cgroup 动手实验 |