结论先行:K8s 1.28 里 Pod 批量卡
Terminating、kubectl get pod全节点超时、节点却Ready,大概率不是网络、不是 controller-manager、更不是 preStop 配错——而是 kubelet 的 PLEG 同步依赖的 CRI gRPC(ListContainers)被 containerd 侧拖慢了。这篇文章给一条可复现的定位链路(crictl ps计时 → kubelet 日志 → containerd 指标),以及一次真实事故的治理数据。
一、现象与背景:节点 Ready,但整台节点"冻结"了
- 集群:38 节点,Kubernetes 1.28.9,containerd 1.6.24(节点发行版自带),cgroup v2,内核 5.15.0-91;
- 出事节点:一台 16C32G worker,当时跑了 42 个容器(含一个 CI 构建器 Pod,里面一个 Job 在循环
docker build风格的短容器,1 小时内起了 300+ 个短命容器); - 现象三连:
- 该节点上 47 个 Pod 全部卡
Terminating,持续约 40 分钟,kubectl delete超时(默认 30s); kubectl get pod -o wide对该节点 Pod 返回超时,其他节点完全正常;kubectl describe node里 condition 是Ready: True,但 events 里滚动出现PLEG is not healthy: pleg was last seen active 2m13s ago。
- 该节点上 47 个 Pod 全部卡
最大的误导点:节点 Ready、kubelet 进程活着、CPU 只有 35%——按常规直觉,系统"没毛病",但所有依赖 Pod 生命周期变更的操作(delete / drain / 滚动更新)全被卡死。
二、原理:为什么 ListContainers 慢,整台节点就"停摆"
K8s 1.28 下 kubelet 的容器状态同步走 CRI gRPC:
- kubelet 的 PLEG 每次同步要调
ListContainers,默认超时 1 分钟(--runtime-request-timeout); - containerd 的 CRI 插件处理 List 类调用时,要在全局状态锁下遍历容器 store 并回查 shim;节点上容器数量多、创建/删除churn 频繁时,这个调用从毫秒级退化到秒级甚至几十秒;
- PLEG 连续 1 分钟拿不到新鲜状态,kubelet 就标记 PLEG unhealthy,此后所有 reconcile 被跳过——delete、probe 结果上报、status 更新全部停摆。这就是"卡 Terminating"的直接机制:不是 Pod 不退出,而是kubelet 根本没机会去推进退出流程,SIGTERM 都没发出去。
注意一个反直觉点:kubectl delete pod 返回超时 ≠ 删除失败。apiserver 端 deletionTimestamp 已经打上去了,Pod 逻辑上已"该死";真正卡住的是节点侧执行环节。所以换节点网络、重启 kubelet API 都治不了。
三、定位与做法(可复现)
1. 先给 CRI 计时,一锤定音:
time crictl ps -q | wc -l # 该节点实测:42 个容器,耗时 12.7s
time crictl inspect <任一容器id> # 实测:3.9s(正常节点 <100ms)
crictl ps 就是 ListContainers 的用户态等价物。>1s 即异常,>10s 基本坐实 PLEG 会被拖垮。
2. 抓 kubelet 日志确认因果链:
grep -E "PLEG is not healthy|context deadline exceeded" /var/log/kubelet.log | tail -20
# 典型输出:PLEG was last seen active 2m13s ago;
# ListContainers ... context deadline exceeded
3. 找 churn 源头: 用 crictl ps -a 看 Exited 容器堆积,结合 journalctl 时间线,定位到那个 CI Pod 的短容器风暴(1h 内 300+ 次 create/destroy)。
4. 治理(按见效速度排序):
- 迁走 churn 源:把构建/CI 类负载调度到独立节点池(nodeSelector + taint),业务节点容器数从 42 降到 28 后,
crictl ps从 12.7s 回落到 0.4s,PLEG 告警消失,47 个 Terminating Pod 在 3 分钟内全部收敛; - 加观测:Prometheus 抓
kubelet_pleg_relist_duration_seconds(P99 > 2s 告警)+containerd侧 gRPC 处理时长,别等 40 分钟后才发现; - 版本层:containerd 1.6.x 的 CRI 插件锁粒度问题在 1.7 有明显改进(List 路径锁竞争减少),我们后续把节点池统一升到 containerd 1.7.13,同类节点 60+ 容器时
crictl ps稳定在 1s 内。
5. 应急兜底(慎用): 对单 Pod 卡死,crictl stop + crictl rm 可强制清理 shim,让 PLEG 下一轮同步放行;不要直接 systemctl restart containerd——会把该节点所有运行中容器一并杀掉,把"卡死"升级成"雪崩"。
四、踩过的坑与反直觉之处
- 节点 Ready 不代表节点可用:PLEG unhealthy 时所有生命周期操作静默停摆,但 node condition 仍是 Ready——监控里
Ready=True和PLEG is not healthy同时存在是完全合法的组合,别用 Ready 排除法。 - CPU 高 ≠ 元凶:containerd 卡住时 CPU 可能只有 35%(在等锁/等 shim 响应),用"CPU 不高所以没资源问题"排除是错的。
- 和 preStop 卡 Terminating 是两码事:preStop 配错是"Pod 在退、退得慢",本例是"Pod 根本没开始退"。判别法:
kubectl get events里有没有Stopping container事件——有是前者,一个没有就是 PLEG/运行时侧。 kubectl delete超时后别反复 delete:deletionTimestamp 只打一次,反复重试只是制造噪音。
五、结论与可复用清单
- 判别口诀:批量卡 Terminating + 节点 Ready + 无 Stopping 事件 → 先
time crictl ps; - 阈值经验值:
crictl ps正常 <100ms,>1s 警惕,>10s 必出 PLEG 事故(1 分钟 runtime-request-timeout 是红线); - 治理三板斧:churn 源隔离到独立节点池、
kubelet_pleg_relist_duration_secondsP99 告警、containerd ≥1.7; - 反模式:拿
Ready做健康判断、用restart containerd救卡死、把 PLEG 问题当 preStop 问题调 grace period。
参考:站内两篇相邻复盘——前者讲 finalizer 失控卡 Terminating(apiserver 侧),后者讲 preStop 时序卡 Terminating(应用侧),本文覆盖第三种:运行时侧(kubelet/PLEG/containerd),三者判别见第四节第 3 条。