本文要点
- 文件系统有两套互相独立的额度:数据块与 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 inodes958767,df -i报 IFree928548,相差 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
两处输出对比着看,文章的伏笔已经埋好了:
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
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 device117 个文件,加上保留的 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 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 -l86745
114998.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 tmpfsFilesystem 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/0tmpfs 也有 inode 上限(默认按内存规模算,可以在挂载时用 nr_inodes= 指定),把 tmpfs 的 inode 用完,一样报 ENOSPC——这一点在 Linux 内存文件系统 tmpfs 与 ramfs:容量上限、内存账本与常见坑 里详细验证过。
而 EFI 分区是 vfat,情况完全不同:
df -i /dev/vda15Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vda15 0 0 0 - /boot/efivfat 根本没有 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 个文件。
相关阅读
- Linux 磁盘空间排查与清理:从 df 到 journald 的完整救火流程
- Linux 内存文件系统 tmpfs 与 ramfs:容量上限、内存账本与常见坑
- Linux 文件系统:从 ext4 到 XFS 的选型与挂载优化
- Linux 稀疏文件:从 truncate 预分配到 du 与 ls 大小不一致
- Linux 环回设备:从 losetup 挂载镜像文件到分区镜像与扩容
- Linux 软链接与硬链接:从 inode 原理到 ln 命令
- Linux logrotate 日志轮转:从轮转策略到防止磁盘写满
本文的全部演示都在一台 1 核 1.9G 内存、无 swap 的 NAT 小机上完成,没有安装任何软件包:耗尽演示用的是 /tmp 下的一个 32MB 镜像文件加环回挂载,演示结束后镜像、挂载点与临时文件均已删除,真实分区与系统文件自始至终没有被改动。
评论 (0)
暂无评论,快来抢沙发吧!