两套独立的账本:数据块与 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