问题背景
我们的生产集群由 6 个 kubeadm 搭建的节点组成,容器运行时为 containerd,跑着订单、支付、用户中心等二十余个无状态服务,外加 Prometheus、CoreDNS 等系统组件。节点是云上 ECS,系统盘 100G,且 containerd 的镜像与可写层目录(/var/lib/containerd)、容器日志目录(/var/log/containers、/var/log/pods)都落在同一块系统盘上——也就是说,kubelet 关注的 imagefs 与 nodefs 实际共用一个分区。集群已稳定运行大半年,直到一次常规发版后的第二天上午,告警把人叫醒。
故障现象
早上九点,监控群里连续弹出节点状态告警:3 个节点在 5 分钟内从 Ready 反复横跳到 NotReady 再恢复,行为极具节律性。同期业务侧开始抖动——订单接口 P99 从 80ms 飙到 2s 以上,部分请求返回 503。
登录集群查看,现象更具体:
kubectl get pod -A里,订单服务(Deployment,期望 5 副本)可用副本在 1~2 之间反复横跳,被驱逐的 Pod 在别的节点重建后不久又被驱逐,形成「驱逐风暴」。kubectl describe node <node>的 Conditions 里DiskPressure为True,Taints 出现node.kubernetes.io/disk-pressure:NoSchedule。- 节点上 kubelet 日志高频刷屏:
Evicting pod ... because node had condition: [DiskPressure]。 - 登录问题节点
df -h,系统盘使用率 92%,「看起来还有空间」,但关键服务已经崩了。
排查过程
- 确认驱逐类型:
kubectl describe node直接给出DiskPressure=True和对应的 taint,说明是磁盘压力触发了 kubelet 的软/硬驱逐,而非 OOM(内存压力会显示MemoryPressure)。这把方向从「应用内存问题」扭到了「磁盘空间/阈值」。 - 对照默认驱逐阈值:kubelet 的
eviction-hard默认是imagefs.available<15%、nodefs.available<10%。问题节点系统盘可用 8%(100%-92%),已经低于nodefs.available<10%的硬阈值;而imagefs与nodefs共用同一分区,自然也一同越线。这正是「df 看着还有空间却被驱逐」的原因——阈值触发的是「可用量百分比」,不是「是否 100% 满」。 - 定位占用大户:
du -sh /var/lib/containerd占 61G,/var/log/containers+/var/log/pods占 23G。进一步看,containerd 用默认json-file日志驱动且没有设置max-size/max-file,某几个业务 Pod 因有人把日志级别临时调到 DEBUG 且忘了改回,单容器日志文件涨到 8G;镜像侧,每次发版都拉新 tag,旧镜像层从未 GC,节点上堆积了同一服务的十几个历史版本。 - 还原驱逐风暴链路:
DiskPressure触发后,kubelet 按 QoS 从低优先级(BestEffort/Burstable)开始驱逐 Pod;被删的 Pod 由 Deployment 在其它节点重建,但新节点同样磁盘吃紧,于是又被驱逐——形成循环。同时NoScheduletaint 阻止 Pod 调度回原节点,但已运行 Pod 仍会因imagefs继续上涨被二次驱逐,节点在 Ready/NotReady 间反复横跳。 - 交叉验证:对比同配置但日志量小的健康节点,使用率 70%,从未触发。锁定根因——日志无轮转 + 镜像不 GC 导致 imagefs 慢性膨胀,最终越过 15%/10% 阈值。
解决方案
止血(先恢复):对问题节点 kubectl drain <node> --ignore-daemonsets --delete-emptydir-data 排空;手动清理——crictl rmi $(crictl images -q) 清旧镜像、truncate -s 0 超大日志文件,把使用率压到 60% 以下,节点 DiskPressure 自动解除、回到 Ready,被驱逐的 Pod 在新节点稳定重建。
根治(调参与治理):
- 调整 kubelet 驱逐配置,给足缓冲:
eviction-hard=imagefs.available<10%,nodefs.available<5%,并设eviction-minimum-reclaim保证单次回收量;增大eviction-pressure-transition-period(默认 5m)避免抖动。 - 日志驱动改造:containerd 配置
io.containerd.grpc.v1.cri下的log段,max-size=100m、max-file=3,或对关键业务接 fluentd/采集到 ES,从根上限制单文件体积。 - 镜像治理:节点设置
--image-gc-high-threshold=70 --image-gc-low-threshold=50,或加 cron 定期crictl rmi清悬空镜像。 - 容量隔离:把 containerd 根目录迁移到独立数据盘,分离
imagefs与nodefs,避免镜像/日志与系统盘互相挤占。 - 业务侧:评审并收敛日志级别,禁止生产环境 DEBUG 全量输出。
根因分析
这次故障表面是「磁盘要满了」,本质是日志与镜像缺少生命周期管理,导致 imagefs 慢性膨胀越过 kubelet 驱逐阈值;而 imagefs/nodefs 共用系统盘又把风险放大了一倍。kubelet 的磁盘压力驱逐本是自我保护机制,但当阈值与容量规划不匹配、且增长源(日志/镜像)无人治理时,保护机制反而成了业务抖动源。
预防措施
- 基础设施层强制:容器日志驱动必须设
max-size/max-file,不允许裸json-file。 - 节点级
imageGC阈值 + 定期清理脚本,控制镜像层堆积。 - containerd 使用独立数据盘,分离
imagefs与nodefs。 - 监控前移:对
nodefs.available/imagefs.available设 20%/25% 告警,先于 kubelet 阈值;同时监控Evicted/TerminatingPod 数量与节点DiskPressure发生次数。 - 建立日志级别评审机制,生产环境默认 INFO,禁止 DEBUG 常开。
总结
磁盘压力驱逐常被误判为「磁盘 100% 满了」。真正要治理的是日志轮转、镜像 GC 与容量隔离这些基础设施层的生命周期问题,以及 eviction 阈值与容量规划的匹配——在基础设施层把增长源管住,远比在应用层一次次救火更根本。