ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Kubernetes DiskPressure 排查与根治:从驱逐机制到生产实践

Kubernetes DiskPressure 排查与根治:从驱逐机制到生产实践 凌晨两点四十值班群炸了。订单服务连续被驱逐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 和 imagefskubelet 眼里的两块盘要理解 DiskPressure先得知道 kubelet 不是监控整台机器的所有磁盘它只关心两条文件系统路径nodefskubelet 自身工作目录所在的文件系统包括/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 条件为DiskPressureTrue调度器随即屏蔽新 Pod。主动回收镜像和死容器也就是触发 imageGC 和 containerGC。按照 QoS 等级从低到高驱逐 Pod先是 BestEffort再是 Burstable最后才轮到 Guaranteed。这里注意驱逐不是随机的kubelet 内部会按照pod 实际使用量超过请求量的比例排序超得越多越先被清理。想一下这个逻辑就知道为什么 DiskPressure 可怕一个节点上的 Pod 被逐到别的节点别的节点如果容量也紧张很容易产生驱逐雪崩。所以看到 DiskPressure 别只盯着当前节点要顺手看看集群里其他节点的水位。2. 现场排查第一步定位涨的是数据还是镜像、是空间还是 inode2.1 三组命令快速看清盘面接到告警后我的习惯是先不做任何破坏性操作先跑三条命令把盘面看清楚df -h df -i lsblkdf -h看空间df -i看 inodelsblk看分区挂载关系。重点看两个挂载点/和/var/lib/containerd或/var/lib/docker。如果这两个路径的df -h结果显示的是同一个分区说明 nodefs 和 imagefs 混在一起那排查范围就得覆盖全部数据。接着用du往下钻找出真正吃空间的大头du -xh --max-depth2 /var/lib/kubelet 2/dev/null | sort -h | tail -30 du -xh --max-depth2 /var/lib/containerd 2/dev/null | sort -h | tail -30 du -xh --max-depth3 /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 日志确认它到底因为什么触发、驱了哪些 Podjournalctl -u kubelet --since 2 hours ago --no-pager | grep -iE evict|diskpressure|imagefs|nodefskubectl 侧看事件和 Node 条件kubectl describe node node-01 | grep -A20 Conditions kubectl get events -A --field-selector reasonEvicted -n productionkubectl describe node输出里 DiskPressure 条件的 Reason 一般是KubeletHasDiskPressureMessage 是kubelet has disk pressure。通过驱逐事件能看到一排被 Evicted 的 Pod 名结合它们的 QoS 等级就能反推 kubelet 当时的驱逐顺序从而判断是内存还是磁盘压力在起作用——这步很多人忽略其实特别有用。我把整个排查起点整理成一个表格检查项命令关注点空间使用df -h / /var/lib/containerd是否接近阈值inode 使用df -i / /var/lib/containerdinode 是否接近 100%大目录分布du -xh --max-depth2 ...日志、镜像、emptyDir 谁最大kubelet 阈值cat /var/lib/kubelet/config.yamlevictionHard 实际值驱逐动作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 的根分区写满节点 DiskPressurePod 被驱逐业务全挂。这里特别容易踩坑的点是很多人排查看/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-size200M第二步清容器日志。找到超过 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-storageresources: 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% 给 warning90% 给 criticalinode 使用率 85% warning95% 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里设置SystemMaxUse500M限制 journal 总量Ubuntu 系的/var/log里unattended-upgrades日志有时候也能悄悄攒好几个 G。这些平台债平时不起眼一旦碰上 DiskPressure清理起来比容器日志还麻烦。最后聊一点个人体会。我处理过最难受的一次 DiskPressure不是盘真的满得不可救药而是告警来时大家急着删文件删掉了正在容灾切换的备份数据最后磁盘压力解了业务数据也丢了。所以每次遇到这类告警我给自己定了一条铁律先花五分钟看监控确认是哪个文件系统、哪类数据在涨再决定动刀的方向。DiskPressure 的覆盖面很广——空间、inode、镜像、日志、emptyDir每一样的处置方式都不一样。磨刀不误砍柴工这五分钟的定位永远比盲目清理值钱。
返回列表