磁盘还有空间却报 No space left:一次 Linux inode 耗尽排查复盘

一个会产生海量小文件的服务突然写不进文件,日志反复报 No space left on device,可 df -h 一看根分区只用了 41%,空着将近 60G,连 touch 一个 0 字节空文件都失败。排查梳理:一个文件系统装文件靠两种独立资源,数据块存内容 df -h 看它,inode 存每个文件的元信息 df -i 看它;每创建一个文件不管多大都吃掉一个 inode,inode 总数在格式化时就定死后期基本改不了;海量小文件会让 inode 远早于数据块耗尽,df -h 正常而 df -i 100% 就是实锤;找 inode 大户是找文件个数最多的目录不是占空间最大的,删海量文件用 rm * 会参数列表过长要用 find -delete,删起来慢且吃 IO 要降优先级低峰期做;预防靠服务自动过期清理加 cron 兜底,监控必须把 inode 使用率也纳入告警,以及一套 inode 与磁盘排查纪律。

2024 年,一个服务突然开始写不进文件了。日志里反复报一句话:No space left on device——设备上没有空间了。这句话再直白不过,我心想:磁盘满了呗。我登上服务器,熟练地 df -h 一看,准备找找哪个目录把空间吃光了——结果根分区只用了 41%,空着将近 60G。我以为是看错了分区,又确认了一遍,没错,就是服务所在的那个分区,实打实空着 60 个 G。这下我彻底傻了:磁盘明明空着大半,系统却咬定"没有空间了",还死活不让我写文件。我随手 touch 了一个空文件试试,touch: No space left on device——一个连 0 字节都不到的空文件,都创建不了。一个空着 60G 的磁盘,凭什么连一个空文件都容不下?"空间"明明还有一大把,那到底是什么东西"没有了"?我盯着 df -h 那个 41% 想了很久,最后才反应过来,我把"磁盘满"这件事,理解得太窄了——一个文件系统能不能再装下一个文件,不只取决于"还有没有空间放数据",还取决于另一样东西,一样我从来没数过、却一直在被悄悄消耗的东西。这件事逼着我把 Linux 文件系统的 inode、inode 耗尽、海量小文件这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一个会产生大量小文件(缓存/会话碎片)的服务
事故现象:
- 服务报 No space left on device,写不进文件
- ★ df -h 看磁盘,空间只用了 41%,空着将近 60G
- touch 一个空文件都失败,同样报 No space left

现场排查:
# 1. 看磁盘空间 —— 空着一大半
$ df -h /
/dev/vda1   100G  39G  61G  41%  /          # ★ 才 41%,空间充足

# 2. 连空文件都建不了
$ touch /data/test
touch: 无法创建 '/data/test': 设备上没有空间   # ★ 0 字节都不行!

# 3. ★ 换个角度看:df -i 看的是 inode
$ df -i /
文件系统      Inode   已用Inode  可用   已用%  挂载点
/dev/vda1   6553600   6553600     0    100%   /
#                                       ^^^^ ★ inode 用满了!100%!

# 4. ★ 谁在狂产小文件 —— 找 inode 大户
$ for d in /data/*; do echo "$(find "$d" | wc -l) $d"; done | sort -rn | head
4821330 /data/cache                         # ★ 一个目录 480 万个文件!

# 5. 看那个目录
$ ls /data/cache | head
sess_0001a... sess_0001b... sess_0001c...    # ★ 海量会话碎片小文件

根因(后来想清楚的):
1. ★ 一个文件系统,装文件靠的是【两种】资源:
   - 数据块(block):存文件【内容】的地方 —— df -h 看它
   - ★ inode:存文件【元信息】的小结构 —— df -i 看它
2. ★ 每创建一个文件,都要【消耗一个 inode】——
   不管这个文件是 10G 还是 0 字节,都【吃掉一个】。
3. 文件系统在【格式化时】,inode 总数就被【定死了】,
   后期一般改不了。它是一个【有限的、会被耗尽】的资源。
4. 我这个服务疯狂产生几百万个【小文件】(会话碎片),
   ★ 数据块没怎么用(文件都很小),inode 却被一个个
   吃光了 —— 480 万文件 = 480 万 inode 没了。
5. ★ inode 一旦耗尽,文件系统就【再也建不了任何新文件】,
   哪怕只是个空文件 —— 因为没有 inode 给它了。
6. 报 No space left,但 space(数据块)其实还多得很,
   真正"left 没了"的,是 inode。
No space left 有两种:数据块满,或 inode 满,别只看 df -h。

修复 1:No space left 不一定是空间满——inode 也会满

# === ★ 先纠正最核心的误解:磁盘"满"有两种,完全不同 ===

# === 一个文件系统,靠两种资源装文件 ===
# 你往磁盘里存文件,其实在消耗【两种】各自独立的资源:
#  1. ★ 数据块(block):真正存文件【内容/数据】的空间。
#     —— 你说的"磁盘空间",通常指的是它。df -h 看它。
#  2. ★ inode(索引节点):每个文件都对应一个 inode,
#     里面存文件的【元信息】:大小、权限、属主、时间戳、
#     以及数据存在哪些块上。df -i 看它。

# === ★ 关键事实:这两种资源会【各自独立地】耗尽 ===
# - 数据块用光 -> 报 No space left(经典的"磁盘满")
# - ★ inode 用光 -> 【同样】报 No space left!
#   哪怕数据块还空着一大半。
# 系统报的是同一句话,但病根可能是两个完全不同的地方。

# === ★ 于是我这次的怪现象就解释通了 ===
$ df -h /                          # 看数据块
/dev/vda1   100G  39G  61G  41%    # ★ 数据块才用 41%
$ df -i /                          # ★ 看 inode
/dev/vda1   6.3M  6.3M   0   100%  # ★ inode 用满了!
# 数据块空着 61G,但 inode 一个不剩 ->
#   建新文件需要一个 inode,没有了 -> No space left。

# === ★ 排查"No space left",必须两个命令一起看 ===
$ df -h /          # ① 数据块够不够
$ df -i /          # ② inode 够不够
# 只看 df -h,就会像我一样:看到空间充足,彻底懵掉。
# ★ df -h 正常 + df -i 100% = inode 耗尽,实锤。

# === 为什么一个空文件也建不了 ===
# touch 建一个 0 字节的空文件,它【不占数据块】,
#   但它【照样需要一个 inode】来记录"我是个文件"。
# ★ inode 没了,连这个 0 字节的文件都登记不了 ——
#   这就是"空着 60G 却 touch 失败"的原因。

修复 2:什么是 inode——为什么小文件多了会耗尽

# === ★ 把 inode 这个概念彻底讲透 ===

# === inode 是什么 ===
# 每个文件,在文件系统里都对应一个 inode 结构,它记着:
#  - 文件类型、大小、权限、属主属组
#  - 创建/修改/访问时间
#  - ★ 文件的数据,具体存在磁盘的哪些块上
# 注意:inode 里【不存文件名】,也【不存文件内容】。
#   文件名在目录项里,内容在数据块里,inode 是中间的"档案"。

# === ★ 核心规则:一个文件 = 一个 inode ===
# 不管文件多大多小:
#  - 一个 0 字节的空文件,占 1 个 inode
#  - 一个 10G 的大文件,也只占 1 个 inode
# ★ inode 的消耗,只和【文件的【数量】】有关,
#   和文件的【大小】完全无关。

# === ★ inode 总数在格式化时就定死了 ===
# 文件系统(ext4/xfs)在【被格式化创建的那一刻】,
#   inode 的总数量就被【一次性算好、固定下来】了。
# 算法大致是:按"平均每个文件多大"估一个比例,
#   比如默认每 16KB 数据块配 1 个 inode。
# ★ 这个总数,文件系统建好后【基本无法再扩】(ext4
#   后期不能加;xfs 能动态加但也有上限)。

# === 看一个文件系统的 inode 总量和比例 ===
$ df -i /
文件系统      Inode    已用    可用   已用%
/dev/vda1   6553600  6553600    0    100%
# ★ 6553600 这个总数,就是格式化时定死的天花板。

$ tune2fs -l /dev/vda1 | grep -i inode    # ext4 看 inode 详情
Inode count:    6553600
Free inodes:    0
Inodes per group: ...

# === ★ 于是"小文件杀手"就出现了 ===
# 文件系统默认是按"文件平均不太小"来配 inode 的。
# 如果一个服务专门产生【海量的小文件】(几 KB 一个):
#  - 数据块:文件小,消耗很慢,df -h 长期很空
#  - ★ inode:文件多,一个文件吃一个,飞快见底
# ★ 结果就是 inode 远先于数据块耗尽 —— 正是我这次。
# 典型的"产小文件大户":会话文件、缓存碎片、
#   邮件队列、海量日志切片、爬虫抓的小页面。

修复 3:确认 inode 满 + 找出谁在产小文件

# === ★ 排查 inode 耗尽,核心是【找到文件最多的目录】 ===

# === 第一步:确认就是 inode 满 ===
$ df -i /
/dev/vda1  6553600  6553600  0  100%  /     # ★ 已用% 100% = 实锤

# === ★ 第二步:找出哪个目录里的文件数量最多 ===
# 注意:不是找"哪个目录大"(那是数据块),
#   是找"哪个目录【文件个数】多"(那才是 inode)。
$ for d in /*; do printf "%8s %s\n" \
    "$(find "$d" -xdev 2>/dev/null | wc -l)" "$d"; done | sort -rn
 4821330 /data                               # ★ /data 下 480 万个
    8200 /usr
    ...
# ★ -xdev:不跨文件系统,只数当前这个分区的。

# === 顺着往下钻,定位到具体目录 ===
$ for d in /data/*; do printf "%8s %s\n" \
    "$(find "$d" 2>/dev/null | wc -l)" "$d"; done | sort -rn | head
 4810500 /data/cache                         # ★ 元凶:/data/cache
# ★ 一层层 find | wc -l,就能把"文件最多"的目录揪出来。

# === 直接数某个目录下有多少文件 ===
$ find /data/cache -type f | wc -l
4810500                                      # ★ 481 万个文件

# === ★ 看清这些小文件是什么、谁在产 ===
$ ls -lt /data/cache | head                  # 按时间看,最新的长啥样
$ ls /data/cache | head
sess_a1b2c3...  sess_a1b2c4...                # ★ 一看就是会话碎片
$ ls -la /data/cache | head -3                # 看文件大小
-rw------- 1 app app  84 ...  sess_a1b2c3     # ★ 才 84 字节,典型小文件

# === 确认是哪个服务在写这个目录(在它狂产文件时抓)===
$ lsof +D /data/cache 2>/dev/null | awk '{print $1}' | sort -u
php-fpm                                       # ★ php-fpm 在产会话文件

# === 也看一眼:有没有某个进程在猛建文件 ===
$ ls /data/cache | wc -l ; sleep 10 ; ls /data/cache | wc -l
# ★ 两次数量差很多 = 确实有进程在持续产小文件。

修复 4:清理 inode + 删海量小文件的坑

# === ★ 找到海量小文件后,清理时有几个坑要避开 ===

# === ★ 坑 1:rm /data/cache/* 会直接报错 ===
$ rm /data/cache/*
-bash: /bin/rm: 参数列表过长                  # ★ Argument list too long
# 原因:* 会被 shell 展开成 480 万个文件名,
#   一条命令塞不下这么多参数 —— 命令行长度有上限。

# === ★ 正解:用 find 配合 -delete 或 xargs ===
$ find /data/cache -type f -delete            # find 自己逐个删,不展开
# 或者:
$ find /data/cache -type f -print0 | xargs -0 rm -f
# ★ xargs 会把文件名【分批】喂给 rm,每批数量适中,
#   不会撞到"参数列表过长"。-print0/-0 是为了正确处理
#   文件名里的空格特殊字符。

# === ★ 坑 2:删海量小文件本身很慢、IO 压力大 ===
# 删 480 万个文件,要改 480 万次目录项、回收 480 万个
#   inode —— 这是【海量小 IO】,会很慢、很吃磁盘。
# ★ 在线上高峰期硬删,可能把磁盘 IO 打满、拖累业务。
# 建议:低峰期做;或给删除命令降低 IO 优先级:
$ ionice -c3 nice -n19 find /data/cache -type f -delete
# ionice -c3 = IO 空闲时才删,nice -n19 = CPU 也让着。

# === 只删旧文件,保留近期的(更安全)===
$ find /data/cache -type f -mtime +7 -delete  # 只删 7 天前的
# ★ -mtime +7:修改时间在 7 天以前。按时间删,
#   不会误删服务正在用的新会话文件。

# === 边删边看 inode 恢复情况 ===
$ watch -n5 'df -i /'
# ★ 看 inode 已用% 一点点往下掉,就知道清理在生效。

# === ★ 如果整个目录都是垃圾、想最快清空 ===
# 一个技巧:直接删父目录再重建,比逐个删快得多 ——
$ ls -d /data/cache_old 2>/dev/null   # 确认没冲突
$ mv /data/cache /data/cache_old      # 先改名,让服务用新目录
$ mkdir /data/cache && chown app:app /data/cache
$ rm -rf /data/cache_old              # 后台慢慢删旧的
# ★ 先 mv 再重建,服务立刻能用新目录,删除从容做。

修复 5:怎么从根上预防 inode 耗尽

# === ★ 清理只是止血,得让小文件别再无限堆积 ===

# === 方向 1:给产小文件的服务,配上自动清理 ===
# 会话/缓存这类碎片文件,本就该【定期过期清理】。
# 比如 PHP 的 session,本身有 gc(垃圾回收)机制,
#   要确认它真的在跑:
$ grep -E 'gc_maxlifetime|gc_probability' /etc/php.ini
session.gc_maxlifetime = 1440         # 会话多久算过期
session.gc_probability = 1            # ★ 不能是 0,否则永不回收
# ★ 我这次一个隐患就是 gc 没正常触发,文件只产不清。

# === 方向 2:用 cron / tmpfiles 定时清理 ===
$ crontab -e
0 4 * * * find /data/cache -type f -mtime +1 -delete
# ★ 每天凌晨清掉一天前的碎片,从源头控制文件总数。
# systemd 系统也可以用 systemd-tmpfiles 配置自动清理。

# === ★ 方向 3:监控,要同时监控 inode ===
# 很多监控只盯 df -h(数据块),【漏了 inode】。
$ df -i / | awk 'NR==2{print $5}'     # 取 inode 使用率
# ★ 把 inode 使用率也纳入告警 —— 它满起来悄无声息,
#   df -h 一切正常,等报 No space left 就晚了。

# === 方向 4:新建文件系统时,为小文件场景多配 inode ===
# 如果一个分区注定要存海量小文件,格式化时就该
#   提高 inode 密度(每隔多少字节配一个 inode):
$ mkfs.ext4 -i 4096 /dev/vdb1
# ★ -i 4096:每 4KB 配一个 inode(默认通常 16KB)——
#   inode 总数翻几倍,扛得住更多小文件。
# 注意:这只能在【格式化时】定,现有文件系统改不了。

# === 看现有文件系统的 inode 配置比例 ===
$ tune2fs -l /dev/vda1 | grep -E 'Inode count|Block count'
# ★ Inode count / Block count 的比例,就是 inode 密度。

# === ★ 治理思路 ===
# 短期:清掉积压的小文件(修复 4)。
# 长期:让服务自动过期清理 + cron 兜底 + 监控 inode。
#   实在是海量小文件的专用盘,格式化时把 inode 配足。

修复 6:inode 与磁盘排查纪律

# === 这次事故暴露的认知盲区,定几条纪律 ===

# === 1. ★ No space left 有两种:数据块满,或 inode 满 ===
$ df -h /        # 看数据块
$ df -i /        # ★ 看 inode —— 这条以前总被漏掉

# === 2. ★ df -h 空间充足但写不了文件 -> 立刻 df -i ===
# inode 100% = inode 耗尽,和剩多少 G 空间无关。

# === 3. 一个文件吃一个 inode,和文件大小无关 ===
# inode 耗尽的根源,永远是【文件【数量】太多】。

# === 4. ★ 找 inode 大户 = 找文件【个数】最多的目录 ===
$ for d in /*; do echo "$(find $d -xdev|wc -l) $d"; done | sort -rn

# === 5. ★ 删海量文件别用 rm *,会"参数列表过长" ===
$ find 目录 -type f -delete          # 用 find -delete 或 xargs

# === 6. 删海量小文件慢且吃 IO,低峰期做、降 IO 优先级 ===
$ ionice -c3 nice -n19 find 目录 -type f -mtime +7 -delete

# === 7. ★ 监控必须把 inode 使用率也纳入告警 ===
# 只盯 df -h 会漏掉 inode 耗尽。

# === 8. 排查 No space left 的命令链 ===
$ df -h /                            # ① 数据块满没
$ df -i /                            # ② ★ inode 满没
$ for d in /*; do ...find|wc -l...   # ③ 找文件数最多的目录
$ find 大户目录 -type f -mtime +N -delete  # ④ 按时间清理
$ watch 'df -i /'                    # ⑤ 看 inode 恢复
# 按这个顺序,No space left 基本能定位。

命令速查

需求                        命令
=============================================================
看数据块(磁盘空间)        df -h /
★ 看 inode 使用情况         df -i /
找文件个数最多的目录        for d in /*; do echo "$(find $d -xdev|wc -l) $d"; done | sort -rn
数某目录下文件总数          find 目录 -type f | wc -l
看文件系统 inode 总量       tune2fs -l /dev/xxx | grep -i inode
删海量文件(避免参数过长)  find 目录 -type f -delete
只删 N 天前的旧文件         find 目录 -type f -mtime +N -delete
低优先级删文件不抢 IO       ionice -c3 nice -n19 find 目录 -delete
盯 inode 恢复               watch -n5 'df -i /'
格式化时多配 inode          mkfs.ext4 -i 4096 /dev/xxx

口诀:No space left 先 df -h 再 df -i,空间充足却写不了八成是 inode 满
      一个文件吃一个 inode 和大小无关,删海量文件用 find -delete 别用 rm *

避坑清单

  1. No space left on device 有两种成因:数据块满,或 inode 满,不只是空间满
  2. 排查时 df -h 和 df -i 要一起看,df -h 充足但写不了文件就立刻 df -i
  3. 每个文件占一个 inode,和文件大小无关,空文件和 10G 文件都吃一个 inode
  4. inode 总数在文件系统格式化时就定死,ext4 后期基本无法再扩容
  5. 海量小文件会让 inode 远早于数据块耗尽,会话缓存碎片是典型的小文件大户
  6. inode 耗尽就找文件个数最多的目录,不是找占空间最大的目录,两者不同
  7. 删海量文件用 rm 目录/* 会报参数列表过长,要用 find -delete 或 xargs
  8. 删几百万个小文件很慢且吃磁盘 IO,应低峰期做并用 ionice nice 降优先级
  9. 监控只盯 df -h 会漏掉 inode 耗尽,必须把 inode 使用率也纳入告警
  10. 预防靠服务自动过期清理加 cron 兜底,海量小文件专用盘格式化时多配 inode

总结

这次"磁盘空着 60G,系统却报 No space left"的事故,纠正了我一个朴素到我从未怀疑过的等式:磁盘满 = 没空间了。在我的脑子里,一个文件系统能不能再装下一个新文件,是一道再简单不过的算术题——就看它还剩多少空间。df -h 告诉我还剩 61G,那这道算术题的答案就该是"能装,而且能装很多"。正因为这个等式在我心里太天经地义,当 df -h 打出那个悠闲的"41%",而系统却斩钉截铁地拒绝我写入哪怕一个 0 字节的空文件时,我整个人是懵的。我反复确认分区有没有看错,反复 touch 想看它是不是偶发——我所有的挣扎,都源于一个我没意识到的、被我当成公理的前提:磁盘的"满",只有一种,就是空间被填满。复盘到根上,我才真正理解,一个文件系统要"收留"一个文件,它需要付出的,从来不是一种资源,而是两种。一种是数据块,那是用来安放文件"血肉"——也就是内容——的地方;df -h 看的,自始至终只是它。而另一种,叫 inode,它像是文件系统为每一个文件单独建立的一份"户口档案"——这个文件叫什么类型、归谁所有、什么时候建的、它的血肉散落在磁盘的哪些角落,全记在这份档案里。这里有一条我以前从不知道的、却致命的规则:每收留一个文件,就要为它建一份户口,就要消耗一个 inode——无论这个文件是 10G 的庞然大物,还是一个连一个字节都没有的空壳。inode 的消耗,只跟文件的"个数"挂钩,跟文件的"胖瘦"毫无关系。而更关键的是,一个文件系统里 inode 的总数,在它被格式化出来的那一刻,就被一次性地、永久地定死了——它是一份有限的、会被用尽的配额。我那个服务,疯狂地吐出几百万个几十字节的会话碎片,这些小文件轻飘飘的,几乎没怎么碰那 61G 的数据块,却把那份名叫 inode 的户口配额,一份一份,悄无声息地领光了。当系统对我喊出 No space left 时,它没有撒谎——只是那个"left 没有了"的东西,从来不是我以为的空间,而是户口本上最后一个空白页。这次最大的收获,是我意识到,很多让我百思不得其解的故障,根源不在于问题本身有多刁钻,而在于我脑子里那个用来理解世界的模型,太粗糙了。我用"空间"这一个维度,去理解"磁盘能不能装文件"这件本该是二维的事,于是当另一个维度——inode——率先触底时,我那个一维的模型,根本没有能力解释眼前的现象,只能让我对着一个矛盾干瞪眼。一个系统报给你的错误信息,往往是它能力范围内最简短的概括,它不会、也没法替你说清背后真正是哪一种资源见了底。所以下一次,当一个熟悉的报错配上一个反常的现象时,我会提醒自己:也许不是系统疯了,而是我用来理解它的那张地图上,缺了一整块大陆。先去把地图补全,再回头看那个矛盾——它往往就不再是矛盾了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

删了大文件磁盘空间却没回来:一次 Linux 已删除文件占用排查复盘

2026-5-20 20:39:05

Linux教程

服务进程凭空消失日志却干净:一次 Linux OOM Killer 排查复盘

2026-5-20 20:48:16

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索