
节点上其他条件比如 MemoryPressure触发时kubelet 是尽量保证节点可用驱逐 pod 往往只是断尾求生但 DiskPressure 触发时kubelet 会开始疯狂地做垃圾回收、删镜像、清日志如果还是不够就按 QoS 顺序驱逐 pod。而且一旦节点背上 DiskPressure 这个标签调度器就不会再把新 pod 调度上来了老 pod 也在被驱逐的边缘等于这个节点直接废掉。所以告警群里出现这行日志的时候基本上就是节点快不行了的信号。我写这篇东西就是想把 DiskPressure 这个故障从原理到排查再到根治完整捋一遍。你在生产环境里踩过的坑大概率都逃不出下面这几条路。1. 先搞懂 DiskPressure 到底意味着什么1.1 节点条件这套机制是怎么工作的Kubernetes 里每个节点都有一组条件Conditions最常打交道的就是Ready、MemoryPressure、DiskPressure、PIDPressure它们本质上就是 kubelet 周期性地采样本机资源把结果同步给 apiserver然后由 Node 控制器汇总成一个状态。你执行kubectl get node看到的STATUS列就是这些条件的综合表现。DiskPressure 的含义很直白节点上的磁盘空间或者 inode已经低于 kubelet 设定的阈值。kubelet 大概每 10 秒会做一次检查发现触发阈值后会把节点的DiskPressure条件置为True同时给节点打上一个node.kubernetes.io/disk-pressure污点Taint。打上污点意味着什么没有对应容忍的 pod 不会再被调度到这台节点上已经在上面运行的 pod 也不会立刻被杀掉会进入一个观察期。这个设计是有讲究的。调度器默认会尊重节点污点避免把新业务继续往一个快没磁盘的节点上塞加重故障而已经在跑的 pod 则交给 kubelet 的驱逐管理器去处理。所以你会看到的现象是节点变 NotReady、新 pod 调度不上去、老 pod 陆续被 Evicted。说白了就是——这个节点已经处于只出不进的状态了。1.2 为什么磁盘压力比内存压力更难缠我个人的感觉是内存压力触发了你把有问题的 pod 杀一批memory.available很快就回来了节点恢复 Ready 也就几十秒的事情。但磁盘不一样它是个存量 增量双重问题。存量指的是节点上已经累积了多少镜像、容器可写层、日志文件、临时文件。增量指的是跑着的业务还在持续产生日志和临时数据。如果根因是业务写日志太凶你光清理存量是不够的过几个小时又会压满。另外一个让 DiskPressure 特别恶心的地方在于它有时候不一定是你业务 pod 的问题。比如节点上跑了 etcd、挂了 NFS 客户端、有个 cron 在刷日志甚至上次排障时留下的 coredump 文件都有可能把磁盘占满。加上多租户共享节点的大环境下你很难一眼看出是谁吃掉了磁盘。所以排查 DiskPressure 不能只盯着 kubelet 日志得从文件系统哪里被吃满了这个事实出发一层一层往下剥。2. kubelet 判定磁盘压力的机制拆解2.1 nodefs 和 imagefs 到底指什么要理解 kubelet 的磁盘驱逐逻辑必须先分清两个概念nodefs和imagefs。nodefs是 kubelet 用来存 pod 数据的文件系统默认就是根分区具体路径一般是/var/lib/kubelet。它里面存着每个 pod 的卷、emptyDir 数据、容器可写层的锚点文件等等。imagefs是容器运行时存放镜像的文件系统对 Docker 来说就是/var/lib/docker对 containerd 来说就是/var/lib/containerd。这两个文件系统在很多节点上是同一个分区这种情况下两者的可用空间是共享的kubelet 会按同一个阈值来判定。但如果你的环境里把 docker 目录单独挂了一个盘kubelet 就会分别检查。判定的逻辑是nodefs.available低于阈值 → 节点进入 DiskPressurenodefs.inodesFree低于阈值 → 节点进入 DiskPressureinode 耗尽文件系统满但df -h却显示有空间imagefs.available低于阈值 → 节点进入 DiskPressureimagefs.inodesFree低于阈值 → 节点进入 DiskPressure我见到过一个很经典的坑df -h看根分区还剩 30%但是 inode 用满了df -i显示Use%100%。这时候大量小文件比如容器日志按天切分、临时文件把 inode 吃光kubelet 照样触发 DiskPressure。所以排查的时候df -h和df -i都要看一个都不能少。2.2 驱逐阈值与驱逐流程kubelet 的驱逐管理器是通过一组阈值来判断该不该动手的。默认的硬阈值eviction-hard是信号默认硬阈值memory.available 100Minodefs.available 10%nodefs.inodesFree 5%imagefs.available 15%imagefs.inodesFree 5%这几个阈值之间是有配合的。比如imagefs.available单独低于 15%kubelet 会先触发容器运行时垃圾回收删掉不再使用的镜像、死掉的容器这一步如果能把镜像清理到阈值以上就不会去驱逐 pod。只有nodefs.available出问题也就是 pod 的数据把磁盘占满了垃圾回收无法解决的时候kubelet 才真正开始驱逐 pod。驱逐顺序遵循 QoS 规则BestEffort最优先被驱逐其次是Burstable最后才是Guaranteed。也就是说那些没有设置资源请求限制的 pod 最危险一旦节点出现 DiskPressure它们首先被献祭。而 Guaranteed pod 也并不是绝对安全如果磁盘压力持续到连 Guaranteed pod 都救不回来kubelet 依然会驱逐只是排在最后。这里要特别提醒一个细节驱逐不等于立即 kill。kubelet 会先给 pod 发 SIGTERM等terminationGracePeriodSeconds到期再 SIGKILL。如果你的 pod 承载了有状态数据被驱逐后又不具备漂移能力那你面对的就是数据丢失或者服务长时间不可用。2.3 硬阈值 vs 软阈值生产环境里除了默认的硬阈值还有人会配置软阈值eviction-soft。软阈值和硬阈值的区别在于软阈值触发后 kubelet 不会立刻动手而是等一个eviction-soft-grace-period的宽限期期间如果资源恢复到阈值以上就安然无恙如果宽限期过了还在阈值以下才开始驱逐。我的建议是新集群可以先别急着上软阈值它虽然能提供缓冲但也引入了一层复杂度和误判风险。先把硬阈值调到合理水平、把监控告警配好等稳定性上来了再考虑软阈值。对于已经上了软阈值的集群一定要把宽限期设置得比告警响应时间短否则你还没收到告警系统已经把 pod 驱逐了。3. 从告警到定位一条完整的排查路径3.1 先看节点状态和事件告警群里出现The node had condition: [DiskPressure]之后我的第一步永远是开三个终端窗口同时跑三组命令kubectl get node -o wide kubectl describe node node-name kubectl get events --field-selector involvedObject.namenode-name --sort-by.lastTimestampkubectl describe node里重点看两部分第一是Conditions列表里的DiskPressure从什么时候变成True的第二是底部的Non-terminated Pods列表能快速看到这个节点上到底跑了多少 pod有没有已经处于Evicted状态的。事件里通常能直接看到Evicting pod、Failed to garbage collect required images这类关键信息。如果节点已经变NotReadykubectl describe可能拿不到最新状态那就直接跳到 ssh 登录节点从节点侧排查。注意一旦节点 NotReadycontrol plane 上很多信息都会失真不要在一个不可达的节点上浪费过多时间。3.2 上节点查磁盘的每一层登录节点后按顺序执行以下命令每一步都有明确目的# 看整体磁盘占用和 inode 使用 df -h df -i # 定位哪个目录最大 du -sh /* 2/dev/null | sort -hr | head -10du -sh /*这一步非常关键。见过太多人去查/var/lib/docker结果查了半天都没问题最后发现根因是数据盘挂载点被一个日志文件怼满了。从根目录往下扫一圈是最不容易漏的方法。接着深入具体的目录# 容器运行时数据 du -sh /var/lib/docker /var/lib/containerd 2/dev/null # Docker 的 overlay2 分区占用 docker system df -v # kubelet 下面的 pod 数据 du -sh /var/lib/kubelet 2/dev/null # 按 pod 维度看日志占用 du -sh /var/log/pods/* 2/dev/null | sort -hr | head -20 # systemd journal 占用 journalctl --disk-usage如果是 containerd 运行时查镜像和容器占用用crictlcrictl images crictl ps -a # 按镜像大小排序 crictl images -o json | jq -r .images[] | \(.repoDigests[0]) \(.size) | sort -k2 -rn | head这套命令跑完磁盘去哪了基本就清楚了。正常情况下你会看到三大块镜像占一部分、容器日志占一部分、其他系统文件占一部分。如果某一块异常膨胀就是元凶。3.3 定位元凶的几种典型套路套路一镜像仓库失联导致镜像堆积。这是个很隐蔽的问题。CI/CD 系统把新镜像推到私有仓库里节点上老的镜像 tag 如果一直不能更新比如镜像仓库连接失败、凭据过期kubelet 的镜像垃圾回收是按使用时间来的不用的镜像不会立刻被删。时间一长节点上堆了几十 GB 的悬空镜像。排查方法很简单docker system df -v看Images那一栏如果REPOSITORY是none的镜像特别多就是这类问题。套路二业务日志不轮转或者轮转配置失效。容器日志默认的存储路径是/var/log/pods/namespace_pod名_uid/容器名/0.log大部分环境装了 logrotate但有时候 logrotate 的配置没覆盖到新路径或者容器内的应用自己写了一个超大的日志文件不 split。你会看到du -sh /var/log/pods/*里某个目录轻松上 GB。套路三emptyDir 或者临时文件写爆。有些业务会把临时文件写到 emptyDir 上如果 pod 挂在节点上跑很久emptyDir 越积越大。这种问题通过kubectl describe node不好定位得用du -sh /var/lib/kubelet/pods/*/volumes/*挨个看。我遇到过一次是一个数据同步工具把临时文件写到 emptyDir清理不及时直接把磁盘怼满了。套路四本地 PV 数据膨胀。如果集群用了local存储类节点上的 LocalPV 体积是实打实占用节点磁盘的。这种数据一旦膨胀节点层面的任何 GC 机制都管不到因为 kubelet 不会去动 PersistentVolume 的数据。这种情形只能一边扩容磁盘一边推动业务侧清理数据。4. 止血与根治生产环境的处理方案4.1 紧急清理三板斧不管根因是什么第一要务是先让节点恢复 Ready把业务捞回来。我在生产环境里直接执行的命令组合是# 1. 清理退出的容器 docker container prune -f # 2. 清理悬空镜像注意只清没有 tag 的 docker image prune -f # 3. 清理 docker build 缓存 docker builder prune -f # 4. systemd journal 限制到 200MB journalctl --vacuum-size200M这套下来通常能释放 20~40% 的磁盘。如果还不达标就需要动真格的了——清掉节点上所有不再使用的镜像。这里我强烈建议不要随手docker rmi -f $(docker images -q)万一你误删了正在用的镜像接下来你就要面对一堆 ImagePullBackOff。稳妥的做法是先列出来看看docker images --format table {{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Size}}凡是none的镜像基本可以放心删其余的要确认没有 pod 在引用之后再删。如果以上手段都不够最后一招是把节点上的 pod 全部驱逐到其他节点然后重启 kubelet 或者重启节点。具体操作是kubectl drain node-name --ignore-daemonsets --delete-emptydir-data--delete-emptydir-data这个参数要慎重它会把 emptyDir 数据删掉如果里面有重要临时数据就麻烦。drain 之后节点上没有 pod 了磁盘占用会骤降这时候再处理根因就从容多了。处理好之后kubectl uncordon node-name让节点重新接收调度。4.2 配置容器日志轮转Logrotate 是治本手段里性价比最高的一步。绝大多数日志撑爆磁盘的根因都是容器日志没有限制大小。Docker 的 daemon.json 里可以配置{ log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 } }意思是单个日志文件最大 100MB最多保留 3 个文件也就是一个容器最多占用 300MB 日志空间。改完配置后记得重启 Docker 才会生效systemctl restart docker这个配置只对改完之后新创建的容器生效存量容器不受影响。所以在发布新版本的时候让 pod 滚动重启一次是最好的机会。如果用的是 containerd相关配置在/etc/containerd/config.toml里参照运行时的 log 配置做类似调整。对于系统组件比如 kubelet、containerd 自身的 journal 日志我习惯在/etc/systemd/journald.conf里加上SystemMaxUse200M然后systemctl restart systemd-journald。不要小看这条生产环境里 journal 日志撑爆磁盘的案例一点都不少尤其是在频繁调试 kubelet 的节点上。4.3 调整 kubelet 驱逐阈值默认阈值其实偏保守尤其是根分区本来就不大的节点10% 可能意味着只有几个 GB 的缓冲空间。我自己生产环境的 kubelet 配置是这么改的# /var/lib/kubelet/config.yaml evictionHard: memory.available: 500Mi nodefs.available: 15% nodefs.inodesFree: 10% imagefs.available: 15% imagefs.inodesFree: 10%改完之后重启 kubeletsystemctl restart kubelet把nodefs.available从 10% 提到 15%是让系统在磁盘真正危险之前就提前进入驱逐模式给运维留出操作空间。但注意阈值不是越高越好。如果你把阈值提到 30%那么磁盘占用率稍有波动就会触发 DiskPressure节点频繁进入不可调度状态业务稳定性会受到不小影响。一般我建议在 15%~20% 之间具体要看节点磁盘总容量和业务波动情况。这里还要注意一点memory.available的默认值是100Mi这个其实非常激进等于让节点的可用内存被吃到只剩 100Mi 才开始驱逐。生产环境建议至少提到 500Mi 以上给系统组件留点喘息的余地。这个参数虽然和 DiskPressure 无关但很多维护经验里都是顺手一起调的。4.4 从监控上提前发现DiskPressure 这种故障最理想的状态是永远不要发生。要做到这一点就得提前监控磁盘增长趋势。用 Prometheus 采集节点磁盘指标配合 Grafana 告警是目前的主流做法。几个核心告警规则可以照着配指标表达式告警条件(1 - node_filesystem_avail_bytes{mountpoint/} / node_filesystem_size_bytes{mountpoint/}) * 100根分区使用率 85%持续 5 分钟(1 - node_filesystem_avail_bytes{mountpoint/var/lib/docker} / node_filesystem_size_bytes{mountpoint/var/lib/docker}) * 100docker 分区使用率 85%node_filesystem_files_free{mountpoint/} / node_filesystem_files{mountpoint/} * 100inode 可用率 20%再加一条 Kubernetes 层面的告警直接监听节点条件kube_node_status_condition{conditionDiskPressure, statustrue} 1节点条件出现异常比磁盘使用率告警更直接因为它是 kubelet 亲口确认的压力状态。我现在的习惯是两套告警都配磁盘使用率超过 80% 给一条 P2DiskPressure 条件为 True 给一条 P1这样的话既能在磁盘真正撑爆之前看到苗头又能在故障真正发生时第一时间接到通知。5. 常见问题与避坑清单5.1 为什么清理完还是 DiskPressure很多人会碰到这种情况磁盘明明清理到 40% 以下了kubectl describe node里DiskPressure依然是True。原因是 kubelet 的驱逐状态有一个冷却机制。呢kubelet 内部有个pressureTransitionPeriod默认约 5 分钟左右不到这个时间它不会把节点状态改回去。所以清理完磁盘之后不要急着骂系统等 5~10 分钟再看。如果等了超过 15 分钟依然是 DiskPressure那就不是冷却的问题而是你的采集点没对准。比如节点上有两个文件系统kubelet 检查的是/var/lib/docker所在的分区你清理的却是根分区上的日志自然不生效。这时候把df -h的每行和kuberlet的--root-dir、--image-gc-high-threshold相关配置对着看一遍就能定位为什么状态没恢复。5.2 驱逐阈值设置太激进的后果我见过一个团队把evictionHard.nodefs.available设成了30%理由是想早点预警。结果节点磁盘稍微一波动就触发 DiskPressure几十个 pod 被反复驱逐业务频繁重启最后发现是镜像缓存和日志正常波动导致的误伤。这里要明白一个逻辑驱逐阈值本质上是最后一根稻草的触发线而不是预警线。预警线应该是监控告警。如果你想提前干预请走 Prometheus 告警路线而不要把驱逐阈值调得过激进。驱逐阈值调得过高的另一个副作用是节点长期处于不可调度状态集群资源利用率下降原本能放 20 个 pod 的节点可能只能放 15 个。5.3 磁盘扩容的正确姿势有些场景下清理是治标不治本比如业务数据本身就在增长磁盘扩容才是正解。云上环境扩容磁盘很简单控制台操作后lsblk看到设备变大再用growpart和resize2fs扩分区growpart /dev/vda 1 resize2fs /dev/vda1执行完df -h就能看到根分区变大了。需要注意的是扩容之前先确认数据盘和系统盘是否分开。如果 docker 目录占用了系统盘而你的数据盘还很空直接把 docker 目录迁移到数据盘挂载点上往往比扩容更经济、更彻底。迁移 docker 目录的常规流程是systemctl stop docker rsync -avP /var/lib/docker/ /data/docker/ sed -i s#ExecStart/usr/bin/dockerd#ExecStart/usr/bin/dockerd --data-root/data/docker# /etc/systemd/system/docker.service.d/override.conf systemctl daemon-reload systemctl start docker做这个操作之前千万记得备份并且要在业务低峰期执行。5.4 一页纸速查表症状排查点处理措施节点 DiskPressure True NotReadydf -h、df -i、docker system df清理悬空镜像 容器日志轮转清理后长时间不恢复kubelet pressure transition period等待 5~10 分钟检查采集点是否对准频繁触发 DiskPressurekubelet 驱逐阈值配置降低阈值到合理区间15~20%镜像堆积导致磁盘膨胀docker images看none镜像配置镜像 GC、修复镜像仓库连接单容器日志暴涨/var/log/pods下查找大文件配置 max-size/max-file logrotate本地 PV 撑爆节点du -sh /var/lib/kubelet/pods/*/volumes/*扩容 业务侧清理数据这一页纸打印出来贴在工位旁边基本上就够了。最后聊一点我的个人体会。DiskPressure 这个故障90% 的情况不是突然发生的而是慢慢累积的。镜像越拉越多、日志越写越大、临时文件越攒越厚等到某一天某个目录悄悄顶到了阈值系统才用一个刺耳的告警提醒你。我踩过好几次坑之后现在的原则很简单每个节点上的磁盘增长趋势都要进监控每个节点的驱逐阈值都要在初始化脚本里显式配置每次排查完 DiskPressure 都要顺手把根因记进运维手册。这三件事做到位DiskPressure 告警基本就从月月见变成一年见不到几次了。