SRE实战 · 生产故障复盘Linux 文件系统 inode 耗尽排查
当磁盘空间充足却提示 No space left on device,SRE 如何快速破局?
2026年3月14日 09:15,某头部零售平台在「2026年Q1全链路压测」期间,核心交易链路节点突然爆发大规模写入失败。监控大盘显示,应用侧 P99 延迟从 45ms 飙升至 12s+,业务日志疯狂打印 java.io.IOException: No space left on device。值班 SRE 第一时间介入,常规检查却发现一个反直觉现象:数据盘 /data 使用率仅为 38%,剩余可用空间高达 1.2TB。
⚠️ 故障定级:P1 核心交易中断
磁盘明明有海量剩余空间,内核却拒绝分配存储资源。这是典型的「假满盘」现象,底层根因直指文件系统元数据瓶颈——inode 耗尽。
在 Linux VFS 架构中,文件创建需要同时满足两个条件:Block(数据块)充足,且 Inode(索引节点)充足。Block 存储实际内容,Inode 存储文件元数据(权限、属主、时间戳、Block指针)。当海量小文件持续生成,即使 Block 远未打满,Inode 池也会率先枯竭,导致内核返回 -ENOSPC。
面对 inode 耗尽,常规 du -sh 毫无意义。SRE 团队迅速切换至元数据视角,执行标准化排查流:
Step 1:确认 inode 水位
$ df -ihFilesystem Inodes IUsed IFree IUse% Mounted on/dev/vdb1 500M 500M 0 100% /data$ dmesg | tail -n 5[2026-03-14T09:15:33.882Z] EXT4-fs warning (device vdb1): ext4_create: inode limit reached[2026-03-14T09:15:33.885Z] EXT4-fs error (device vdb1): ext4_journal_check_start: inode table full, no space left
输出明确显示 IUse% 达到 100%,内核日志直接打印 inode limit reached。分区格式化时默认分配了 5亿 个 inode,当前已全部耗尽。
Step 2:定位 inode 聚集目录
面对数千万级小文件,find /data -type f | wc -l 会导致 IO 打满、机器假死。SRE 采用分治法,逐层统计目录 inode 数量:
$ for dir in /data/*/; do echo -n "$dir: "; find "$dir" -maxdepth 1 -xdev -type f 2>/dev/null | wc -l; done | sort -t: -k2 -nr | head -10/data/app_logs/: 482000000/data/cache/tmp/: 1240000/data/exports/csv/: 850000
结果一目了然:/data/app_logs/ 独占 4.82 亿个 inode。进一步查看该目录下文件特征:
$ ls -lhS /data/app_logs/ | head -5total 1.2G-rw-r--r-- 1 appuser appuser 1.2K 2026-03-14 09:10 trace_req_a8f3c.log-rw-r--r-- 1 appuser appuser 0.8K 2026-03-14 09:10 trace_req_b12d9.log-rw-r--r-- 1 appuser appuser 1.5K 2026-03-14 09:10 trace_req_c77e1.log
文件体积极小(0.8K~1.5K),但数量级呈指数增长。结合发布记录,确认是 2026年3月10日 上线的「分布式链路追踪 Agent v2.4.0」引入了异步落盘降级逻辑。
通过 strace -p <PID> -e trace=openat,write,close 抓取进程系统调用,结合研发代码 Review,完整还原故障链条:
1. 降级策略缺陷
新 Agent 默认将 Span 数据批量发送至远端 Collector。当压测导致网络拥塞(TCP 队列积压)时,Agent 触发 fallback_to_disk 逻辑。但实现存在严重 Bug:每条 Trace 独立生成一个 .log 文件,而非追加写入同一 Buffer 文件。
2. 清理机制失效
原设计依赖后台 cleanup_job 在 Collector 恢复后删除本地缓存文件。但由于压测期间 CPU 负载长期 >90%,GC 停顿频繁,定时任务线程被阻塞,清理队列积压超 12 小时,形成单向写入的 inode 漏斗。
3. Ext4 元数据瓶颈
Ext4 文件系统在分配 inode 时需锁定 inode table bitmap。当并发创建请求 > 5000/s 时,ext4_alloc_inode 引发严重自旋锁竞争,dmesg 中出现大量 soft lockup 告警,进一步拖慢系统调用返回速度。
| | | |
|---|
| | | |
| | | dir_index 哈希冲突,lookup 延迟飙升 |
| | | Block 碎片化率 0%,Inode 碎片化 100% |
| | 34.8% (inode_bitmap_lock) | |
🛠️ 紧急止血(MTTR 控制在 15 分钟内)
1. 停止写入源:通过配置中心下发 agent.fallback.enabled=false,阻断新文件生成。2. 批量清理:使用 xargs 分片删除,避免 rm -rf 单进程打满 CPU。3. 释放句柄:对已删除但仍被进程 hold 的文件,重启相关 Java 进程释放 inode。
📝 分步执行命令参考
Step 1:安全删除(按 10万/批)
find /data/app_logs/ -type f -name "*.log" -mtime +0 -print0 | xargs -0 -n 100000 rm -f
Step 2:验证 inode 回收(需延迟 10-30s 生效)
watch -n 2 "df -ih /data | grep -v Filesystem"
Step 3:重建目录结构(防再次打满)
rm -rf /data/app_logs && mkdir -p /data/app_logs/$(date +%Y/%m/%d) && chown appuser:appuser /data/app_logs -R
🔮 长期架构优化
1. 存储引擎替换:将临时缓存目录挂载至 tmpfs 或使用 XFS 文件系统。XFS 采用动态 inode 分配机制,理论支持 2^64 个文件,彻底规避静态上限问题。2. 日志规范落地 强制要求所有 Sidecar 采用 Logrotate + 单文件追加模式(Append-only),禁止按 RequestID 切分独立文件。3. 配额限制:通过 ext4 quota 或 cgroup-v2 io.max 限制单目录 inode 创建速率,设置硬阈值拦截。
本次故障暴露出 SRE 日常巡检的盲区:监控大盘长期仅关注 df -h(Block 使用率),完全缺失 df -i 指标采集。2026年Q1 起,我们已将 Inode 水位纳入基础监控基线,并沉淀了以下标准化资产。
📦 SRE 必备:一键 inode 诊断脚本
该脚本支持快速扫描 Top N 目录、检测已删除未释放句柄、输出结构化报告。可直接保存为 /usr/local/bin/check_inode.sh 并在巡检 Cron 中定时执行(建议 2026-06-08 前完成全量主机部署)。
#!/usr/bin/env bash# 2026-03-20 SRE Team: Inode Exhaustion Diagnostic Toolset -euo pipefailTOP_N=${1:-5}TARGET_DIR=${2:-/}echo -e "\n[INFO] 当前系统时间: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"echo "=========================================="echo "🔍 1. 全局 Inode 使用概览"echo "=========================================="df -ih | grep -vE '^Filesystem|tmpfs|devtmpfs' | awk '{printf "%-20s %8s %8s %8s %5s %s\n", $6, $2, $3, $4, $5, $1}'echo ""echo "=========================================="echo "📊 2. 顶层目录文件数统计 (Top $TOP_N)"echo "=========================================="find "$TARGET_DIR" -maxdepth 1 -xdev -type d 2>/dev/null | while read dir; do count=$(find "$dir" -xdev -type f 2>/dev/null | wc -l) if [ "$count" -gt 0 ]; then printf "%-40s %10d files\n" "$dir" "$count" fidone | sort -k2 -nr | head -n $TOP_Necho ""echo "=========================================="echo "🗑️ 3. 已删除但未释放的句柄检测"echo "=========================================="DEL_COUNT=$(lsof +L1 2>/dev/null | wc -l)if [ "$DEL_COUNT" -gt 0 ]; then echo "⚠️ 发现 $DEL_COUNT 个已删除文件仍被进程占用!" lsof +L1 2>/dev/null | awk '{printf "%-10s %-20s %-10s %-10s %s\n", $1, $2, $3, $4, $9}'else echo "✅ 无泄漏句柄,inode 回收正常。"fiecho ""echo "=========================================="echo "💡 建议操作"echo "=========================================="echo "若 IUse > 80%,请执行:"echo " 1. find <dir> -type f -mtime +1 -delete"echo " 2. lsof +L1 | awk '{print $2}' | sort -u | xargs -r systemctl restart"echo "=========================================="