Kubernetes 节点根分区磁盘占用过高排查复盘
Kubernetes 节点根分区磁盘占用过高排查复盘一、事件概述1. 事件背景2026 年 7 月 15 日上午，监控发现 K8s 工作节点 zcyun-k8s-node5（IP：10.10.184.204）根分区使用率持续攀升至 89%，逼近告警阈值，存在磁盘写满导致节点上所有 Pod 业务中断的风险。该节点承载物联网（IoT）数据、规则引擎、设备管理等核心服务。2. 影响范围A. 影响资源节点根分区 /dev/vda1（总容量 40G，已用 34G，剩余 4.5G）B. 风险等级P2 级（尚未触发业务故障，但距离写满仅一步之遥）C. 潜在影响若根分区写满，将导致节点上 28 个容器无法写入日志与临时数据，引发服务大面积异常、数据库连接失败、Pod 被驱逐等连锁问题。3. 处理结果经排查定位并处置后，根分区使用率从 89% 下降至 23%，释放约 25G 空间，节点恢复正常水位。二、排查过程1. 排查思路磁盘使用率过高的问题，按「先排除 inode 耗尽，再定位大文件归属，最后定位到具体文件」的层级化思路推进，整体决策流程如下。graph TD A["df 显示根分区 89%"] --> B{"inode 使用率?"} B -->|"仅 5%"| C["排除碎文件问题 锁定大文件"] C --> D["du 逐层定位 /var 29G"] D --> E["/var/lib/docker 24G overlay2 可写层"] E --> F["docker system df Containers 22.6G"] F --> G["docker ps -s 定位容器"] G --> H["iot-data 11.4G iot-rule 10.9G"] H --> I["进入容器可写层 UpperDir"] I --> J["rocketmq_client.log 10 份 1.1G 归档"] J --> K["根因锁定"]2. 步骤一：确认磁盘与 inode 状态首先通过 df 命令确认磁盘整体情况，并检查 inode 使用率以排除「小文件过多导致 inode 耗尽」的可能。df -h # 根分区 40G，已用 34G，89% df -i # inode 仅使用 5%结论：inode 使用率仅 5%，排除碎文件问题，判定为「少数大文件」占用空间。3. 步骤二：逐层定位大目录使用 du 命令从根目录逐层向下统计，定位空间占用大头。du -hx --max-depth=1 / # /var 29G du -hx --max-depth=1 /var # /var/lib 24G du -hx --max-depth=1 /var/lib # /var/lib/docker 24G结论：/var/lib/docker 占用 24G，其中 overlay2 目录为最大头；镜像本身仅 2.8G，说明问题出在容器运行时数据。4. 步骤三：区分镜像与容器可写层通过 docker system df 区分镜像缓存与容器可写层占用。docker system df关键发现：Containers 项 SIZE 为 22.61GB，但 RECLAIMABLE 为 0。这意味着占用来自「容器运行时写入可写层」的数据，而非镜像缓存，因此常规的 docker prune 清理无效。5. 步骤四：定位到具体容器通过 docker ps -s 查看每个容器可写层大小，按体积排序。docker ps -s --format 'table {{.Size}}\t{{.Names}}\t{{.Image}}'定位到两个异常容器：容器可写层大小镜像xcloud-srv-iot-data11.4Gnew-iot/xcloud-srv-iot-dataxcloud-srv-iot-rule10.9Gnew-iot/xcloud-srv-iot-rule两者合计约 22G，与容器总占用 22.61G 完全吻合。6. 步骤五：进入可写层定位具体文件通过 docker inspect 获取容器可写层路径（UpperDir），直接查看其中写入的大文件。docker inspect <容器名> --format '{{.GraphDriver.Data.UpperDir}}' # /var/lib/docker/overlay2/<hash>/diff在两个容器的 /root/logs/rocketmqlogs/ 目录下，均发现大量 RocketMQ 客户端日志：文件大小rocketmq_client.log.1 ~ rocketmq_client.log.10每份 1.1Grocketmq_client.log（当前写入）191M ~ 653M7. 步骤六：分析日志内容确认根因查看当前日志文件内容，发现全是 RocketMQ 客户端在 INFO 级别高频打印的持久化日志。2026-07-15 08:49:56 INFO RocketmqClient - [persistAll] Group: iot-data-online-g updateConsumeOffsetToBroker MessageQueue [topic=iot-device-online-changed, brokerName=broker-a, queueId=0] 16020日志在同一毫秒内针对每个 MessageQueue 打印一条，由于该服务订阅了多 topic、多 group、多 queueId、双 broker，每次心跳与持久化周期都会刷出大量 INFO 行，导致日志文件持续膨胀。三、根因分析1. 磁盘占用构成根分区 34G 占用的构成关系如下图所示，容器可写层中的 RocketMQ 日志占据了绝对主体。graph LR A["/dev/vda1 占用 34G"] --> B["容器可写层 22G"] A --> C["journald 日志 3.9G"] A --> D["镜像层 2.8G"] A --> E["系统与其它 5.3G"] B --> B1["iot-data RocketMQ 日志 11.4G"] B --> B2["iot-rule RocketMQ 日志 10.9G"]2. 直接原因两个 IoT 服务容器内嵌的 RocketMQ 客户端，在默认 INFO 日志级别下高频打印 persistAll 与 updateConsumeOffsetToBroker 日志；日志写入容器可写层（overlay2 的 UpperDir），单文件上限 1G、滚动保留 10 份，导致每个容器稳定占用约 11G。3. 根本原因（5 Whys 分析）A. 为什么磁盘占用这么高？两个容器的 RocketMQ 客户端日志累积到约 22G。B. 为什么日志会累积到这么大量？客户端日志级别配置为默认的 INFO，而该级别下会按 MessageQueue 维度高频打印持久化日志。C. 为什么已有的定时清理没有生效？原有的 clearDockerLog.sh 脚本只清理 /var/lib/docker/containers/ 下的 stdout 日志（json.log），而 stdout 日志全机仅 1M；真正的元凶是容器可写层内的应用日志，原脚本完全不涉及。D. 为什么日志写在容器可写层而非挂载卷？应用将日志输出到容器内 /root/logs 目录，未通过 emptyDir、hostPath 或 PVC 挂载到独立存储，也未接入 logrotate 轮转。E. 为什么客户端日志级别未做约束？RocketMQ 客户端默认日志级别为 INFO，应用启动参数未通过 -Drocketmq.client.logLevel 显式调低，缺少日志治理规范。4. 深层反思容器可写层本质是临时存储，不应承载业务日志。日志要么输出到 stdout 由容器运行时统一接管并轮转，要么挂载到独立卷并配置切分。本次问题暴露了日志治理与容器存储规范的双重缺失。四、解决方案1. 临时方案：优化定时清理脚本对原有 clearDockerLog.sh 脚本进行增强，使其覆盖三类元凶，并复用已有的 cron 任务（/etc/cron.d/logclear，每 10 分钟执行一次）。清理脚本的核心逻辑如下：# 1) 容器 stdout 日志（原有功能） find /var/lib/docker/containers/ -type f -name '*-json.log' | while read f; do : > "$f"; done # 2) 容器可写层内的应用日志（新增，命中本次根因） docker ps -q | while read cid; do upper=$(docker inspect "$cid" -f '{{.GraphDriver.Data.UpperDir}}') # 已轮转归档日志：直接 rm 立即释放（不被进程持有） find "$upper" -type f \( -name '*.log.[0-9]*' -o -name '*log*.gz' \) -exec rm -f {} \; # 当前写入的大日志：truncate 清空 find "$upper" -type f -name '*.log' -size +50M -exec sh -c ': > "$1"' _ {} \; done # 3) journald 系统日志（新增） journalctl --vacuum-size=200M执行后效果：overlay2 占用从 24G 降至 7.5G，docker system df 的 Containers 从 22.6G 降至 1.1G。2. 一个关键坑：df 显示滞后清理归档日志后，df 一度仍显示 77% 使用率，而 du 与 docker system df 都显示真实占用仅 8.6G。经过多轮排查（排除 deleted-but-open 文件、mmap 映射、孤儿块、保留块后），最终定位为 Linux 内核在大量文件删除后的块使用统计延迟（VFS lazy accounting）。sync && echo 3 > /proc/sys/vm/drop_caches # 刷新内核统计 df -h / # 立即显示真实值 23%sequenceDiagram participant U as 运维 participant S as 服务器 participant D as Docker participant C as 清理脚本 U->>S: df 发现根分区 89% U->>S: du 逐层定位 S-->>U: /var/lib/docker 24G U->>D: docker ps -s 定位容器 D-->>U: iot-data / iot-rule 共 22G U->>S: 查看可写层日志 S-->>U: RocketMQ 日志爆炸 U->>C: 优化清理脚本并手动执行 C-->>S: 删除归档 + vacuum journal U->>S: df 仍显示 77%（统计延迟） U->>S: sync + drop_caches S-->>U: 真实使用率 23%经验：清理大文件后若 df 未及时下降，应先执行 sync 与 drop_caches 刷新内核统计，避免误判为文件残留或文件系统损坏，切勿贸然 fsck。3. 永久方案：从源头治理日志定时清理只是兜底，RocketMQ 日志仍会持续产生。根治需从应用侧入手，在启动参数中调低客户端日志级别：-Drocketmq.client.logLevel=WARN调整后，INFO 级别的 persistAll 刷屏日志将停止输出，从源头杜绝日志膨胀。4. 预防措施A. 日志治理规范业务日志统一输出到 stdout，由容器运行时接管并配置 json 日志轮转（max-size、max-file）若需文件日志，必须挂载到 emptyDir 或独立卷，并接入 logrotateB. 监控告警前置对节点磁盘使用率设置分级告警（如 75% 预警、85% 紧急）纳入容器可写层增长监控，及时发现异常写入的 PodC. 清理脚本完善保留增强后的清理脚本作为兜底机制，但应明确其仅处理症状，根治仍依赖应用侧日志级别调整。五、经验总结1. 排查方法论inode 优先：磁盘高占用先查 df -i，快速区分「碎文件」与「大文件」两类问题层级化定位：du 逐层缩小范围，再用 docker system df 区分镜像与可写层docker prune 的局限：RECLAIMABLE 为 0 表示占用在运行时可写层，prune 无效，需从可写层入手2. 关键认知A. 容器可写层非持久存储可写层（overlay2 UpperDir）随容器生命周期存在，不应承载重要日志。容器删除重建后可写层即清空，这也提供了一种「重置可写层」的应急手段。B. stdout 日志与可写层日志是两回事/var/lib/docker/containers/ 下的 json 日志是容器标准输出，而应用写到的容器内文件落在可写层，两者清理方式完全不同。排查时需明确区分。C. df 统计可能滞后大量删除文件后，df 的使用率可能因内核统计延迟而不准确，需结合 du、docker system df 交叉验证，必要时 sync + drop_caches 刷新。3. 流程优化建议建立 RocketMQ、Kafka 等消息中间件客户端的日志级别基线规范将本次增强的清理脚本推广到其它 K8s 节点排查类问题形成标准化 playbook，缩短定位时间参考资料RocketMQ Client 日志级别配置说明Docker 存储驱动 overlay2 工作原理Linux drop_caches 与内存回收