暗色模式

Linux inode 耗尽:磁盘没满却写不进文件的排查

技术教程
2026-10-04
7
0
本文要点
  • 文件系统有两套互相独立的额度:数据块与 inode。任何一套用尽,应用收到的都是 No space left on device(ENOSPC)——所以 df -h 显示还有空间,不代表还能建文件。
  • inode 的总数在 mkfs 格式化那一刻就定死了,之后无法在线增加。本文用 32MB 镜像加 mkfs.ext4 -N 128,在 /tmp 里把「128 个 inode 被 117 个 0 字节文件吃光」的全过程跑了一遍。
  • 耗尽时的对照非常直观:df -h 报已用 1%、可用 26M,df -i 报 100%,紧接着 touch 就报 No space left on device。
  • 判断还能不能再建文件,只认 df -i:本机 tune2fs -l 报 Free inodes 958767,df -i 报 IFree 928548,相差 30219,sync 之后依旧对不上。
  • 一个 inode 占 256 字节,本机 8G 分区预分配了 1032192 个(inode 表本身约 252MB,占分区 3%);吃 inode 的永远是小文件——本机 /usr/src/linux-headers-*/include/config 一个目录就有 8 千多个文件。

磁盘没满,却写不进文件

服务器上最让人困惑的报错之一是这个:

touch: cannot touch '/data/hello.txt': No space left on device

第一反应是 df -h,结果发现根分区才用了 40%,可用空间好几个 G。空间明明有,为什么内核说「没有空间」?

因为 Linux 的文件系统有两套各自独立的额度:一套是数据块(block),一套是索引节点(inode)。写一个文件要同时消耗两者——数据块存内容,inode 存「这个文件是谁的、多大、权限是什么、数据块放在哪」这些元数据。任何一套用尽,报错都是同一个 ENOSPC,也就是那句 No space left on device。

df -h 只看数据块,要看 inode 得用 df -i。这篇文章不满足于「记住 df -i 就行」——我们在演示机上真的造了一个 inode 用尽的文件系统,把耗尽前后的每一屏都摆出来,再回到真实分区上算清楚 inode 的账。

造一个只有 128 个 inode 的文件系统

正常分区有几百万个 inode,靠写文件去耗光它不现实。好在 mkfs.ext4 允许指定 inode 总数,我们可以拿一个 32MB 的镜像文件造一个「只有 128 个 inode」的迷你文件系统,整个过程只在 /tmp 里动手,不碰真实分区。

dd if=/dev/zero of=/tmp/inode-demo.img bs=1M count=32 status=none
mkfs.ext4 -q -N 128 -F /tmp/inode-demo.img
mkdir -p /tmp/inode-demo && mount -o loop /tmp/inode-demo.img /tmp/inode-demo
df -h /tmp/inode-demo; df -i /tmp/inode-demo

32MB 镜像用 mkfs.ext4 -N 128 格式化后挂载到 /tmp/inode-demo:df -h 显示 28M 容量、已用 24K、可用 26M、使用率 1%;df -i 显示 Inodes 128、IUsed 11、IFree 117、IUse% 9%

两处输出对比着看,文章的伏笔已经埋好了:

  • df -h:容量 28M,已用 24K,可用 26M,使用率 1%。
  • df -i:inode 总数 128,已用 11,可用 117,使用率 9%。

一个刚格式化、里面一个文件都没有的文件系统,为什么已经用掉 11 个 inode?

这是 ext4 的保留 inode:1 到 10 号被文件系统自己占着(坏块记录、根目录、日志等特殊用途),11 号给了 lost+found 目录。用 ls -id 看 inode 号能直接证实:

ls -id / /lost+found
 2 /
11 /lost+found

根目录是 2 号 inode,lost+found 正好是 11 号。一个小知识点:inode 编号从 1 开始,2 号永远是根目录——1 号 inode 是坏块记录文件,故意留空。所以这个 128 个 inode 的文件系统,真正能给人用的只有 117 个。

把 117 个 inode 用光

现在往里面写文件,一直写到写不动为止。循环里给 touch 加了 2>/dev/null,是为了让失败的那一次安静退出,不打断循环计数:

i=0; while touch /tmp/inode-demo/f$i 2>/dev/null; do i=$((i+1)); done; echo "created $i files"
df -h /tmp/inode-demo; df -i /tmp/inode-demo
touch /tmp/inode-demo/hello.txt

同一个文件系统写满 inode 之后:循环报告 created 117 files,df -h 仍然显示只用了 24K、使用率 1%,df -i 已经变成 128/128/0、100%,最后一次 touch 报 No space left on device

created 117 files
Filesystem      Size  Used Avail Use% Mounted on
/dev/loop7       28M   24K   26M   1% /tmp/inode-demo
Filesystem     Inodes IUsed IFree IUse% Mounted on
/dev/loop7        128   128     0  100% /tmp/inode-demo
touch: cannot touch '/tmp/inode-demo/hello.txt': No space left on device

117 个文件,加上保留的 11 个,正好 128,一个不多一个不少——df -i 的 IUse% 到 100% 的那一刻,文件系统就彻底拒绝新文件,无论还剩多少空间。

再看一眼 df -h:写了 117 个文件之后,已用依然是 24K,使用率依然是 1%。这 24K 还几乎全是目录自己的数据块(117 个文件名挤在同一个目录里,目录也要占块),117 个 0 字节文件的内容一个字节都没占。

这就是 inode 耗尽最迷惑人的地方:所有容量监控都显示正常,只有写入在失败。如果你的应用是「收到请求就写一个小文件」(session、缓存、队列),那么故障会表现为「服务随机报错」,而磁盘容量报表一路绿灯。

一个 inode 占多大:本机的 inode 账本

回到真实分区。tune2fs -l 能读出 ext4 超级块里记着的 inode 参数,df -i 给出运行时的使用量,/proc/slabinfo 则告诉我们内核为了这些 inode 花了多少内存:

tune2fs -l /dev/vda1 | grep -iE 'inode count|free inodes|inode size|per group'
df -i / | tail -1
grep ext4_inode_cache /proc/slabinfo

演示机真实根分区的 inode 账本:tune2fs 报 Inode count 1032192、Free inodes 958767、Inodes per group 16128、Inode blocks per group 1008、Inode size 256;df -i 报 1032192 总数、103644 已用、928548 可用、11%;slabinfo 里 ext4_inode_cache 有 102478 个活跃对象

Inode count:              1032192
Free inodes:              958767
Blocks per group:         32768
Fragments per group:      32768
Inodes per group:         16128
Inode blocks per group:    1008
Inode size:               256
/dev/vda1      1032192 103644 928548   11% /
ext4_inode_cache  102478 103788   1176   27    8 : tunables    0    0    0 : slabdata   3844   3844      0

(grep 里写了 per group,所以 Blocks per group / Fragments per group 也一起被捞了出来,顺手可以当参照。)

这屏数字里有三条信息值得展开:

① 每个 inode 在磁盘上占 256 字节,而 inode 表是一次性预留的。 1032192 个 inode × 256 字节 ≈ 252MB,这块空间从这个分区格式化那天起就被划走了,不随文件多少变化。换算一下更直观:每个块组有 16128 个 inode,占 1008 个块(16128 × 256 = 1008 × 4096 = 4128768 字节,两边严丝合缝),1032192 ÷ 16128 = 64 个块组,整张 inode 表就是 64 × 4128768 ≈ 252MB。对一个 8G 分区来说,这是雷打不动的 3%。

② 内存里每个在用的 inode 要花约 1.1KB,比磁盘上的 256 字节贵 4 倍多。 /proc/slabinfo 里 ext4_inode_cache 一行:第二个数是对象总数 103788,第三个是单个对象 1176 字节。内核里的 inode 对象除了磁盘上那份 256 字节的元数据,还要挂 VFS 层的引用计数、页缓存指针、锁、扩展属性等一堆结构。这一项在这台 1.9G 内存的机器上就占了约 116MB(103788 × 1176B),注意它的活跃对象数 102478 和 df -i 的已用 103644 基本对得上——内存里缓存了多少 inode,和磁盘上用了多少 inode 是同一个数量级的事。

③ 别拿 tune2fs 的 Free inodes 判断还能不能建文件。 同一时刻,超级块说空闲 958767 个,df -i 说空闲 928548 个,差了 30219 个。我一开始怀疑是「刚删的文件还没写回超级块」,于是 sync 之后再测——两个数依旧不变,差距原样保留。

这说明两者是两套不同口径的计数,不是同一份数据的缓存与落盘关系。至于差异的确切来源,我没有在源码层面逐项查证,也不打算在这篇文章里猜。对运维来说真正要记住的只有一条:判断「还能不能建文件」,认 df -i。 应用建文件失败时拿到的是 ENOSPC,它和 df -i 看到的是同一套运行时账本;tune2fs 那份适合看「这个文件系统一共规划了多少 inode」,不适合看「现在还剩多少」。

inode 的数量,格式化那一刻就定死了

理解了上面这屏数字,就能理解 inode 最要命的一个特性:它不能在线增加。

ext4 在格式化时会根据 bytes-per-inode 这个比例算出 inode 总数,默认值是 16384——也就是「预计每 16KB 数据配 1 个 inode」。所以:

  • 同样是 1T 的分区,按默认参数格式化大概得到 6 千多万个 inode(1T ÷ 16KB),分给「存小文件」的场景完全不够;
  • 而如果这个分区是用来存视频、备份、数据库大文件的,几千万个 inode 里可能只用掉几千个,剩下的 inode 表空间(每个 256 字节)纯属浪费;
  • 想要更多 inode,mkfs.ext4 -i 4096 可以把比例压到 4KB 一个,或者用 -N 直接指定总数——但这两个参数只在格式化时有效;
  • 格式化之后再想增加,常规手段做不到:分区扩容(resize2fs 变大)时新加的块组会按同样比例自带一批新 inode,但那是动分区表和文件系统大小的操作,不是「调整 inode 数量」。

换句话说,inode 耗尽是「规划问题」,不是「运行问题」——出问题的时候已经晚了,重建文件系统才能改。这也是为什么它值得单独监控:磁盘满了可以删文件、可以扩容,inode 满了只能清小文件,或者把数据搬到新建的文件系统上。

谁在吃 inode:本机实测分布

inode 耗尽几乎总是小文件造成的——一个 10G 的日志文件占 1 个 inode,一万个 10KB 的碎片文件占一万个。在真实机器上找大户,一条 find 就够了:把每个文件所属的目录名打印出来计数,再排序:

find / -xdev -type f -printf '%h\n' 2>/dev/null | sort | uniq -c | sort -rn | head -6

这台演示机上的结果是:

   8823 /usr/src/linux-headers-5.15.0-194-generic/include/config
   8776 /usr/src/linux-headers-5.15.0-30-generic/include/config
   2650 /var/lib/dpkg/info
   1333 /usr/src/linux-headers-5.15.0-194/include/linux
   1328 /usr/src/linux-headers-5.15.0-30/include/linux
    854 /usr/share/man/man1

第一名是内核头文件里的 include/config 目录——内核的每个配置项就是一个文件,一个目录 8823 个。两个版本的内核头文件加起来贡献了 1.7 万多个 inode,而这堆文件加起来才几十 MB。这就是「小文件吃 inode」的典型形态:占空间不多,占 inode 极狠。

-xdev 的作用是不跨挂载点——否则 /proc、/sys 这些虚拟文件系统里成千上万的条目会混进来,它们并不真正占用 ext4 的 inode。

顺手把总数也数出来,inode 的账就能对上:

find / -xdev -type f | wc -l; find / -xdev -type d | wc -l
86745
11499

8.6 万个普通文件、1.1 万个目录,加上符号链接、设备节点、Unix 套接字这些特殊文件,合计就是 df -i 里那个「已用 103644」。注意目录本身也占 inode——目录在 ext4 里是一种特殊文件,你在一个目录里建一万个文件,除了消耗一万个 inode,目录自己还要不断扩容占数据块(前面 117 个文件用掉 24K 就是这么来的)。

现实里最容易踩的几处:

  • 会话/缓存目录:PHP 的 session、上传分片、各类 cache/ 目录,一个请求一个文件;
  • 邮件队列:/var/spool 下的邮件是一封一个文件,队列积压就是 inode 雪崩;
  • 日志没轮转干净:logrotate 每天切出来的小文件长期不清,一年就是几百个文件乘以应用数;
  • 容器与镜像层:一个镜像层里可能塞进几万个小文件,解压时消耗的是宿主机文件系统的 inode。

tmpfs 与 vfat:有 inode 的,和压根没有 inode 的

同样是 df -i,在不同文件系统上给出的东西完全不一样。这台机器上的 tmpfs:

df -i -t tmpfs
Filesystem     Inodes IUsed  IFree IUse% Mounted on
tmpfs          253139   685 252454    1% /run
tmpfs          253139     1 253138    1% /dev/shm
tmpfs          253139     3 253136    1% /run/lock
tmpfs           50627    26  50601    1% /run/user/0

tmpfs 也有 inode 上限(默认按内存规模算,可以在挂载时用 nr_inodes= 指定),把 tmpfs 的 inode 用完,一样报 ENOSPC——这一点在 Linux 内存文件系统 tmpfs 与 ramfs:容量上限、内存账本与常见坑 里详细验证过。

而 EFI 分区是 vfat,情况完全不同:

df -i /dev/vda15
Filesystem     Inodes IUsed IFree IUse% Mounted on
/dev/vda15          0     0     0     - /boot/efi

vfat 根本没有 inode 这个概念(它的目录项直接记文件名和起始簇号),所以 df -i 只能显示 0 和 -。在 vfat 上你永远不会遇到 inode 耗尽,只会遇到「簇用完」。反过来说,用 df -i 输出里的 - 判断「这个文件系统不适用 inode 口径」,比背文件系统类型表可靠。

三步定位 inode 耗尽

把上面的操作整理成排查顺序:

第一步,确认是不是 inode 的问题。 故障现象是写入报 No space left on device,但 df -h 有空间。这时跑 df -i,看哪个挂载点的 IUse% 接近或等于 100%:

df -i

第二步,在出问题的挂载点上找大户。 用前面那条 find 命令(记得带 -xdev),按目录统计文件数排序;如果知道嫌疑目录,也可以直接数:

find /var/spool -xdev -type f | wc -l

第三步,如果现象是「内存里 inode 吃得多」(比如 slab 里 ext4_inode_cache 异常大),可以看内核的 inode 账本:

cat /proc/sys/fs/inode-nr; cat /proc/sys/fs/inode-state

本机的输出:

165769    11102
165769    11102    0    0    0    0    0

两个数字分别是当前在内存里的 inode 对象总数和其中空闲可复用的数量——注意这是 VFS 层的对象池,和磁盘上「用了多少个 inode」不是一回事,文件删除后对象会留着复用,所以 165769 比 df -i 的 103644 大很正常。inode-state 的前两个数与 inode-nr 相同,第三个数是收缩请求计数(本机实测为 0),后面 4 个是占位字段。

处置与预防

已经满了,怎么腾 inode:

  • 合并小文件:几万个小文件先 tar 打包成一个归档再删原件,inode 立刻释放几万个——注意打包本身要占空间,先确认容量够;
  • 清过期数据:session 目录、/var/spool 队列、缓存目录里的陈旧文件,先确认没有业务在读再删;
  • 日志加 maxage:logrotate 只写 rotate N 的话,轮转出来的旧文件会一直堆着,配合 maxage 才能真正过期删除,细节见 Linux logrotate 日志轮转:从轮转策略到防止磁盘写满;
  • 删之前先看清是不是硬链接:硬链接不额外占 inode,删掉一个链接名不等于释放 inode,最后一个链接名消失才释放,参考 Linux 软链接与硬链接:从 inode 原理到 ln 命令。

还没满,怎么预防:

  • 把 df -i 的 IUse% 纳入监控,尤其是「小文件型」的挂载点(session、邮件队列、容器数据盘);
  • 按用途规划 inode:格式化海量小文件的分区时用 -i 4096 或 -N 显式指定,别用默认的 16384 比例;纯大文件盘则相反,省下的 inode 表空间就是省下的容量;
  • tmpfs 挂载显式限 nr_inodes=,避免某个程序在 /dev/shm 里堆小文件堆到内存告急;
  • 文件的「空间账」和「inode 账」分开看——df -h 和 df -i 是一对,缺一个都不完整,这也是 Linux 磁盘空间排查与清理 里那把「救火」流程的前置检查。

小结

  • inode 存元数据,和数据块是两套额度,任何一套用尽都报 No space left on device;df -h 有空间 ≠ 还能建文件。
  • 本文实测:32MB 镜像 + mkfs.ext4 -N 128,一个刚格式化就占掉 11 个 inode(1–10 号保留 + 11 号 lost+found),117 个 0 字节文件把它精确填满,此时 df -h 仍显示已用 24K / 1%,df -i 已是 100%。
  • 一个 inode 磁盘上占 256 字节,内存里约 1176 字节;本机 1032192 个 inode 的表空间约 252MB,占分区 3%。
  • inode 数量在 mkfs 时定死,不能在线增加,-i / -N 只在格式化时有效。
  • tune2fs -l 的 Free inodes 与 df -i 的 IFree 实测相差 30219 且 sync 不收敛(本机 958767 vs 928548):判断能不能建文件,认 df -i。
  • 吃 inode 的是小文件与目录:本机 86745 个文件 + 11499 个目录,最大的单个目录(内核头文件的 include/config)就有 8823 个文件。

相关阅读

本文的全部演示都在一台 1 核 1.9G 内存、无 swap 的 NAT 小机上完成,没有安装任何软件包:耗尽演示用的是 /tmp 下的一个 32MB 镜像文件加环回挂载,演示结束后镜像、挂载点与临时文件均已删除,真实分区与系统文件自始至终没有被改动。

发表评论

暂无评论,快来抢沙发吧!