磁盘明明还有好几个 G 的剩余空间,应用却开始大面积报错” No space left on device “,新日志写不进去、上传接口直接 500、连 Nginx 都吐不出临时文件。这种”磁盘 inode 耗尽”的诡异故障,在生产环境比你想的更常见——它和真正的磁盘满完全不同,用 df -h 永远看不出问题,必须 df -i 才能露馅。本文复盘一次真实的服务异常,带你从现象一路定位到根因,并给出可落地的根治与监控方案。
一、故障现场:磁盘有空间,却写不进文件
某天凌晨,监控开始零星报警:订单服务的上传接口成功率从 99.9% 掉到 80%,错误日志里反复出现 No space left on device。第一反应是磁盘满了,于是登录机器执行 df -h,结果让人困惑——数据盘还有 12G 空闲,使用率只有 67%。但应用确实写不进任何新文件,连 touch /tmp/test 都报同样的错。
这就是 inode 耗尽最典型的症状:文件系统的”存储空间”和”文件数量配额(inode)”是两个独立的资源。磁盘还有空间,但用来记录文件元信息的 inode 已经被占满,系统无法再创建哪怕一个 0 字节的新文件。很多开发者第一次遇到时都会卡在”明明有空间为什么写不进”,根因就在这里。
二、先分清两件事:空间满 vs inode 满
2.1 df -h 与 df -i 的区别
df -h 看的是”已用/剩余字节数”,对应数据块(block);df -i 看的是”已用/剩余 inode 数”,对应文件数量上限。一个有空间但 inode 耗尽的文件系统,df -h 一切正常,df -i 却会显示 Use% 100%。下面这条命令是排查的第一步,必须同时看两列:
# 同时查看空间使用率和 inode 使用率
df -h /data
df -i /data
# 典型 inode 耗尽输出(注意 IUse%)
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/vdb1 6553600 6553600 0 100% /data
2.2 一张表看懂症状差异
| 现象 | 磁盘空间满(block) | inode 耗尽 |
|---|---|---|
| df -h 使用率 | 接近 100% | 通常正常 |
| df -i 使用率 | 正常 | 100% |
| 写新文件报错 | No space left on device | No space left on device |
| 删除大文件能否缓解 | 能 | 不能(要看删的是不是小文件) |
| 常见元凶 | 日志/备份/上传堆积 | 海量小文件/失控的 session 目录 |
看到这里应该明白:同样是”写不进文件”,根因可能完全相反,处置手段也不同。误判为磁盘满去删大文件,往往白忙一场。和 服务器时钟漂移、限流误杀 这类”症状与根因错位”的故障一样,inode 耗尽也最考验排查者的第一直觉。
三、定位 inode 消耗大户
3.1 全文件系统扫描(慎用)
最直白的思路是统计每个目录下的文件数量。但注意:对整个根文件系统直接 find / 会非常慢,且可能扫到 /proc、/sys 等虚拟文件系统导致报错。务必加 -xdev 限制在同一文件系统,并排除挂载点。
# 统计整块盘的文件总数(只扫本文件系统)
find /data -xdev -type f | wc -l
# 如果只想看某个挂载点
find / -xdev -type f 2>/dev/null | wc -l
3.2 分级定位目录
全盘计数只能告诉你”确实有很多文件”,要定位到具体目录,需要逐层下钻。下面这段小脚本会遍历一级子目录,分别统计每个目录里的文件数,按从多到少排序,快速锁定”重灾区”。
# 在疑似根目录下,统计各一级子目录的文件数
cd /data
for d in */; do
printf "%-30s %s\n" "$d" "$(find "$d" -type f 2>/dev/null | wc -l)"
done | sort -k2 -n -r | head -20
一旦锁定目标目录(比如 /data/app/logs 下挂着 300 万个小文件),继续往里下钻,直到找到真正失控的路径。经验法则:单个目录超过 10 万文件,就该警惕了,无论是日志、缓存还是上传临时文件。
四、真实根因:谁在疯狂制造小文件
4.1 日志目录失控
本次故障的根因,是某个定时任务每分钟生成一份调试日志,文件名带毫秒时间戳,一份只有几 KB,一年累积下来就是千万级小文件。日志没有轮转、没有清理,inode 被慢慢吃光。这是 inode 耗尽最高频的元凶。
4.2 Docker 镜像层与容器残留
另一个常见来源是 Docker:频繁构建镜像、反复启停容器会在 /var/lib/docker 下堆积大量层文件和临时目录。如果没定期 prune,一台 CI 机器跑几个月就可能把 inode 吃满。可参考 Docker Compose 一键编排 的目录规划,给 Docker 数据单独挂盘,避免和主业务争抢 inode。
4.3 其他常见元凶
还有几类也容易踩坑:PHP 的 session 文件长期不 GC、邮件服务器的 spool 队列、消息队列的落盘小消息、以及崩溃产生的 core dump 碎片。排查时把上面”分级定位”脚本在 /var、/tmp 也各跑一遍,通常能一网打尽。
五、临时止血与彻底根治
5.1 临时清理
线上先止血:删除明确无用的小文件。注意大批量删除时不要一次性 rm -rf *,文件太多会让参数展开失败,建议用 find 配合 -delete 分批删,并限制单次数量防止 IO 打满影响业务。
# 删除 7 天前的日志小文件(先 dry-run 确认)
find /data/app/logs -type f -name '*.log' -mtime +7 -print
find /data/app/logs -type f -name '*.log' -mtime +7 -delete
# 分批删除,避免单次参数过长
find /data/app/cache -type f -print0 | head -n 50000 | xargs -0 rm -f
5.2 logrotate 兜住日志
根治日志类问题,最稳的是上 logrotate,按大小或时间切割、压缩、保留 N 份后删除。下面是一份可直接用的配置,每天检查、超过 50M 即轮转、保留 7 份:
# /etc/logrotate.d/myapp
/data/app/logs/*.log {
daily
size 50M
rotate 7
compress
delaycompress
missingok
notifempty
create 0644 app app
postrotate
systemctl reload myapp >/dev/null 2>&1 || true
endscript
}
5.3 Docker 定期 prune
针对 Docker 残留,用定时任务定期清理悬空镜像、停止容器的卷和网络。把它写进 crontab,每周日凌晨执行一次,能长期压住 inode 增长:
# 每周日 03:00 清理悬空资源(保留近 24h 的镜像以便回滚)
0 3 * * 0 docker image prune -f
0 3 * * 0 docker container prune -f
0 3 * * 0 docker volume prune -f
六、把 inode 纳入监控告警
排查完一次还不够,关键是让下一次在发生前就被发现。inode 使用率一样要进监控系统。Node Exporter 已经暴露了 node_filesystem_files_free 等指标,配合 Prometheus 一条告警规则即可:
# Prometheus 告警规则:inode 使用率超过 80% 即告警
- alert: InodeWillFill
expr: |
(node_filesystem_files - node_filesystem_files_free)
/ node_filesystem_files * 100 > 80
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.mountpoint }} inode 使用率超 80%"
如果暂时没有 Prometheus,一段简单的 shell + 阈值判断配 crontab 也能顶上,核心是”持续观测、提前预警”,而不是等业务已经报错再救火。
七、上线前排雷 checklist
把下面几条当成服务上线的默认项,能挡掉绝大多数 inode 故障:① 任何写文件的目录都要有清理策略(logrotate 或定时脚本);② Docker 数据单独挂盘并配置定期 prune;③ 监控里加入 df -i / inode 使用率指标,阈值设在 80%;④ 单目录文件数超过 10 万要有告警;⑤ 临时文件目录(/tmp、上传缓存)必须有 TTL 清理。这些和 Redis 缓存三大问题 的防护思路一致:把”容量”当成会耗尽的一等公民来对待。
结语
inode 耗尽是一个”看 df -h 永远发现不了”的陷阱,但它背后没有魔法:海量小文件 + 缺乏清理策略,迟早会把 inode 吃光。排查路径很固定——先用 df -i 确认、再用分级脚本定位目录、最后从日志/Docker/session 里揪出元凶。止血靠清理,根治靠 logrotate 与定期 prune,长效靠把 inode 写进监控。下次再遇到”有空间却写不进”,希望你能比故障先一步想到它。




