凌晨两点四十,值班群炸了。订单服务连续被驱逐,Prometheus 弹出一片 NodeCondition 告警,逐条点开都是同一句话:The node had condition: [DiskPressure]。登录节点一看,根分区使用率 97%,kubectl get events里FailedScheduling明晃晃写着0/4 nodes are available: 4 node(s) had condition DiskPressure。
这套组合拳我在不同环境里处理过七八次,每次根因都不完全一样,有的是日志把盘写爆,有的是镜像堆积,有的竟然是 inode 耗尽——df -h明明还剩 20G,节点照样报 DiskPressure。这篇文章我就把 DiskPressure 从现象、机制、排查到根治完整展开一遍,重点讲清楚 kubelet 到底是怎么判定"磁盘压力"的,以及生产环境里最容易被忽略的几个引爆点。
1. DiskPressure 不是一句"磁盘满了":先看懂 kubelet 的驱逐通道
1.1 那条 FailedScheduling 事件是怎么来的
很多人第一次见 DiskPressure 是在调度事件里,以为这是 kubelet 报的错。实际上 DiskPressure 是 Kubernetes 暴露在 Node 对象上的一个 Condition,全称是DiskPressure,由节点上的 kubelet 维护。调度器在做 Predicate 检查时,发现节点处于 DiskPressure 状态,就直接把这个节点从可调度列表里划掉,于是新 Pod 全部失败,事件里就出现了node(s) had condition DiskPressure的描述。
这里有个容易混淆的点:节点报 DiskPressure 不一定代表节点 NotReady。很多时候节点的 Ready 状态还是 True,网络、kubelet 心跳都正常,但调度器已经不往这个节点派新 Pod 了,同时存量 Pod 还面临被驱逐的风险。结果就是:看起来节点"活着",但业务已经在悄悄降级。所以生产环境里遇到 DiskPressure,优先级要提到最高,它不像某些 Fluentd 报错可以缓一缓,它是真会杀业务的。
1.2 nodefs 和 imagefs:kubelet 眼里的两块盘
要理解 DiskPressure,先得知道 kubelet 不是监控整台机器的所有磁盘,它只关心两条文件系统路径:
- nodefs:kubelet 自身工作目录所在的文件系统,包括
/var/lib/kubelet、Pod 的 emptyDir、容器日志、可写层对应的宿主机路径等。在大多数 kubeadm 部署的环境里,nodefs 就是根分区/。 - imagefs:容器运行时存放镜像和容器层数据的文件系统。用 containerd 就是
/var/lib/containerd,用 Docker 就是/var/lib/docker。
我见过不少环境里这两条路径在同一个分区上,比如云主机默认只给根分区 40G,/var/lib/kubelet 和 /var/lib/containerd 全在/上。这种布局下,镜像一涨、日志一涨、emptyDir 一涨,全都算到同一个盘上,DiskPressure 来得特别快。kubelet 自己会对 nodefs 和 imagefs 做去重处理,如果它们指向同一个挂载点,就当成一条盘来统计。
1.3 触发阈值和触发后的动作
kubelet 的驱逐管理器有一组阈值参数,默认值大致如下(不同版本细节有差异,以实际部署为准):
| 阈值项 | 默认值 | 含义 |
|---|---|---|
nodefs.available | 小于 10% | nodefs 可用空间低于 10% 触发 |
nodefs.inodesFree | 小于 5% | nodefs 可用 inode 低于 5% 触发 |
imagefs.available | 小于 15% | imagefs 可用空间低于 15% 触发 |
imagefs.inodesFree | 小于 5% | imagefs 可用 inode 低于 5% 触发 |
memory.available | 小于 100Mi | 内存压力阈值,无 DiskPressure 相关 |
对应 kubeadm 初始化的集群,这些配置在/var/lib/kubelet/config.yaml的evictionHard字段里能看到。kubelet 还有一个 soft 阈值区间,soft 阈值触发后不会立刻驱逐,而是等一个 grace period,环境没恢复才动手。生产环境里我建议重点关注 hard 阈值,因为大部分 DiskPressure 告警都是 hard 阈值直接命中。
一旦某个阈值被击穿,kubelet 会做三件事:
- 更新 Node 条件为
DiskPressure=True,调度器随即屏蔽新 Pod。 - 主动回收镜像和死容器,也就是触发 imageGC 和 containerGC。
- 按照 QoS 等级从低到高驱逐 Pod:先是 BestEffort,再是 Burstable,最后才轮到 Guaranteed。这里注意,驱逐不是随机的,kubelet 内部会按照
pod 实际使用量超过请求量的比例排序,超得越多越先被清理。
想一下这个逻辑就知道为什么 DiskPressure 可怕:一个节点上的 Pod 被逐到别的节点,别的节点如果容量也紧张,很容易产生"驱逐雪崩"。所以看到 DiskPressure 别只盯着当前节点,要顺手看看集群里其他节点的水位。
2. 现场排查第一步:定位涨的是数据还是镜像、是空间还是 inode
2.1 三组命令快速看清盘面
接到告警后,我的习惯是先不做任何破坏性操作,先跑三条命令把盘面看清楚:
df -h df -i lsblkdf -h看空间,df -i看 inode,lsblk看分区挂载关系。重点看两个挂载点:/和/var/lib/containerd(或/var/lib/docker)。如果这两个路径的df -h结果显示的是同一个分区,说明 nodefs 和 imagefs 混在一起,那排查范围就得覆盖全部数据。
接着用du往下钻,找出真正吃空间的大头:
du -xh --max-depth=2 /var/lib/kubelet 2>/dev/null | sort -h | tail -30 du -xh --max-depth=2 /var/lib/containerd 2>/dev/null | sort -h | tail -30 du -xh --max-depth=3 /var/log 2>/dev/null | sort -h | tail -20-x参数很关键,它让 du 不跨文件系统统计,避免你把 NFS 挂载或者外部数据盘的大小也算进来,导致结果失真。
2.2 确认 kubelet 的监控盘与阈值配置
看完了实际使用情况,还要确认 kubelet 到底按什么标准在判断。kubeadm 部署的集群,直接看:
cat /var/lib/kubelet/config.yaml重点看evictionHard段,确认当前生效的阈值是不是默认值。有些团队会把阈值改得很激进,比如nodefs.available: "15%",这种配置下磁盘用到 85% 就开始驱逐了,报警自然会早很多。没有这个文件的托管集群,或者从 systemd 直接启动的 kubelet,检查启动参数:
ps -ef | grep kubelet看命令行里有没有--eviction-hard相关的参数。我遇到过有人把--eviction-hard写在 kubelet 启动参数里,同时还改了 config.yaml,两边配置不一致,结果 kubelet 自己都蒙了,驱逐行为完全不可预测。
2.3 从 kubelet 日志和事件还原驱逐现场
盘面情况摸清后,去看 kubelet 日志,确认它到底因为什么触发、驱了哪些 Pod:
journalctl -u kubelet --since "2 hours ago" --no-pager | grep -iE "evict|diskpressure|imagefs|nodefs"kubectl 侧看事件和 Node 条件:
kubectl describe node node-01 | grep -A20 Conditions kubectl get events -A --field-selector reason=Evicted -n productionkubectl describe node输出里 DiskPressure 条件的 Reason 一般是KubeletHasDiskPressure,Message 是kubelet has disk pressure。通过驱逐事件能看到一排被 Evicted 的 Pod 名,结合它们的 QoS 等级,就能反推 kubelet 当时的驱逐顺序,从而判断是内存还是磁盘压力在起作用——这步很多人忽略,其实特别有用。
我把整个排查起点整理成一个表格:
| 检查项 | 命令 | 关注点 |
|---|---|---|
| 空间使用 | df -h / /var/lib/containerd | 是否接近阈值 |
| inode 使用 | df -i / /var/lib/containerd | inode 是否接近 100% |
| 大目录分布 | du -xh --max-depth=2 ... | 日志、镜像、emptyDir 谁最大 |
| kubelet 阈值 | cat /var/lib/kubelet/config.yaml | evictionHard 实际值 |
| 驱逐动作 | journalctl -u kubelet -g evict | 驱逐了谁、频率多少 |
3. 复盘生产环境里四个最容易引爆 DiskPressure 的场景
3.1 容器日志只写不转:nodefs 被日志吃掉
最大的锅,通常不是业务数据,而是容器日志。Kubernetes 默认情况下,容器写到 stdout 的日志由容器运行时负责落盘,落盘路径一般是/var/log/pods/<namespace>_<pod>_<uid>/<container>/0.log。如果你没有给 kubelet 配置日志轮转参数,这个文件就会一直长下去。我处理过一个案例:一个 Java 服务一天能写 30G 日志,三天时间就把一个 100G 的根分区写满,节点 DiskPressure,Pod 被驱逐,业务全挂。
这里特别容易踩坑的点是:很多人排查看/var/log,发现系统日志才几个 G,觉得"没问题啊",却忘了/var/log/pods下的容器日志可能已经占了 60G。用 du 盘点时一定要把容器日志目录单独拎出来看:
du -sh /var/log/pods/* 2>/dev/null | sort -h | tail -103.2 镜像和"死容器"越攒越多
镜像也是 DiskPressure 的常客。kubelet 有镜像 GC 机制,默认在镜像占用达到 85% 时开始回收,回收到 80% 才停手。但注意,这个 GC 只删"未被任何容器引用"的镜像。如果你们发版很频繁,新镜像一波波拉下来,旧镜像没被清理;或者每次发布失败产生的中间层镜像堆在 containerd 里,imagefs 很容易爆。
用 containerd 的集群,可以看这两个目录的大小:
du -sh /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs du -sh /var/lib/containerd/io.containerd.content.v1.contentsnapshotter 是容器层数据,属于"活着"的数据,不能乱删;content 是镜像层和 blob 缓存,清理价值更高。但生产环境不建议直接对 containerd 目录动手,正确做法是用crictl rmi --prune让运行时自己清理未被引用的镜像。
3.3 emptyDir 与挂载目录失控
再一个高频场景是 emptyDir。emptyDir 默认落在 kubelet 的 nodefs 上,它设计初衷是给容器提供一个临时目录,生命周期跟着 Pod 走,所以很多人写业务的时候把它当成"不要钱的硬盘"来用:解压临时包、写缓存文件、下载大文件,全往 emptyDir 里塞。Pod 一直不重建,emptyDir 里的数据就一直在,越积越多。
还有一种情况是宿主机目录被直接挂进容器,比如 deployment 里写了 hostPath。一个运维监控 Agent 把宿主机的/var/log挂进容器里写日志、一个业务容器挂载宿主机的/tmp当缓存目录,这些数据全部绕过容器 runtime 层,kubelet 的镜像 GC 完全管不到,只能靠人肉清理。
3.4 inode 耗尽:df -h 显示有空间,节点照样报警
这个场景很多人第一次遇到会懵:df -h明明显示根分区还有 20G,为什么节点还是 DiskPressure?原因在于 kubelet 默认还监控nodefs.inodesFree和imagefs.inodesFree,也就是 inode 剩余比例。
inode 是什么?简单理解,它是文件系统给每个文件分配的"身份编号",创建文件就要消耗一个 inode。如果一个目录下有几十万个 1KB 的小文件,空间没占多少,inode 却先耗尽了,文件系统就再也建不了新文件,kubelet 自然判定为磁盘不可用。
排查命令:
df -i /看IUse%那一列,接近 100% 基本就是 inode 问题。再定位小文件聚集地:
for d in /var/lib/kubelet /var/lib/containerd /var/log /tmp /var/tmp; do echo "$d: $(find $d -xdev -type f 2>/dev/null | wc -l)" done生产环境里最常见的 inode 杀手是日志系统:journald 会写大量小日志文件,容器的/dev/null下每个 Pod 都会生成一堆文件(这一条在特定情况下很常见),某些缓存目录也会产生海量碎片文件。
4. 从应急止血到根治的完整处置清单
4.1 止血第一优先:先救节点,再救业务
DiskPressure 一旦触发,业务已经在被驱逐了,这时候不要想着"先观察一下",直接动手释放空间。我的止血顺序是:
第一步,清系统日志。journald 通常是最快能释放空间的:
journalctl --vacuum-size=200M第二步,清容器日志。找到超过 500M 的容器日志文件,确认对应 Pod 后,先让 kubelet 重建或者滚动重启该 Pod,再删除旧日志文件。千万别在 Pod 还持有文件句柄的时候直接rm大日志文件——Linux 下文件句柄还开着,空间不会真正释放,你删了个寂寞。稳妥做法是truncate -s 0 <日志文件>先腾空内容,让容器运行时重新写,等 Pod 换了一个新文件后再处理旧文件。
第三步,清悬空镜像和停止的容器:
crictl rmi --prune crictl ps -a # 对已停止的容器逐一删除 crictl rm <container-id>如果你用的是 Docker 作为运行时,用docker image prune -a和docker container prune -f效果类似。注意:crictl rmi --prune删除的是没有被任何容器引用的镜像,不影响正在运行的业务。
第四步,清 emptyDir 和 hostPath 的大文件。这一步要看应用,最好拉上对应业务的开发一起确认,别把正常数据当垃圾删了。
4.2 cordon、drain 和 LocalPV 的坑
如果上面四步做完,磁盘使用率还在 90% 以上,说明数据量太大了,光靠清垃圾解决不了问题,这时候就要考虑把节点先摘出去,让业务去别的节点跑,自己慢慢处理。
标准操作是:
kubectl cordon node-01 kubectl drain node-01 --ignore-daemonsets --delete-emptydir-datadrain 会先把节点上的 Pod 驱逐到其他节点,然后你就有了一个干净的维护窗口。但这里有三个坑:
--delete-emptydir-data会删除该节点上所有 emptyDir 数据,如果你的业务确实依赖 emptyDir 里的缓存,数据会丢,要提前告知确认。- LocalPV 或 hostPath 兜底的存储,drain 不会帮你备份数据。有 StatefulSet 用了 LocalPV 的话,必须先手动确认数据副本状态,否则 Pod 被逐走,数据还留在宿主机上,再调度回来之前可能已经被你后面的大扫除干掉了。
- drain 之后节点会变成 Ready,SchedulingDisabled,如果集群只有这一个节点,drain 等于把集群打停,手滑之前想想清楚。
4.3 平台层根治:日志轮转、镜像 GC 与驱逐阈值调参
止血只是把眼前这关过了,不治本的话一周后必然再来一次。平台层的根治分三块。
日志轮转是性价比最高的一步。在 kubelet 配置里加上:
containerLogMaxSize: 20Mi containerLogMaxFiles: 5意思是每个容器日志文件最多 20Mi,最多保留 5 个文件,写满了就滚动。改完重启 kubelet 生效。对于存量已经写大的日志文件,改完配置不会自动帮你删,还是得用上面 truncate 的方式手动处理一次。
镜像 GC 参数如果频繁发版比较猛,可以调低:
imageGCHighThresholdPercent: 70 imageGCLowThresholdPercent: 60这样镜像占用到 70% 就开始回收,到 60% 才停,镜像堆积的速度会慢很多。但要提醒一句:镜像 GC 再勤,也不如给 imagefs 单独一个大分区来得实在。
驱逐阈值一般不建议乱动。默认的 nodefs 可用 10%、imagefs 可用 15% 是社区的经验值,你把阈值改小(比如 nodefs.available 改到 5%)虽然能让节点晚一点报 DiskPressure,但磁盘真正写满之后,kubelet 连写自己的状态文件都写不进去,系统会进入一个更糟糕的状态。阈值是"报警铃",不是"解药"。
4.4 应用层根治:ephemeral-storage 限制与 emptyDir 改造
平台层做完了,应用层的配合也必不可少。最推荐的一招是给 Pod 声明临时存储限制。Kubernetes 支持在 requests 和 limits 里写ephemeral-storage:
resources: requests: ephemeral-storage: "1Gi" limits: ephemeral-storage: "2Gi"这样 kubelet 在容器写爆临时存储时,会优先把这个 Pod 驱逐,而不是让整个节点被拖下水。这相当于给每个 Pod 划了一条"违约线",符合"限制粒度越小,故障半径越小"的原则。
emptyDir 的改造方面,如果是纯内存型缓存(比如临时解压的小文件),可以声明:
volumes: - name: cache emptyDir: medium: Memory内存型 emptyDir 不走磁盘,自然不占 nodefs。但要注意它占用的是 Memory,还得配合内存 limits,别把内存又打爆了。如果是真正的大数据临时目录,比如临时下载大文件、解压安装包,建议直接改成 PVC,把数据挪到独立存储上,从根本上脱离 nodefs 的容量约束。
4.5 把存储搬到大盘:数据目录迁移
如果检查发现根分区就是很小(比如云主机默认 40G),而/var/lib/containerd又没法快速扩容,最省事的方法是把容器运行时目录迁到一块大数据盘上。
containerd 的配置在/etc/containerd/config.toml,找到 root 参数改成新路径:
root = "/data/containerd"或者用 kubelet 的--root-dir参数把 kubelet 工作目录移走:
# 在 kubelet 启动参数中追加 --root-dir=/data/kubelet迁移前必须 drain 节点,把数据用rsync或者cp -a复制到新目录,确认元数据、权限都一致后再切换。这个操作有一定风险,常规情况下我更推荐用lsblk、LVM 扩容根分区的方式,先把空间扩出来,比搬目录省事得多。实在要搬,一定先在测试环境过一遍。
5. 别等告警再救火:监控、容量和巡检三板斧
5.1 磁盘与 inode 告警选型
DiskPressure 这种问题,最理想的处理方式是永远不让它发生。要做到这点,监控得先有。node-exporter 默认导出的指标里就有两个关键项:
node_filesystem_avail_bytes:文件系统可用字节数。按mountpoint标签拆分后,重点监控/var/lib/kubelet和/var/lib/containerd所在挂载点。
- alert: KubeletNodeDiskPressure expr: (node_filesystem_avail_bytes mountpoint="/" / node_filesystem_size_bytes mountpoint="/") < 0.15node_filesystem_files_free:可用 inode 数量。同样按 mountpoint 拆分,低于总量 10% 时就该告警了,因为 inode 耗尽的恢复手段比空间耗尽更麻烦——你通常得逐个目录找小文件,特别费时间。
我的告警策略是:磁盘使用率 80% 给 warning,90% 给 critical;inode 使用率 85% warning,95% critical。宁可早一点被吵醒,也别等到 kubelet 半夜给你驱逐 Pod。
5.2 容量规划经验
容量规划这块我走过不少弯路,总结成一句话:永远给 /var/lib/kubelet 和 /var/lib/containerd 各留一块独立的盘。不管是最小化的测试集群还是生产集群,我都建议在装系统时把根分区至少给到 100G,并单独分出 /var/lib/kubelet 对应的数据盘。计算方式很简单,拿单节点最大并发 Pod 数乘以每个 Pod 最大日志量,再乘 2,基本就是这块盘需要的最小容量。举个例子,单节点跑 50 个 Pod,每个 Pod daily log 滚转上限 100M,那 nodefs 至少留 50 * 100M * 2 = 10G 的弹性空间,这只是日志的部分,还没算 emptyDir。
5.3 日常巡检与发行版细节
日常巡检建议做三件事:
- 每天定时用上面的 du 命令扫一遍各大目录,生成磁盘增长趋势,谁涨得最快一目了然。
- 低峰期跑一次
crictl rmi --prune,把悬空镜像清掉,避免高峰期触发 GC 造成 IO 抖动。 - 定期审计没有配置
ephemeral-storage限制的工作负载,重点盯 batch 任务和定时任务。
不同发行版也会踩到不同的坑。比如 Rocky Linux 和 CentOS 系默认用 systemd-journald,/var/log/journal默认可能没有大小上限,需要在/etc/systemd/journald.conf里设置SystemMaxUse=500M限制 journal 总量;Ubuntu 系的/var/log里unattended-upgrades日志有时候也能悄悄攒好几个 G。这些"平台债"平时不起眼,一旦碰上 DiskPressure,清理起来比容器日志还麻烦。
最后聊一点个人体会。我处理过最难受的一次 DiskPressure,不是盘真的满得不可救药,而是告警来时大家急着删文件,删掉了正在容灾切换的备份数据,最后磁盘压力解了,业务数据也丢了。所以每次遇到这类告警,我给自己定了一条铁律:先花五分钟看监控确认是哪个文件系统、哪类数据在涨,再决定动刀的方向。DiskPressure 的覆盖面很广——空间、inode、镜像、日志、emptyDir,每一样的处置方式都不一样。磨刀不误砍柴工,这五分钟的定位,永远比盲目清理值钱。