跳转到内容

磁盘还有空间却报 No space left on device?可能是 inode 用完了 ​

磁盘还剩 80% 的空闲,df -h 看着一切正常,可一写入文件就报 No space left on device。写个小文件不行,重启服务报错,连 touch 都失败——这是每个运维迟早会撞上的一次「空间谜案」。问题多半不在容量,而在 inode。

Linux 文件系统里有两套互相独立的上限:一套管数据块(block),一套管 inode。df -h 只看第一套,所以你看到还有空间;df -i 看的是第二套,它可能早就顶到了 100%。这篇文章先讲清两者的区别,再教你怎么定位是哪个目录吃光了 inode、怎么安全清掉、怎么让它不再复发。

df -h 和 df -i:两笔互相独立的账 ​

一个文件落到磁盘上,要占用两样东西:数据块存内容,inode 存「户口」。inode 里装着文件大小、权限、属主、时间戳、指向数据块的指针——但注意,文件内容本身不在这里。每创建一个文件、目录、符号链接,都要消耗一个 inode,哪怕那个文件是 0 字节。

问题就出在这:block 和 inode 是两笔独立的账,任何一笔花完都会报 No space left on device,报错还一模一样,只能靠现象区分:

  • 数据块用完 → 大文件写不进去,但还能建空文件;df -h 的 Use% 逼近 100%。
  • inode 用完 → 任何新文件(包括空文件和目录)都建不了,但已有文件还能继续追加写;df -h 可能才用了三成,df -i 的 IUse% 却是 100%。

所以第一件事永远是两条命令一起看。下面这台就是典型的 inode 打满,注意 df -h 和 df -i 的反差:

bash
$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        59G   18G   39G  32% /

$ df -i
Filesystem      Inodes IUsed   IFree IUse% Mounted on
/dev/sda1      3932160 3920000   12160  100% /

磁盘才用了三成,inode 却一个不剩。inode 总数是在格式化(mkfs)时定死的,ext4 默认按每 16KB 一个 inode 的比例分配,所以 59G 的盘约分到 390 万个 inode。事后不能随便加——想增加总数,只能备份数据、用 mkfs.ext4 -N <数量> 指定比例重新格式化。动手前先想好怎么恢复:本地加密过的数据换盘后要靠密钥才能重新解锁,见 本地加密与数据恢复说明。所以正确思路不是「加 inode」,而是「找出吃掉它们的文件并清掉」。

谁吃光了 inode:定位小文件大户 ​

绝大多数 inode 耗尽,罪魁都是海量小文件。几十万个几 KB 的文件,占的数据块没多少,inode 却一个不落——上面那台机器就是 18G 的实际数据,塞了 390 万个文件。典型来源有几个:

  • PHP 的 session 文件(/var/lib/php/sessions/ 下的 sess_ 开头),垃圾回收(gc)没配好,越积越多;
  • 邮件队列里堆积的退信(/var/spool/exim、/var/spool/postfix);
  • /tmp、/var/tmp 里的临时文件,systemd-tmpfiles 没清或脚本只写不删;
  • 应用缓存、node_modules、npm/yarn 缓存里成千上万的小文件;
  • 某个写坏的 cron 任务,每秒建一个文件还从不清理。

定位小文件用 inode 视角的 du 最直接(GNU coreutils 8.22+ 支持 --inodes),输出直接告诉你哪个目录 inode 占得多:

bash
$ du --inodes -x -s /var/* 2>/dev/null | sort -rn | head
1523310  /var/lib
 840021  /var/lib/php/sessions
 610204  /var/cache
  83110  /var/log

/var/lib/php/sessions 一个目录就占掉 84 万个 inode,元凶基本锁定。如果 coreutils 版本旧、没有 --inodes,就按目录数文件个数,完全不管文件大小:

bash
find /var -xdev -type f | cut -d/ -f3 | sort | uniq -c | sort -rn | head

看到某个目录文件数异常,再进去确认:

bash
find /var/lib/php/sessions -type f | wc -l

ls 一下会看到目录里塞满了 sess_ 开头的文件,很多是几天甚至几周前的——session 清理机制根本没生效。

安全清掉:别直接 rm 一大把 ​

清这些文件要谨慎,两个坑最常见。

第一,别用 rm /var/lib/php/sessions/* 这种通配符——几十万个文件会撑爆 shell 的参数列表(ARG_MAX),而且展开本身就很慢。用 find 的 -delete 或配合 xargs 分批处理:

bash
find /var/lib/php/sessions -type f -name 'sess_*' -mtime +1 -delete

-mtime +1 只删一天前的旧 session,正在用的不碰,相对安全。动手前先跑一遍不带 -delete 的版本,看看会删多少、范围对不对,再真正执行。

第二,删完 df -i 还是满的——多半是「删了却没释放」。Linux 里,只要还有进程握着某个文件(哪怕已经 unlink),它的 inode 和 block 就一直占着。用 lsof 看:

bash
$ lsof +L1 | grep -v COMMAND | head
php-fpm   1234 www-data  3w  REG  8,1  0  0  12345 /var/lib/php/sessions/sess_abc (deleted)

+L1 列出所有已被删除但仍被打开的文件,NLINK 一列是 0 就是这类。找到那个进程,重启它或对对应路径 > /path/to/file 截断,inode 立刻回来。删文件前先 lsof 扫一遍,能少走很多弯路。

根治:让 inode 不再默默打满 ​

清掉这一次是治标,根治要做三件事。

  1. 给 session 和临时文件的清理机制配好。PHP 的 gc 靠 session.gc_maxlifetime(多久算过期,默认 1440 秒)和 session.gc_probability/gc_divisor(多久触发一次,默认 1/100)配合工作;很多 inode 打满的服务器,根源就是 gc_probability 被设成了 0,gc 从不运行。Debian/Ubuntu 系还多一层保险:/etc/cron.d/php 会定时跑 sessionclean 清理过期 session,确认这个 cron 还在、没有因为 PHP 升级被停掉。systemd 的 systemd-tmpfiles 会定时清 /tmp 和 /var/tmp,确认对应单元在正常跑。

  2. 告警同时盯两套指标。监控里只加了磁盘使用率是不够的,df -i 的 inode 使用率同样要加阈值——80% 预警、90% 严重。很多半夜的故障,都是 inode 先悄悄逼近 100% 而无人知晓。

  3. 找出那个持续制造小文件的应用。如果 inode 增长曲线很陡,说明有程序在不停建小文件——多半是缓存策略写错了,或者临时文件只建不删。定位到具体目录后,回到业务代码或配置里解决,比隔三差五手删强。

判断 inode 还有多少余量、总数多大,可以看:

bash
df -i
tune2fs -l /dev/sda1 | grep -iE 'inode (count|size)'

tune2fs 能直接看到文件系统的 inode 总数(Inode count)和单个 inode 大小(Inode size,ext4 默认 256 字节)。这也解释了为什么不能无脑把 inode 调大——每个 inode 都要占 256 字节的元数据空间,数量翻倍,元数据占的空间也跟着翻倍。

下次再遇到 df -h 正常却写不进文件,先跑 df -i——这是区分两类资源的第一步,也是最容易被跳过的一步。block 满和 inode 满的解法完全不同:前者删大文件、扩盘或做轮转,后者清海量小文件、修好它们的产生机制。方向错了,删再多也没用。


远程排查时用 Termark 这类 SSH 客户端连上去,df -i、du --inodes、find 直接在终端里跑,把结果复制给同事也顺手。这类「资源被不知不觉打满」的问题,本质都是存储策略——数据该存哪、存多久、谁来清理,客户端本地也有同样的问题,见 数据存储路径。

参考资料 ​

Termark · SSH 客户端与终端工作台