Appearance
磁盘空间还剩一半,为什么写不进文件?一次 inode 耗尽的排查实录
现象
凌晨 03:17 收到业务告警,Tomcat 应用开始批量报错!!!!!!!!:
java.io.IOException: No space left on device
at java.io.FileOutputStream.writeBytes(Native Method)
at com.xxx.cache.LocalCacheWriter.write(LocalCacheWriter.java:88)第一反应是磁盘满了。登录机器执行 df -h,结果让人迷惑:
Filesystem Size Used Avail Use% Mounted on
/dev/vdb1 500G 225G 250G 45% /data空间还有 250G,使用率只有 45%,但应用就是写不进文件。 这就是这次排查最花时间的地方。
环境
| 项 | 值 |
|---|---|
| 操作系统 | CentOS 7.9,kernel 3.10.0 |
| 文件系统 | ext4 |
| 数据盘 | /dev/vdb1,500G,挂载在 /data |
| 应用 | Tomcat 8.5 + 本地文件缓存 |
| 监控 | Prometheus + node_exporter |
排查过程
第 1 步:确认不是 df 读错
先排除"文件被删除但句柄未释放"这个经典干扰项——这会让 df 显示的空间和实际可用空间不一致:
bash
# 找出已删除但仍被进程占用的文件
lsof | grep deleted | awk '{sum+=$7} END {print sum/1024/1024 " MB"}'输出 0 MB,说明没有句柄泄漏。df 的数据是真的。
第 2 步:手动复现,坐实问题
不看应用日志,直接拿最原始的方式验证——能不能建新文件:
bash
touch /data/test-write.log
# touch: cannot touch '/data/test-write.log': No space left on device确认:确实写不进去,而且这是一个"空间够但写不了"的状态。到这一步,脑子里应该立刻跳出 inode。
第 3 步:看 inode
bash
df -i /dataFilesystem Inodes IUsed IFree IUse% Mounted on
/dev/vdb1 32768000 32768000 0 100% /dataIUse% 100%,IFree 为 0。 根因在第一次 df -i 时就暴露了——前面 25 分钟全花在"空间够为什么写不进去"上。
WARNING
df -h 有空间不代表能写文件。文件的"空间"和"数量"是两个独立配额:空间由容量决定,数量由 inode 决定。排障时 df -h 和 df -i 必须一起看。
第 4 步:定位是哪个目录在贡献文件数
先看总量,确认问题是"文件太多"而不是"某个目录异常":
bash
find /data -xdev -type f | wc -l
# 324189023240 万个文件。接下来按目录统计 inode 占用,找出最大来源:
bash
# du --inodes 直接统计目录下的文件数量(GNU coreutils 支持)
du --inodes -s /data/*/ 2>/dev/null | sort -rn | head27180344 /data/app
3912047 /data/logs
1102841 /data/backup
213670 /data/tmp/data/app 占了两千七百万个。继续往下钻:
bash
du --inodes -s /data/app/*/ 2>/dev/null | sort -rn | head -527102218 /data/app/cache锁定 /data/app/cache。
第 5 步:确认这些小文件是什么
bash
ls /data/app/cache | head -5
# 1728364920113_8821.cache
# 1728364920147_8822.cache
# 1728364920188_8824.cache
# 看文件名里的时间戳,换算一下
python3 -c "import datetime; print(datetime.datetime.fromtimestamp(1728364920))"
# 2026-09-30 14:42:00文件名前缀是毫秒级时间戳——每毫秒都可能产生一个新文件。再看目录大小和文件年龄分布:
bash
du -sh /data/app/cache # 19G
find /data/app/cache -type f -mtime +7 | wc -l # 2841002219G 的空间装着 2700 万个文件,其中 2841 万个是 7 天前的。
根因
三个因素叠加,缺一不可:
- 应用侧:本地缓存只有写入逻辑,没有任何清理逻辑。缓存 key 带毫秒时间戳,同一毫秒内的不同请求也会落成不同文件。
- 文件系统侧:ext4 的 inode 总数在格式化那一刻就固定了,建好后不能扩容。这块 500G 的盘默认约每 16KB 分配一个 inode,上限 3276 万个。写满了就是写满了,跟剩余空间无关。
- 监控侧:只配了
disk_usage告警,没配 inode 告警。这次是应用报错才发现的,不是监控发现的。
第 3 点才是真正的问题——故障不是"没有预警",而是"预警的维度缺了一个"。
修复
立即止血:清理过期缓存
WARNING
直接删除活跃的缓存目录有风险。这里删除的是 -mtime +7 的过期文件,建议先停掉应用的写入或确认应用能容忍缓存失效,再执行。生产环境请勿直接照抄。
bash
# 先看看要删多少,确认范围
find /data/app/cache -type f -mtime +7 | wc -l
# 分批删除,避免单次 IO 打满磁盘影响业务
find /data/app/cache -type f -mtime +7 -deleteTIP
用 find -delete 而不是 -exec rm -f {} \;。后者每个文件都要 fork 一次 rm 进程,2700 万文件会跑到天荒地老;-delete 在同一进程内完成,快一个数量级。
清理后验证:
bash
df -i /data
# IUse% 从 100% 降到 18%,应用恢复写入根治:让清理自动化
加一个每天凌晨执行的清理任务,只保留 3 天数据:
bash
cat > /etc/cron.daily/clean-app-cache <<'EOF'
#!/bin/bash
CACHE_DIR=/data/app/cache
find "$CACHE_DIR" -type f -mtime +3 -delete 2>/dev/null
find "$CACHE_DIR" -type d -empty -delete 2>/dev/null
EOF
chmod +x /etc/cron.daily/clean-app-cache补上监控:把 inode 加进告警
node_exporter 本身就暴露了 inode 指标,只是之前没配规则。加一条 Prometheus 告警:
yaml
groups:
- name: filesystem
rules:
- alert: FilesystemInodeAlmostFull
expr: node_filesystem_files_free / node_filesystem_files * 100 < 15
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} {{ $labels.mountpoint }} inode 剩余不足 15%"
description: "当前剩余 {{ $value | printf \"%.1f\" }}%,请检查是否有小文件堆积。"复盘
排查耗时 40 分钟,其中约 25 分钟浪费在"空间明明够为什么写不进去"上。
如果第一反应就是 df -h 和 df -i 一起看,这个时间可以压缩到 5 分钟以内。所以结论不是"下次要更仔细",而是把动作固化下来:
| 改进项 | 具体动作 |
|---|---|
| 排查习惯 | 磁盘类问题第一条命令就是 df -hi,两个维度一起看 |
| 监控覆盖 | 容量和 inode 分别告警,缺一个都算监控不全 |
| 应用设计 | 需要长期存在的本地缓存,必须自带 TTL 清理,不能依赖运维兜底 |
| 文件系统选型 | 如果目录下必然是小文件海量场景,格式化时显式调大 -i(inode 密度),或者改用 xfs |
最后一条值得单独说:ext4 的 inode 数量无法事后调整,只能在 mkfs 时通过 -i 参数决定。这是一个"建盘时就定了、事后无法补救"的决策——如果早知道会有海量小文件,mkfs.ext4 -i 4096 可以让 inode 上限翻四倍。
附:本次用到的命令
| 命令 | 用途 |
|---|---|
df -hi | 同时查看容量和 inode 使用率,磁盘问题第一命令 |
lsof | grep deleted | 排查文件已删除但句柄未释放导致的空间不一致 |
du --inodes -s <dir>/* | 按目录统计 inode 占用,快速定位文件堆积点 |
find <dir> -type f -mtime +7 | wc -l | 统计符合条件的目标文件数,删除前先确认范围 |
find <dir> -type f -mtime +7 -delete | 批量删除过期文件,比 -exec rm 快得多 |