我翻了不少 K8s 入门资料,发现一个普遍现象——讲控制面的文章铺天盖地,API Server、etcd、Scheduler 讲得头头是道,但写到 Worker Node 就一笔带过,好像"节点嘛,就是跑 Pod 的地方",完事了。
我自己在学这块的时候卡过一阵。Pod 卡在 ContainerCreating 到底卡在哪一层?kubelet 挂了容器会不会跟着没?crictl 看到的容器和 kubectl 看到的不一样是怎么回事?这些问题靠读文档读不出答案,得去 Worker Node 的执行链路里找。
这篇整理一下我从 Scheduler 绑定节点到 Linux 内核创建容器进程这条链路里学到的东西。有些地方我可能理解得不够透彻,如果有误欢迎指出。
有的 人以为 Scheduler 调度完,Pod 就跑起来了。不是。
Scheduler 只干了两件事:选一个 Node,把 spec.nodeName写回 API Server。它不拉镜像、不创建容器、不配网络。打个比方,Scheduler 是分派工单的调度员——它把工单递给车间,但车间的活儿谁来干、怎么干,它管不着。
真正让 Pod 从 YAML 变成运行进程的,是 Worker Node 上的一串组件。

一个 Worker Node 上跑着三个核心组件:
kubelet是每个 Node 上的常驻代理。它通过 Watch API Server 发现"有新 Pod 被分配到我这了",然后开始干活——拉镜像、建容器、挂网络、做健康检查、定期上报状态。你可以理解为车间里的班组长,拿到工单后协调各个工位。
Container Runtime是真正运行容器的软件。现在主流是 containerd,CNCF 毕业项目,Kubernetes 默认推荐。CRI-O 是另一个选择,专为 K8s 设计,更轻量。Docker Engine 从 1.24 开始不再直接被 K8s 支持,要走 cri-dockerd 适配层——但 Docker 构建的 OCI 镜像格式仍然是通用的,你的 Dockerfile 不用改。
kube-proxy管 Service 网络,用 iptables 或 nftables 规则把 Service 的 ClusterIP 流量转发到后端 Pod。它不直接参与 Pod 创建,但没有它,Pod 之间通过 Service 名字互访就不通。
加上 CNI 插件(Calico、Flannel 这些,管 Pod 网络)和 CSI 插件(管存储挂载),就构成了完整的 Worker Node。
(这一层的整体架构图我放在站点上了,感兴趣可以点击“阅读原文”看看。

这是全文的核心。我用一个 kubectl apply -f nginx.yaml的例子,把完整链路走一遍。
第一层:API Server 收到请求。kubectl 把 YAML 发给 API Server,经过认证、授权、准入三道关,最终写入 etcd。这时候 Pod 只是个存在 etcd 里的对象,spec.nodeName还是空的。
第二层:Scheduler 选节点。Scheduler 的 Watch 机制发现有未调度的 Pod,开始两阶段调度——先 Filter,过滤掉资源不足、Taint 不匹配、NodeSelector 不符的节点;再 Score,对剩余节点打分,选出最优解。然后把 spec.nodeName设为 worker-node-01,写回 API Server。
到这里为止,容器还没影。接下来才是 Worker Node 的事。
第三层:kubelet 发现任务。worker-node-01上的 kubelet 通过 Watch API Server 看到有个新 Pod 分给了自己,进入 Pod 同步循环(SyncLoop)。
第四层:kubelet 先创建 Pod Sandbox。它通过 CRI 调用 RunPodSandbox,创建基础设施容器(就是那个 Pause 容器)。这个 Sandbox 提供了 Pod 级别共享的 Network Namespace、IPC Namespace、UTS Namespace 和 cgroup。CNI 插件也在这阶段被调用——给 Pod 分 IP、建 veth pair、配路由。
第五层:kubelet 逐个创建业务容器。Sandbox 就绪后,kubelet 按"先 Init 容器后普通容器"的顺序,对每个容器调用 CRI 的 PullImage拉镜像、CreateContainer准备 rootfs、StartContainer启动。
containerd 拿到指令后,把镜像转成 OCI Runtime Spec,交给 runc。runc 才是真正调 Linux 系统调用的那个——它创建 Namespace、设 cgroup、启动容器进程。
第六层:Linux 内核。PID Namespace 让容器内 ps只看到自己的进程,Network Namespace 给 Pod 独立 IP,cgroup 把 resources.limits.cpu映射到 cpu.max、resources.limits.memory映射到 memory.max。到这一步,容器进程才算真正跑起来了。
容器跑起来之后,kubelet 还得持续干活——向 API Server 上报 Pod 状态从 Pending 变成 Running,执行 Liveness/Readiness Probe,每 10 秒上报一次 Node 心跳。
六层协作,任何一层出问题,Pod 就卡住。
"Pod 是 kubelet 创建的"—— 严格来说不是。kubelet 是项目经理,接收任务、分解任务、调用 CRI 要求 Runtime 执行。真正调 Linux 系统调用创建 Namespace 和 cgroup 的是 runc。kubelet 自己不碰内核。
"kubelet 挂了 Pod 就死了"—— 这个我实际试了一下。
一开始我在 worker01 上想验证这个结论,结果 kubectl get pod直接报错:
E0723 22:12:04.170725 1008027 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: Get \"http://localhost:8080/api?timeout=32s\": dial tcp [::1]:8080: connect: connection refused"
The connection to the server localhost:8080 was refused - did you specify the right host or port?
worker 节点上没配 kubeconfig,kubectl 默认连 localhost:8080,而 API Server 不在那儿。这个问题卡了我一下——后来意识到要看 Pod 状态得回 master 上操作。
于是我切到 master01 上,sudo systemctl stop kubelet,kubelet 停了,状态显示 inactive (dead),之前连续跑了 4 周多:
○ kubelet.service - kubelet: The Kubernetes Node Agent
Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; preset: enabled)
Active: inactive (dead) since Thu 2026-07-23 22:14:23 CST; 7s ago
Duration: 4w 1d 5h 20min 44.191s
然后 kubectl get pod看,所有 Pod 状态还是 Running:
NAME READY STATUS RESTARTS AGE
nginx-f79c7c654-24gsk 1/1 Running 0 8d
nginx-f79c7c654-kfb52 1/1 Running 1 (7d11h ago) 8d
nginx-f79c7c654-kgns4 1/1 Running 0 8d
recovery-nginx 1/1 Running 1 (60d ago) 65d
再用 crictl ps看实际容器,全都在——这里要注意,crictl是直接跟 containerd 通信的,不走 API Server,所以 worker 节点上也能用:
CONTAINER IMAGE NAME POD ID
cebdfe6456a25 315157c5c4d76 kube-apiserver 58d63287142c0
c41dbe851f77e c2616bc3c6956 kube-controller-manager 8aba2ccfbb3e3
852309ee06968 b665198f31ae0 kube-scheduler d68b61036a78
fac0f695a7850 78282b5844742 kube-proxy 910ee2195a75d
7bb370ae166b6 ee85eb1f0edd2 etcd e73993905c81a
2ba9a00633eb6 e74b80806e1da node-exporter 478067df8ca9e
e0be0d56181ad f3bc59d46b56d calico-node 214ed2114dbfc
容器一个没停。连 kube-apiserver、etcd 这些控制面组件都还在跑——因为它们也是 containerd 管的容器,跟 kubelet 没有绑定关系。
当然,kubelet 停了之后新 Pod 没法创建了,因为没人接收 PodSpec。Node 心跳超时(默认 40 秒)后状态会变 NotReady,再过宽限期(默认 5 分钟)控制面会把 Pod 标记为 Terminating,在其他健康节点上重建——前提是 Pod 属于 ReplicaSet 或 Deployment 这类控制器。
这个实验建议在自己集群上试一遍,体感跟读文档完全不一样。记得在测试节点上做,别在生产上玩。
"Docker 从 K8s 消失了"—— Dockershim 在 1.24 被移除,Kubernetes 不再直接支持 Docker Engine 作为 Runtime。但 Docker 构建的 OCI 镜像格式仍然是行业标准,Dockerfile 照写,镜像仓库照用。变的只是运行时那一层:以前是 K8s → Docker → containerd → runc,现在直接 K8s → containerd → runc,少了一层。
kubelet 配置文件在 /var/lib/kubelet/config.yaml,有几个参数我建议你认真看一下:
maxPods默认 110,如果你的节点规格大(比如 32 核 64G),可以适当调高。但注意每个 Pod 会给 kubelet 和 etcd 增加 watch 压力,不是越大越好。
evictionHard是硬驱逐阈值,memory.available: "500Mi"表示节点可用内存低于 500Mi 时开始驱逐 Pod。这个值按你节点规格调,大机器设太小会导致内存充足时就开始驱逐。
imageGCHighThresholdPercent和 imageGCLowThresholdPercent控制镜像垃圾回收——磁盘使用超过 85% 开始清理不用的镜像,降到 80% 停止。磁盘紧张的节点可以适当调低。
系统资源预留别忽略。--system-reserved和 --kube-reserved给系统进程和 K8s 组件留 CPU 和内存,不然极端情况下 Pod 把资源吃光,系统进程和 kubelet 自己都跑不动。
光看文章不动手,过几天就忘。几个命令你在自己集群上跑一遍:
crictl ps看 Node 上实际运行的容器进程,跟 kubectl get pod对比着看。crictl pods --name <pod名>能拿到 Pod Sandbox ID,crictl inspect <容器ID> | jq .info.runtimeSpec.linux.resources能看到容器的 cgroup 限制实际值——不过这个命令输出特别长,我试的时候直接复制都复制不过来,建议加 jq 过滤。
ls -la /proc/<PID>/ns/看容器进程的 Namespace,你会发现同一个 Pod 里的容器共享同一个 network namespace。
理解 Worker Node 的执行链路,不光是为了面试答题。
排查 Pod 卡在 ContainerCreating 时,你得知道卡在哪一层——是镜像拉不下来、CNI 网络插件出错、还是 Storage 挂载失败,排查方向完全不同。kubelet 日志、crictl 输出、容器运行时日志,对应的是链路的不同位置。
下一篇想拆开讲 kubelet 的 SyncLoop 内部机制——Pod 是怎么从"被 Watch 到"到"进入创建流程"的,中间有个 PLEG 循环,那块值得单独说。
你目前集群用的什么容器运行时,containerd 还是还挂着 Docker?如果遇到过 Pod 创建卡住的问题,卡在哪一层?