系统与运维

inode 耗尽故障:磁盘没满却写不进去

典型症状是 df -h 显示使用率只有 20%,却持续报 No space left on device。根因是文件系统把 inode 用光了——每个文件、目录、符号链接都占一个 inode,与占用多少数据块无关。本文讲清 inode 与数据块的两套独立账本、小文件场景为何最危险、如何逐层统计目录里的文件数量,以及不误伤数据的清理与预防策略。

作者:巧匠团队·9 分钟阅读·更新于 2026-09-30

两套独立的账本:数据块与 inode

每个支持 POSIX 权限的 Linux 文件系统都维护两套资源:一类是**数据块**(block),存文件内容;另一类是**inode**,存文件的元数据——权限、属主、时间戳、大小、指向数据块的指针。inode 本身也是磁盘上的一小块空间,但它是**预先固定数量**的。

关键在于 inode 总数在格式化时就确定了。ext4 默认给整个文件系统分配固定比例的块用作 inode 表,所以哪怕一个数据块都没写,inode 也可能先用完。这就是「`df -h` 显示 20% 使用率却写不进去」的全部原因:容量账和数据没问题,文件数账已经爆了。

还有个容易混淆的点:inode 耗尽时报错信息和空间耗尽**完全一样**,都是 `No space left on device`(errno ENOSPC)。内核不会区分原因,所以唯一可靠的办法是养成 `df -h` 和 `df -i` 一起看的习惯,只看其中一个必然被误导。

符号链接、硬链接、空文件、目录各占一个 inode,其中空文件也照样占。这解释了为什么一个 0 字节的文件能成为罪魁祸首——它不占数据块,但占一个 inode。大量此类文件累积起来足以吃掉一整块文件系统。

df -h          # 数据块水位
df -i          # inode 水位,IUse% 达到 100% 即耗尽
# 两条一起看,缺一不可

哪些场景最危险

① 海量小文件:典型的静态资源目录、编译产物(`node_modules`、`.venv`、`target/`)、备份解包目录。每个文件吃一个 inode,但只占极少的块——典型的「容量空、inode 满」。单文件 4KB 块配 1KB 内容时,一个 inode 就浪费掉了 3/4 的块空间。

② 会话与缓存目录:PHP/Java 的 session 目录、Redis 的持久化分片、应用的本地缓存(如 `~/.cache`)、Nginx 的 `proxy_cache`。它们的特点是文件生命周期短、数量增长快,某个批处理任务一晚上就能写出几十万个小文件。

③ 邮件与队列 spool:`/var/spool/postfix`、Postfix 的队列、未做归档的日志分段。消息一封一个文件、附件一封多文件,遇到突发流量增长非常陡峭。

④ 容器 overlay 层与镜像层:每个镜像层、每个容器可写层都算在 overlay 的 inode 账上,本地跑几十个镜像的机器极易触发。这点常被忽略,因为直觉上容器「跑在别处」。

df -i /
# 典型输出:iused 100%,avail 为 0,而 df -h 仍有大量剩余容量

定位:逐层统计文件数量

思路和定位大目录一样,只是把 `du` 换成 `find | wc -l`。仍然采用「先分区、再下钻」的策略,避免一上来就扫全盘造成 IO 风暴——在 inode 紧张的机器上,全盘 find 可能要跑几十分钟。

第一步在报满的挂载点内做一层统计。注意目录本身也占 inode,所以统计结果和 `df -i` 的已用数会有少量差额,这是正常的,不要为了对齐而困惑。

第二步钻进最大的那个目录继续统计,通常两三轮就能锁定。最快的判定法是数「文件数远大于看起来的数据量」的比例:如果某目录有 50 万个文件却只占 200MB,inode 账必然是它的主项。

很多情况下你甚至不需要精确定位,直接对已知的嫌疑目录(session 目录、缓存目录、`node_modules`)跑一次 `ls | wc -l` 就能确认。但在生产机上做这一步前,先确认 `find` 不会触发大量目录读取——必要时加上 `-xdev` 限制在挂载点内。

find /var -xdev -type f -o -type d 2>/dev/null | wc -l
# 逐层下钻,对比每个子目录的文件数
for d in /var/*; do printf "%s " "$(find "$d" -xdev 2>/dev/null | wc -l)"; echo "$d"; done | sort -rn | head

安全清理与扩容

清理的第一原则是「删可再生的小文件,别碰数据」。缓存、session、临时文件、构建产物都属于可再生类别,删掉后应用能自行重建,代价只是短暂变慢。数据库文件、用户上传、日志归档不在此列——它们即使也是小文件,删除都是数据事故。

对缓存目录,优先用应用自带的清理命令而不是 `rm -rf`:Redis 用 `SCAN` 渐进删除避免阻塞,Nginx 用 `proxy_cache_purge`,包管理器用 `apt clean` / `yum clean all`。直接 `rm -rf` 一个包含海量文件的目录会产生大量目录项操作,在 inode 已满时可能直接卡住甚至失败。

清理前先确认空间真的会释放:inodes 是立即回收的,删除文件后 inode 立刻可用,不需要重启或任何同步动作。这一点和磁盘空间一致,所以可以边清边观察 `df -i`。

如果清理拿不出足够的空间,或者文件数本来就是业务必需(海量小文件本身就是业务数据),那唯一出路是扩容或换文件系统。ext4 的 inode 数在建 FS 时固定,无法在线调整;`xfs` 可以通过 `xfs_growfs` 在线扩容,但 inode 上限同样在建 FS 时确定。另外要注意,某些文件系统(如 `tmpfs`)的大小在挂载时由 size 参数决定,重新挂载并增大参数即可快速缓解。

du -sh /var/cache/* 2>/dev/null | sort -rh | head
apt clean
rm -rf /var/lib/app/sessions/*   # 可再生内容,可安全清理
df -i /                            # 清理后立即复查
mount -o remount,size=4G /dev/sdb1 # tmpfs 类可直接放大

预防:监控与限额

监控上把 inode 使用率做成独立告警项,而不是附在磁盘使用率里。典型阈值是 IUse% 超过 80% 警告、90% 严重——注意 inode 通常是**阶跃式**消耗的,一旦告警,往往很快就会到 100%,比容量耗尽留给你的反应时间短得多。

其次是给「小文件型」目录设配额或独立挂载点。把 session、缓存、日志 spool 放到独立的 ext4/xfs 挂载点上,即使它被写满,也不会连带把根分区拖垮。systemd 提供了 `RuntimeMaxSec` 之外的另一套手段:对服务加 `TemporaryFileSystem=/var/cache/app` 之类的指令,让临时文件只存在于内存文件系统,进程退出即消失。

应用层同样有责任:把「每条记录一个文件」的写法改成批量文件(合并日志分片、数据库代替文件树),或者强制设置最小文件尺寸(很多业务导出可以直接改用 tar 包)。这类改造能从根本上把 inode 账从数据块账上摘下来。

最后是排查纪律:任何 `No space left on device` 故障,恢复后都要留一条结论:这次是容量还是 inode?下次同样症状时值班的人能在 10 秒内判断,而不是再花一小时重新推一遍。

# 告警表达式思路:inode 使用率独立成一条规则
# node_filesystem_files_free / node_filesystem_files  < 0.1  即已耗尽
systemctl show app.service -p TemporaryFileSystem

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。