Skip to content

磁盘空间还剩一半,为什么写不进文件?一次 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 /data
Filesystem       Inodes   IUsed    IFree IUse% Mounted on
/dev/vdb1      32768000 32768000        0  100% /data

IUse% 100%,IFree 为 0。 根因在第一次 df -i 时就暴露了——前面 25 分钟全花在"空间够为什么写不进去"上。

WARNING

df -h 有空间不代表能写文件。文件的"空间"和"数量"是两个独立配额:空间由容量决定,数量由 inode 决定。排障时 df -h 和 df -i 必须一起看。

第 4 步:定位是哪个目录在贡献文件数 ​

先看总量,确认问题是"文件太多"而不是"某个目录异常":

bash
find /data -xdev -type f | wc -l
# 32418902

3240 万个文件。接下来按目录统计 inode 占用,找出最大来源:

bash
# du --inodes 直接统计目录下的文件数量(GNU coreutils 支持)
du --inodes -s /data/*/ 2>/dev/null | sort -rn | head
27180344        /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 -5
27102218        /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   # 28410022

19G 的空间装着 2700 万个文件,其中 2841 万个是 7 天前的。

根因 ​

三个因素叠加,缺一不可:

  1. 应用侧:本地缓存只有写入逻辑,没有任何清理逻辑。缓存 key 带毫秒时间戳,同一毫秒内的不同请求也会落成不同文件。
  2. 文件系统侧:ext4 的 inode 总数在格式化那一刻就固定了,建好后不能扩容。这块 500G 的盘默认约每 16KB 分配一个 inode,上限 3276 万个。写满了就是写满了,跟剩余空间无关。
  3. 监控侧:只配了 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 -delete

TIP

用 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 快得多

用 VitePress 构建