kubelet 是 Kubernetes 里唯一在每个节点上都运行的核心组件——它把 API Server 上的 PodSpec「翻译」成容器运行时的真实操作,同时负责探针执行、资源统计、驱逐决策与状态回传。绝大多数节点级故障(Pod 卡在 ContainerCreating、PLEG is not healthy、节点 NotReady、容器莫名被杀)根因都落在 kubelet 的某条链路上。本文沿着 kubelet 的内部处理链路逐层拆解:Pod 同步 → PLEG → CRI → 探针 → 驱逐 → cgroup,并给出每一环可落地的排障命令与参数。
1. kubelet 的职责与启动参数
1.1 kubelet 做什么、不做什么
kubelet 是节点代理(node agent),不是控制器。它只对已经绑定到本节点(spec.nodeName 等于自己)的 Pod 负责,具体职责包括:
- 通过 list-watch 从 API Server 同步 Pod、ConfigMap、Secret、PVC 等对象;
- 调用 CRI 创建/启动/停止容器,调用 CSI 挂载卷,调用 CNI 配置网络;
- 周期性执行 liveness/readiness/startup 探针;
- 采集节点与容器资源用量,回写 Node Status 与 Pod Status;
- 在节点资源告急时驱逐 Pod,保护节点本身不崩溃。
它不负责调度(那是 kube-scheduler 的事)、不负责创建 Pod(那是控制器的 Reconcile 结果)、也不负责服务发现。理解这条边界,很多「Pod 没起来是不是 kubelet 的锅」的争论就有答案了。
1.2 关键启动参数
kubelet 的参数分三类:配置文件(--config,推荐)、命令行 flag、以及被弃用的 KubeletConfiguration 子字段。
# 常见的 kubelet 启动参数(systemd 单元或 kubeadm 生成的配置)
--config=/var/lib/kubelet/config.yaml # 主配置(KubeletConfiguration)
--kubeconfig=/etc/kubernetes/kubelet.conf # 访问 API Server 的凭证
--container-runtime-endpoint=unix:///run/containerd/containerd.sock
--pod-infra-container-image=registry.k8s.io/pause:3.9
--node-ip=10.0.1.12
--root-dir=/var/lib/kubelet
config.yaml 里的高频调优项:
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
address: 0.0.0.0
readOnlyPort: 0 # 只读端口默认关闭,安全基线要求
authentication:
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
mode: Webhook # 走 SubjectAccessReview,配合 RBAC
cgroupDriver: systemd # 与容器运行时保持一致,否则容器会被双重管理
serializeImagePulls: false # 并行拉镜像,加速大批量启动
maxPods: 110
evictionHard:
memory.available: "100Mi"
nodefs.available: "10%"
一句话:
cgroupDriver与运行时不一致、readOnlyPort未关、maxPods拍脑袋,是 kubelet 配置里最常见的三个坑。
2. list-watch 与 Pod 同步循环
2.1 数据来源不是「查询」而是「缓存」
kubelet 不为每个 Pod 单独去 API Server 查询,而是启动时先 LIST 全量对象填充本地缓存,再 WATCH 增量更新。这套机制叫 informer,由 client-go 提供。
LIST /api/v1/pods?fieldSelector=spec.nodeName=<node> → 全量填充 DeltaFIFO
WATCH 同一 selector → 增量事件入队
↓
本地 store(thread-safe cache)
↓
syncLoop 从 work queue 取 Pod → 执行同步
关键点:watch 断线会触发重新 LIST(relist),这也是 API Server 压力突增的常见来源。当集群出现大量 too many requests 或 apiserver 的 watch 指标飙升时,往往某个 kubelet 的缓存反复重建。
2.2 syncLoop 的四个输入源
kubelet 的主循环 syncLoop 监听四类事件:
| 来源 | 触发内容 | 典型场景 |
|---|---|---|
| configCh | Pod 配置变更 | Pod spec 更新、新增 Pod |
| housekeepingCh | 周期清理 | 孤儿容器、过期状态 |
| plegCh | 运行时状态变化 | 容器退出、容器被外部杀掉 |
| syncCh | 定时同步 | 周期性重试,兜底一致性 |
2.3 同步时的幂等设计
每个 Pod 的同步是声明式收敛:kubelet 读取 PodSpec 期望状态,对比运行时实际状态(通过 CRI 查询),只做差量操作。所以「容器被 docker rm 手动删掉」后 kubelet 会自动重建——这不是魔法,是 PLEG 上报事件后触发重新同步的结果。
3. PLEG:Pod 生命周期事件生成器
3.1 PLEG 的工作方式
PLEG(Pod Lifecycle Event Generator) 是 kubelet 里最容易被误解的组件。它不是通过 inotify 之类的事件机制感知容器状态,而是**周期性轮询(relist)**整个运行时的所有 Pod,对比上一次的快照,把差异转成事件:
每 relistPeriod(默认 1s)执行一次:
runtime.ListPodSandbox() + ListContainers() → 得到当前全量状态
与上次快照 diff
容器从 running → exited ⇒ 生成 ContainerDied 事件
sandbox 从 ready → not ready ⇒ 生成 SandboxChanged 事件
事件推入 plegCh,唤醒 syncLoop
3.2 relist 延迟与 PLEG unhealthy
relist 本身可能很慢(运行时卡住、节点负载极高)。kubelet 用 runtimeState 的健康检查判断 PLEG 是否「生病」:
# 触发条件:最后一次 relist 的耗时超过阈值
--node-status-update-frequency # 默认 10s
PLEG unhealthy 判定:relist 耗时 > relistPeriod * 某倍数(实际实现里为 3 分钟无成功 relist)
一旦 PLEG unhealthy,Node Condition 会先变成 Ready=False,随后 NotReady。这就是「节点 NotReady 但 kubelet 进程还在、容器还在跑」的最常见解释。
3.3 PLEG 排障三件套
# 1) 看 kubelet 日志里的 PLEG 报错
journalctl -u kubelet -f | grep -i pleg
# 常见输出:PLEG is not healthy: pleg was last seen active 3m20s ago
# 2) 直接压测运行时是否卡顿(耗时单位秒)
time crictl ps -a | wc -l
time crictl pods | wc -l
# 3) 看节点负载与运行时日志
journalctl -u containerd -f
根因排序:容器运行时卡死(containerd/CRI-O 阻塞)> 节点 CPU/IO 饱和导致 relist 超时 > 运行时 socket 被打满 > 内核 cgroup 异常。修复方向通常是升级运行时、限制节点上 Pod 数量、降低 IO 抖动。
一句话:PLEG 是轮询而非事件,它健康与否直接决定节点 Ready 状态——
PLEG is not healthy几乎总是运行时慢或节点饱和,而不是 kubelet 本身的问题。
4. CRI:容器运行时接口
4.1 CRI 的两个服务
CRI(Container Runtime Interface) 是 kubelet 与运行时之间的 gRPC 契约,包含两个 service:
| Service | 方法举例 | 作用 |
|---|---|---|
| RuntimeService | RunPodSandbox、CreateContainer、StartContainer、StopContainer | Pod/容器生命周期 |
| ImageService | PullImage、ListImages、RemoveImage | 镜像管理 |
kubelet 里有个抽象层叫 dockershim 已移除(v1.24 起),现在直接对接实现了 CRI 的运行时:containerd(内置 cri 插件) 与 CRI-O。
# 查看 kubelet 实际连接的运行时端点
ps -ef | grep kubelet | tr ' ' '\n' | grep container-runtime-endpoint
# 或
crictl info | head -20
4.2 PodSandbox 与 pause 容器
CRI 引入了一个重要概念:PodSandbox。kubelet 创建一个 Pod 时,第一步不是启动业务容器,而是让运行时创建一个 sandbox——通常就是那个 pause 容器。它持有 Pod 的 network namespace,业务容器共享它。
创建 Pod 的顺序:
1. RunPodSandbox → 创建 pause 容器 + 网络命名空间
2. 调用 CNI 配置网络 → 给 sandbox 分配 IP
3. CreateContainer x N → 逐个创建业务容器(加入 sandbox 的 netns)
4. StartContainer x N → 启动业务容器
理解了这一点,ContainerCreating 卡住就能对症下药:卡在第 1 步是运行时问题,卡在第 2 步是 CNI 问题(可参考 Kubernetes 网络与 CNI
一文)。
4.3 crictl 排障
crictl 是 CRI 的命令行客户端,它看到的才是 kubelet 眼中的真实状态:
crictl ps -a # 所有容器(含已退出)
crictl pods # 所有 sandbox
crictl inspect <container-id> # 容器详情(含 cgroup 路径、退出码)
crictl logs <container-id> # 看日志(绕过 kubelet)
crictl stats # 实时资源占用
crictl rmi --prune # 清理无用镜像
一句话:kubectl 看到的是「期望 + 汇报状态」,crictl 看到的是「运行时真相」——两者不一致时,以 crictl 为准。
5. 探针:liveness / readiness / startup
5.1 三种探针的语义
| 探针 | 失败后果 | 该测什么 |
|---|---|---|
| livenessProbe | 重启容器 | 进程是否死锁/卡死(不能测下游依赖) |
| readinessProbe | 从 Service Endpoints 摘除 | 能否正常处理流量(可测依赖就绪) |
| startupProbe | 重启容器(启动期) | 慢启动应用的启动完成判定 |
三者最容易搞混的是 liveness 与 readiness。记住一条判据:liveness 失败会杀掉容器,readiness 失败只是不给流量。如果 liveness 探针里写了数据库查询,数据库抖动就会导致全部容器被重启——这是经典的生产事故。
5.2 探针的实现方式与参数
livenessProbe:
httpGet:
path: /healthz
port: 8080
httpHeaders:
- name: Custom-Header
value: Awesome
initialDelaySeconds: 5 # 首次探测前的等待
periodSeconds: 10 # 探测间隔
timeoutSeconds: 1 # 单次超时
successThreshold: 1 # 成功阈值(liveness 只能为 1)
failureThreshold: 3 # 连续失败次数才判定失败
四种探测方式:exec(在容器内执行命令,退出码非 0 为失败)、httpGet(2xx/3xx 为成功)、tcpSocket(端口可连即成功)、grpc(v1.24+,走 gRPC health checking 协议)。
# startupProbe 保护慢启动应用:最多容忍 30 * 10s = 5 分钟启动
startupProbe:
httpGet: { path: /healthz, port: 8080 }
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
5.3 探针的常见坑
| 坑 | 现象 | 对策 |
|---|---|---|
| liveness 依赖下游 | 下游抖动 → 全量重启 | liveness 只测自身进程 |
| initialDelaySeconds 过短 | 启动期被反复杀 | 改用 startupProbe |
| timeoutSeconds 太小 | 高负载下误判失败 | 探针超时留余量 |
| 探针过于频繁 | kubelet CPU 升高 | periodSeconds 别低于 5s |
| exec 探针 fork 进程 | 容器内进程数暴涨 | 优先 httpGet/grpc |
探针由 kubelet 执行(不是容器内),因此 exec 探针会在容器里 fork 进程——容器 PID 1 处理不当会产生僵尸进程,长期累积后触发 PIDPressure。
6. 驱逐:节点压力下的自我保护
6.1 驱逐信号
kubelet 持续监控一组驱逐信号(eviction signals):
| 信号 | 含义 | 默认硬阈值 |
|---|---|---|
| memory.available | 可分配内存 | < 100Mi |
| nodefs.available | 节点根文件系统可用空间 | < 10% |
| nodefs.inodesFree | 根文件系统 inode | < 5% |
| imagefs.available | 镜像文件系统可用空间 | < 15% |
| pid.available | 可分配 PID | 视配置 |
6.2 软驱逐与硬驱逐
--eviction-hard=memory.available<100Mi,nodefs.available<10%
--eviction-soft=memory.available<200Mi
--eviction-soft-grace-period=memory.available=1m30s
--eviction-max-pod-grace-period=30
--eviction-minimum-reclaim=memory.available=100Mi,nodefs.available=1Gi
- 硬驱逐:信号越过阈值立即驱逐,Pod 只有很短的宽限期。
- 软驱逐:越过软阈值后,若在
grace-period内持续越界才驱逐,给应用留出自行回收的机会。
6.3 驱逐顺序
当必须驱逐时,kubelet 按以下优先级排序(先牺牲的排前面):
1. 是否超出 requests(超过自己请求量的先被驱逐)
2. Pod 优先级(PriorityClass 低者先)
3. QoS 等级:BestEffort → Burstable → Guaranteed
4. 资源使用量超过 requests 的幅度
被驱逐的 Pod 状态是 Failed,reason: Evicted,并带上驱逐原因。它由上层控制器(Deployment/ReplicaSet)重建——所以驱逐不丢数据的前提是应用无状态或状态外置。
# 查看被驱逐的 Pod
kubectl get pods -A --field-selector status.phase=Failed
kubectl describe pod <pod> | grep -A3 "Status:"
一句话:驱逐是 kubelet 保护节点的最后手段——它按「超请求量 + 低优先级 + 弱 QoS」的顺序牺牲 Pod,配置合理的 requests 与 QoS 就是给关键业务买保险(详见 资源限制与 QoS )。
7. cgroup 与资源落地
7.1 cgroup v1 与 v2
kubelet 通过 cgroup 把 requests/limits 变成内核级约束。cgroup v2 已成为主流(较新发行版默认启用),它用统一层级替代 v1 的多层级:
# 判断节点在用哪一版 cgroup
stat -fc %T /sys/fs/cgroup
# cgroup2fs ⇒ v2;tmpfs ⇒ v1
| 维度 | cgroup v1 | cgroup v2 |
|---|---|---|
| 层级 | 每控制器一棵树 | 单棵统一树 |
| 内存限制 | memory.limit_in_bytes | memory.max |
| CPU 限制 | cpu.cfs_quota_us | cpu.max |
| 压力指标 | 无 | memory.pressure、cpu.pressure |
7.2 QoS 对应的 cgroup 结构
kubelet 按 QoS 建立分层 cgroup,这正是驱逐顺序的内核依据:
/sys/fs/cgroup/kubepods.slice/
├── kubepods-besteffort.slice/ ← BestEffort Pod
├── kubepods-burstable.slice/ ← Burstable Pod
└── kubepods-guaranteed.slice/ ← Guaranteed Pod
└── kubepods-pod<uid>.slice/
└── cri-containerd-<cid>.scope/
内存紧张时,内核优先回收 BestEffort 子树,这与 kubelet 的驱逐顺序一致——配置 QoS 不只是「给 kubelet 看」,它真的写进了内核 cgroup 层级。
7.3 容器级限制的落地
# 查看某容器的实际 cgroup 限制
crictl inspect <container-id> | grep -i cgroup
cat /sys/fs/cgroup/kubepods.slice/.../memory.max
cat /sys/fs/cgroup/kubepods.slice/.../cpu.max # 格式:quota period
若应用「明明 limits 是 1 核却跑满多核」,检查容器内是否读到正确的 CPU quota——JVM、Node、Go runtime 的自动并行度都依赖 cpu.max(v2)或 cpu.cfs_quota_us(v1)。
8. 节点组件协同与排障路径
8.1 一次 Pod 创建的完整链路
kube-scheduler 选定节点 → 写 spec.nodeName
↓
kubelet watch 到 Pod → syncLoop 入队
↓
CSI:挂载卷(如需)→ CRI:RunPodSandbox → CNI:配置网络
↓
CRI:CreateContainer → StartContainer
↓
探针启动 → readiness 通过 → EndpointSlice 加入端点
8.2 分层排障清单
| 症状 | 优先检查 | 命令 |
|---|---|---|
| Pod 一直 ContainerCreating | 卷挂载 / CNI | kubectl describe pod、crictl pods |
| Pod 反复重启 | 探针 / OOM | kubectl describe pod 看 Last State |
| 节点 NotReady | PLEG / kubelet | journalctl -u kubelet |
| 容器被杀 | 驱逐 / cgroup | dmesg -T | grep -i oom |
| 镜像拉不下来 | 镜像仓库 / 凭证 | crictl pull <image> |
# 节点上一把梭的排查命令
journalctl -u kubelet --since "10 min ago" | grep -iE "error|pleg|evict"
crictl ps -a | grep -v Running
df -h /var/lib/kubelet && df -i /var/lib/kubelet
systemctl status containerd
更多集群级排障方法论可参考 Kubernetes 集群排障与诊断 ;节点池规划与维护窗口设计见 Kubernetes 节点管理与节点池策略 。
9. 小结
| 环节 | 核心机制 | 关键参数/命令 |
|---|---|---|
| Pod 同步 | list-watch + informer 缓存 | --kubeconfig、watch 指标 |
| PLEG | 周期性 relist + 事件 diff | relistPeriod、PLEG is not healthy |
| CRI | RuntimeService / ImageService | crictl、--container-runtime-endpoint |
| 探针 | kubelet 侧执行,三类语义不同 | initialDelay/period/timeout/failureThreshold |
| 驱逐 | 信号阈值 + 优先级排序 | eviction-hard/soft、reason: Evicted |
| 资源落地 | cgroup v2 统一层级 | memory.max、cpu.max |
一句话记住:kubelet 是把声明式 API 变成真实容器的最后一公里——list-watch 拿到期望、PLEG 感知现实、CRI 执行动作、探针判定健康、驱逐保护节点。排障时先分清「kubectl 的汇报状态」与「crictl 的运行时真相」,再顺着同步链路逐层定位,绝大多数节点问题都能收敛到具体的某一环。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。