磁盘没满却写不进去:Linux inode 耗尽排查实战,df -i、find 统计与幽灵 inode 定位

df 显示还有空间,服务却写不进去:inode 耗尽的典型症状

这是一个让无数站长第一次遇到时完全懵圈的故障:网站突然上传不了文件、PHP 报 No space left on device、MySQL 写入失败,但你登录服务器执行 df -h,却看到磁盘使用率只有百分之四十,明明还有一大半空间空着。于是你开始怀疑权限、怀疑磁盘坏道、怀疑文件系统损坏,折腾很久都找不到原因。

真相通常是:你的磁盘 inode 用完了,但数据块(block)还有大量剩余。这是两个完全独立的资源,df -h 只显示块的使用情况,要看 inode 必须加 -i 参数。本文把 inode 的本质、耗尽原因、排查路径和根治方案讲透。

inode 到底是什么:把文件系统想成一本图书馆

Linux 的每个文件都由两部分组成:数据块(block)存放文件的实际内容,索引节点(inode)存放文件的元信息——权限、属主、大小、创建修改时间,以及「内容存放在哪些数据块上」的指针。可以把 inode 理解成图书馆的目录卡片,block 理解成书架上的书。找书要先翻卡片,卡片用完了,就算书架再空也放不进新书。

文件系统格式化时会一次性分配固定数量的 inode,之后无法在线增加。用 dumpe2fs 可以看到某个文件系统的 inode 总量和每多少字节分配一个 inode:

# 查看 inode 总量、已用、空闲
df -i

# 查看文件系统的 inode 配置(Inode count / Bytes per inode)
dumpe2fs -h /dev/vda1 | grep -i inode

默认情况下 ext4 大约每 16KB 分配一个 inode。也就是说,一个 40GB 的分区大约有 250 万个 inode。听起来很多,但如果你存放的是大量极小的文件,就可能把 inode 先于块空间耗完。

什么场景最容易耗尽 inode

inode 耗尽不是平均分布的,它高度依赖你存放的文件类型。以下五类场景是重灾区:

  • PHP 会话文件/var/lib/php/sessions/tmp 里每个用户会话就是一个几十字节的小文件。默认 session 回收靠概率触发,站点流量一上来,会话文件可能堆积到几十万个。
  • 邮件队列:如果服务器装了 Postfix 或用了 mail() 发信(比如 Typecho 评论通知),退信和队列文件会长期堆积在 /var/spool/postfix/var/mail
  • 缓存碎文件:某些 PHP 缓存、模板编译缓存会把一个页面的缓存拆成大量小文件,每个 visitor 一个条目。
  • npm、Composer 依赖node_modules 是著名的 inode 杀手,几万个几 KB 的文件是常态,多个项目叠加就很可观。
  • 日志切割与临时文件:定时任务写出的临时文件、统计程序生成的碎文件,如果没有清理机制也会慢慢堆积。

另外还有一个隐蔽原因:已删除但仍被进程占用的文件。文件被删后如果还有进程持有它的文件描述符,inode 不会立即释放。这在下文会单独讲。

排查路径:五步定位吃光 inode 的元凶

确认是 inode 问题后,要快速找到是哪个目录在疯狂产小文件。按下面五步走,通常十分钟内能锁定元凶。

第一步,确认确实是 inode 耗尽。

df -i
# 关注 IUse% 一列,达到 100% 即是本故障

第二步,找出哪个分区有问题,然后从该分区的根目录逐层下钻。注意统计 inode 数量用 find 配合 wc -ldu 统计的是块大小而不是 inode 数,这里不能用 du:

# 统计 /var 下所有文件(含子目录)的数量
find /var -xdev -type f | wc -l

# 逐个子目录统计,找出集中点(按文件数排序)
for d in /var/*/; do
    printf "%8d  %s\n" "$(find "$d" -xdev -type f 2>/dev/null | wc -l)" "$d"
done | sort -rn | head -20

-xdev 的作用是不跨越文件系统边界,避免把其他挂载点也stat一遍,既慢又不准。找到文件数异常的子目录后,继续对它下钻,重复同样的循环即可。

第三步,处理已删除但被占用的文件。如果 df -i 显示已满,但 find 统计出来的文件数跟 inode 总量对不上,多半就是这类「幽灵 inode」。用 lsof 找出持有已删除文件的进程:

# 找出持有 deleted 文件、且按大小排序的前 20 个
lsof -nP 2>/dev/null | grep -i deleted | awk '{print $7, $1, $2, $9}' | sort -rn | head -20

输出里的第一列是文件大小,第三列是进程 PID。确认这些是日志文件(比如某个被删掉的 access_log 仍被 Nginx 持有)后,最干净的做法是平滑重启持有它的进程nginx -s reopensystemctl reload),让进程重新打开新文件、释放旧 inode。不要直接 kill -9,那样会中断服务。

第四步,针对会话文件做定向清理。PHP 会话目录是最高频的元凶。先看数量,再按时间清理过期会话:

# 查看会话文件数量
find /var/lib/php/sessions -type f | wc -l

# 删除 1 天前的会话文件(比修改时间更稳妥的是按访问时间)
find /var/lib/php/sessions -type f -mtime +1 -delete

清理只是一时之计,要根治还要调整回收策略。PHP 的 session.gc_probability 默认是 1、session.gc_divisor 默认是 100,意味着每 100 次请求才有 1 次触发回收,低流量站点可能一天都触发不了几次。更好的做法是给会话设置合理的过期时间,并用系统定时任务兜底清理,而不是依赖 PHP 的概率回收。

第五步,处理邮件队列。检查 /var/spool/postfix/maildrop/var/spool/postfix/deferred,如果堆积严重,说明有大量发信失败在重试。先看队列状态,再决定是清理还是修复发信配置:

# 查看邮件队列
mailq | tail -5

# 清空队列(仅在确认这些邮件都是垃圾/退信时使用)
postsuper -d ALL

根治方案:四个层面的长效措施

临时清理解决不了根本问题,下一次流量高峰还会复发。建议从下面四个层面建立长效机制。

层面一:给临时目录挂 tmpfs。把会话目录或缓存目录放到内存文件系统上,既快又天然不会消耗磁盘 inode(重启后自动清空)。注意 tmpfs 会占用内存,容量要设合理:

# /etc/fstab 中挂载,给 256MB 上限
tmpfs /var/lib/php/sessions tmpfs rw,nosuid,nodev,noexec,size=256M,mode=1733 0 0

挂载 mode=1733 是为了满足 PHP 会话目录对权限的要求(仅属主可写、其他用户可创建)。挂载后记得把发行版自带的 tmpfiles 清理规则也考虑进去,避免两套机制打架。

层面二:升级或迁移文件系统时选对 inode 密度。如果确实需要存放海量小文件,XFS 相比 ext4 在小文件密集场景下表现更好,它采用动态 inode 分配,不会出现「块有空间但 inode 用尽」的情况。重新格式化分区时(务必已经备份!)可以用 mkfs.ext4 -i 8192 把 inode 密度提高一倍,或者直接选用 XFS。

层面三:给碎文件重灾区加监控。把 inode 使用率纳入日常巡检,在到 80% 时就告警,而不是等到写不进去才发现。一行 crontab 就能做到:

# 每天 9 点检查根分区 inode 使用率,超过 80% 写入告警文件
0 9 * * * df -i / | awk 'NR==2 && int($5)+0 >= 80 {print strftime("%F %T"), "inode 告急", $5 > "/var/log/inode-alert.log"}'

层面四:为定时任务和缓存程序设上限。任何会持续写小文件的程序,都要有清理机制。检查 /etc/cron.dailylogrotate 配置和自建程序,确保临时文件、编译缓存、会话都有明确的保留期。特别是自己写的统计或采集脚本,务必在写完文件后主动清理,不要指望它自己消失。

一个真实的排查记录:会话文件吃掉 300 万 inode

把上面的方法串成一个具体场景,你会更清楚整个排查链路是怎么走的。某天早上后台开始报上传失败,前台评论提交也报错,页面提示写入失败。执行 df -h 看到根分区只用了百分之四十三,一切正常;但执行 df -i,IUse% 赫然是 100%。判断明确,就是 inode 耗尽。

接着从 /var 开始逐层统计文件数,很快锁定了 /var/lib/php/sessions 这个目录——里面躺着三百多万个会话文件。进一步查看这些文件的修改时间,发现绝大部分都是几个月前的,也就是说站点从上线以来,会话文件几乎没有被有效回收过。

为什么会这样?原因是该站点的 PHP 使用了默认的会话回收配置,session.gc_probability 为 1、session.gc_divisor 为 100,同时 session.gc_maxlifetime 设得不短。在访问量不高的情况下,触发回收的概率极低,加上每个访客都会产生一个会话文件,几个月下来就累积成了这个规模。

处理分两步。第一步先解除故障,直接按修改时间清理过期会话:find /var/lib/php/sessions -type f -mtime +1 -delete,跑完后 df -i 的 IUse% 立刻降到个位数,网站恢复正常。第二步做根治,把会话目录改为 tmpfs 挂载(内存文件系统不占磁盘 inode,重启自动清空),并增加一条系统定时任务每天兜底清理,不再依赖 PHP 的概率回收机制。改完后再观察一周,inode 使用率稳定在低位,问题彻底解决。

这个案例的启示是:磁盘空间监控和 inode 监控是两件必须同时做的事。很多站长只监控了 df -h,于是 inode 从百分之几慢慢爬升到百分之百的整个过程完全不可见,直到服务彻底写不进去才发现,故障往往是半夜突发的。把 df -i 加进巡检脚本,成本几乎为零,却能避免一次深夜救火。

小结

inode 耗尽是一个「症状和病因完全不在一个维度」的故障:现象是写不进去,看着像空间不足,实际却是文件数量到了上限。记住三个关键动作:用 df -i 而不是 df -h 确认症状,用 find -xdev -type f | wc -l 而不是 du 统计文件数,用 lsof | grep deleted 揪出幽灵 inode。根治则从会话目录 tmpfs 化、文件系统选型、inode 使用率监控三个方向下手。把这套方法存下来,下次遇到「空间没满却写不进」,你就能在十分钟内定位到真凶。

Last modification:September 22nd, 2026 at 09:24 pm

Leave a Comment