我的服务器突然报磁盘空间不足、写不了任何文件,可我 df 一看磁盘明明还有一大半空闲,百思不得其解,排查半天才发现真正被耗尽的不是磁盘空间、而是一个我从来没关注过的东西——inode 的深度复盘
这是一次让我对"容量,从来不只有'大小'这一个维度"有了刻骨认知的事故。我有台服务器,某天服务突然集体报错:No space left on device(设备上没有剩余空间)——写日志失败、创建临时文件失败、连 touch 一个新文件都失败。这报错的字面意思再清楚不过:磁盘满了呗。我心想赶紧清理磁盘吧,可一敲 df -h,整个人都懵了:
磁盘空间明明还剩一大半,使用率才 40% 多,哪里"满"了?我反复确认,空间真的很充足。可系统就是铁了心地说"没有剩余空间",任何创建新文件的操作都失败。我一度怀疑是磁盘坏了、是文件系统损坏、是权限问题,折腾了好久毫无头绪。直到一个老同事瞟了一眼,淡淡提醒我:"你 df -i 看过没?"我一敲,真相大白——磁盘的"空间"(block)确实还很空,但"inode"已经 100% 用满了!原来一个目录下堆了几百万个几字节的小文件,把 inode 耗尽了;而每创建一个文件都要消耗一个 inode,inode 没了,哪怕还有大把空间,也一个新文件都创建不出来。
故障现场:空间(block)很空,inode 却 100% 满了
我把当时的诊断结果还原出来,对比一目了然:
# 报错: No space left on device, 我第一反应看空间:
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 100G 42G 58G 42% /
# ↑ 空间使用率才 42%, 还剩 58G! 看起来一点都不满
# 可文件就是创建不了:
$ touch /tmp/test
touch: cannot touch '/tmp/test': No space left on device # ?! 明明有空间
# 老同事一句话点醒, 看 inode:
$ df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/vda1 6553600 6553600 0 100% /
# ↑↑↑ inode 用满了! IUse% 100%, IFree 为 0
# → 没有空闲 inode → 无法创建任何新文件(无论空间多空)
# 揪出谁吃光了 inode —— 找文件数最多的目录:
$ find / -xdev -type d 2>/dev/null | while read d; do
echo "$(ls -1 "$d" 2>/dev/null | wc -l) $d"; done | sort -rn | head
4200000 /var/app/cache/sessions ← 凶手! 420 万个小文件
...
# 一个缓存目录里堆了几百万个几字节的会话小文件, 从不清理 → 吃光 inode
看着 df -h 说"还剩 58G"、df -i 说"inode 用满了"的鲜明对比,我才彻底明白:一块磁盘的"容量",根本不是"能存多少字节"这一个维度;它还有另一个我从来没在意过的维度——"能存多少个文件",由 inode(索引节点)的数量决定。每个文件(以及目录)都要占用一个 inode 来记录它的元信息(权限、大小、数据块位置等),而文件系统在格式化时,inode 的总数就基本定死了。我那个缓存目录堆了几百万个微小文件,字节数加起来没多少(所以空间很空),却把 inode 一个不剩地全用光了——于是"磁盘很空、却一个文件都创建不了"这个看似矛盾的现象就出现了。我一直盯着"空间够不够"这一个仪表盘,却不知道旁边还有一个"inode 够不够"的仪表盘,早已爆红。
第一件事:搞懂 inode——磁盘的"另一种容量",和空间各自独立耗尽
冷静下来,我去把"文件系统的 inode"这一课认真补了,才明白这个"有空间却写不了"的根源:
【inode 是什么, 为什么会和空间各自独立地耗尽】
文件系统里, 存一个文件需要两样东西:
1. 数据块(block): 存文件的【内容】——这决定"占多少空间(字节)"
2. inode(索引节点): 存文件的【元信息】(权限、属主、时间、数据块指针等)
——每个文件/目录都占用【一个 inode】
关键: 这是【两种独立的资源】, 各有总量, 各自独立耗尽:
- 空间(block)耗尽: 文件内容太多, 字节装满了 → df -h 显示 100%
- inode 耗尽: 文件【数量】太多, inode 用光了 → df -i 显示 100%
- 两者【任一耗尽】, 都会报 "No space left on device"!
为什么"空间很空却 inode 满了":
- inode 总数在【格式化文件系统时】就大致固定了(按一定比例预分配)
- 海量【小文件】: 每个只占几字节空间, 却各占一个完整 inode
- 于是: 空间(字节)绰绰有余, inode(文件个数)却被小文件耗尽
→ 经典场景: session/cache 小文件、海量日志碎片、邮件队列、临时文件不清理
诊断口诀:
报 "No space left" 时, df -h(看空间)和 df -i(看 inode)【都要看】!
只看 df -h 会被"还有空间"误导, 完全找不到真正满了的那个维度。
这一下点醒了我:我把"磁盘容量"狭隘地理解成了"能装多少字节"这一个数字,而忽略了它其实是两个独立维度——"装多少字节(空间/block)"和"装多少个文件(inode)"——的组合,这两者会各自独立地耗尽。报错信息 No space left on device 笼统地说"没空间了",其实涵盖了"这两个维度任一满了";而我只盯着 df -h 这一个维度,看它还有余量,就把"磁盘满"这个真相整个排除了,自然南辕北辙。真正满的那个维度(inode),恰恰是我视野里根本不存在的那个;我不是没看,而是压根不知道还有它可看。
第二件事:正解——清理小文件释放 inode,并从源头治理海量小文件
找到根因,正解就清晰了:应急是找到吃光 inode 的海量小文件目录、清理掉释放 inode;治本是从源头管理小文件的产生——给缓存/会话/临时文件设过期清理、用集中存储代替散落小文件、监控里同时盯空间和 inode。让"文件个数"这个维度也被纳入容量管理。
# 应急: 定位并清理吃光 inode 的海量小文件
# 1) 确认是 inode 满(而非空间)
df -i
# 2) 找出文件数最多的目录(逐层缩小范围)
for d in /var/* /tmp/* /home/*; do
echo "$(find "$d" -xdev | wc -l) $d"
done | sort -rn | head
# 3) 安全清理过期小文件(以 session/cache 为例: 删 7 天前的)
find /var/app/cache/sessions -type f -mtime +7 -delete
# 海量删除别直接 rm *(参数会超长), 用 find -delete 或分批
# 治本(从源头管理小文件的产生):
# - 给缓存/会话/临时目录配【定期清理】(cron / tmpwatch / systemd-tmpfiles)
# - 会话/缓存改用集中存储(Redis 等), 别散成磁盘上海量小文件
# - 合并小文件(打包归档)、控制日志切割碎片数量
# 监控: 空间和 inode 都要监控、都要告警!
df -h # 空间
df -i # inode ← 这个常被遗漏, 务必一起监控
这套做法的精髓,是把"文件数量(inode)"和"占用空间(字节)"当成同等重要的两种容量来对待:应急时先用 df -i 确认是哪个维度满了、再定位清理;治本时则要管住小文件的"产生"——能不落地成海量小文件就别落地(用 Redis 存 session),必须落地就配套定期清理,别让它们无限堆积。而最关键的一条:监控告警里,df -i 必须和 df -h 一起在,否则 inode 这个维度永远是盲区。治本不是"磁盘再扩大点"(扩空间救不了 inode),而是"把被忽略的那个维度也管起来"。
【关于 inode / 容量, 几条铁律】
1. 报 "No space left on device" → df -h 和 df -i 都看, 别只看空间
2. inode 耗尽多由"海量小文件"导致: session/cache/日志碎片/临时文件不清理
3. 应急: df -i 确认 → 找文件数最多的目录 → find -mtime -delete 清过期
4. 治本: 源头减少小文件(集中存储如 Redis)、配定期清理、合并归档
5. 监控必须覆盖【两个维度】: 空间使用率 + inode 使用率, 都要告警
6. 扩磁盘空间救不了 inode 耗尽; 调整 inode 数量需重新格式化, 故重在源头治理
第三件事:其他"盯着一个维度、真正爆的是另一个维度"的同类坑
顺着"容量/健康有多个维度、别只盯一个"这条线,我把同类的坑都梳理了一遍,它们都源于"用单一指标判断了一个多维度的状态":
第一个,内存看着够、却被 OOM。总内存有余,但某个 cgroup/容器的内存限额到了,或是大量内存被 cache 占着、可用的 available 不足——看 total 没问题,真正爆的是限额或 available 维度。
第二个,CPU 使用率不高、却很卡。CPU 利用率低,但负载(load)高、或是被 I/O wait 拖住、或被 CPU 限额节流——卡的不是"算力用满",而是别的维度。
第三个,带宽够、连接数/文件描述符却满了。网络带宽很闲,但连接数到了上限、或 ulimit 文件描述符耗尽——和 inode 如出一辙,数量维度先爆。
第四个,数据库空间够、连接池/锁却满了。表空间充足,可连接池被占满、或锁等待堆积——拖垮系统的是并发资源维度,不是存储维度。
第四件事:磁盘的两个容量维度,一张表对照
我把磁盘"空间"和"inode"这两个独立维度整理成一张表,这是我现在遇到磁盘类报错时的诊断依据:
| 维度 | 空间 / block | inode |
|---|---|---|
| 衡量的是 | 能存多少字节(内容大小) | 能存多少个文件(数量) |
| 怎么查 | df -h | df -i |
| 被什么耗尽 | 大文件、内容多 | 海量小文件、文件个数多 |
| 耗尽报错 | No space left on device | 同样是 No space left on device |
| 典型场景 | 大日志、大数据文件 | session/cache 小文件、日志碎片 |
| 怎么救 | 清大文件 / 扩容 | 清小文件 / 源头治理(扩容无用) |
这张表把真相摊开了:同一句 No space left on device,背后可能是空间满、也可能是 inode 满,这是两个完全独立的维度,诊断和解法都不同。只看 df -h 会在 inode 耗尽时被"还有空间"彻底带偏;只有同时看 df -i,才能分清到底是哪个维度满了、对症下药。
第五件事:我对"磁盘满了"的几个想当然
这次事故,本质是我把"磁盘容量"想当然地等同于了"空间大小"这一个维度。把这些想当然列出来,每一条都值得警惕:
| 我曾经的想当然 | 事故教我的真相 |
|---|---|
| "报没空间,就是磁盘字节满了" | 也可能是 inode(文件数)满了,空间还很空 |
| "df -h 显示有余量,就肯定不是磁盘问题" | df -h 只看空间;inode 满 df -h 照样显示有余量 |
| "磁盘容量就是能存多少 GB" | 还有一个维度:能存多少个文件(inode 数) |
| "小文件占不了多少地方,无所谓" | 小文件占空间少,却各吃一个 inode,海量则耗尽 |
| "加块盘/扩容就能解决磁盘满" | 扩空间救不了 inode 耗尽,得清小文件/源头治理 |
| "监控了磁盘空间就够了" | 必须同时监控 inode 使用率,否则它是盲区 |
第六件事:排查"资源耗尽"类问题时,我现在的自检习惯
现在每当我排查"资源不足/满了"类报错,或某个指标看着充足却出问题,我都会先按这张图问自己:
这张图的精髓,是"报资源不足而我看的指标却充足时,先怀疑这个资源有我没在看的另一个维度;磁盘就 df-h 和 df-i 都查"。排查就别只看一个仪表盘、把这个资源的所有维度都查一遍、治本就把被忽略的维度(inode、文件数)也纳入管理和监控。这套习惯,让我从"磁盘满=空间满"变成了"容量是多维的、每个维度都会独立耗尽"——核心始终是:磁盘的容量不是"能存多少字节"一个维度,而是"能存多少字节(空间/block)"和"能存多少个文件(inode)"两个独立维度,各有总量、各自独立耗尽,任一满了都报 No space left on device;海量小文件占空间极少却各吃一个 inode,会在空间充足时耗尽 inode、导致一个新文件都建不了;正解是 df -i 确认、清理海量小文件释放 inode、源头用集中存储/定期清理治理小文件,并把 inode 和空间一起纳入监控。
我立下的几条规矩
这场"磁盘很空却写不了文件"的事故,换来了我做运维时,刻进骨子里的几条铁律:
- 磁盘容量有两个独立维度:空间(字节,df -h)和 inode(文件数,df -i),各自独立耗尽。
- No space left on device 可能是空间满、也可能是 inode 满——两个都要查,别只看 df -h。
- 海量小文件占空间极少,却各吃一个 inode,会在空间充足时把 inode 耗尽。
- inode 耗尽扩磁盘空间没用;应急清理过期小文件,治本从源头减少小文件产生。
- session/cache/临时文件优先用集中存储(Redis 等),必须落地就配定期清理。
- 监控告警必须同时覆盖空间使用率和 inode 使用率,别让 inode 成为盲区。
- 排查一切"资源不足却指标充足"的问题,先怀疑这个资源有我没在看的另一个维度。
附:我现在用来"双维度盯磁盘 + 自动清理小文件"的脚本
这是我后来沉淀下来的一段巡检脚本,把这次踩坑的两条教训(空间和 inode 都要看、海量小文件要定期清)固化进了一个能挂 cron 的脚本里,让 inode 这个曾经的盲区再也不会无声无息地爆掉:
#!/bin/bash
# 双维度磁盘巡检 + 海量小文件自动清理(挂 cron 每小时跑)
THRESHOLD=85 # 使用率告警阈值(%)
SESSION_DIR=/var/app/cache/sessions
# 1) 同时检查【空间】和【inode】两个维度, 任一超阈值都告警
check() {
local mp=$1
local space=$(df --output=pcent "$mp" | tail -1 | tr -dc '0-9')
local inode=$(df --output=ipcent "$mp" | tail -1 | tr -dc '0-9')
echo "[$mp] 空间=${space}% inode=${inode}%"
if [ "$space" -ge "$THRESHOLD" ]; then
alert "$mp 空间使用率 ${space}% 超阈值"
fi
if [ "$inode" -ge "$THRESHOLD" ]; then # ← 关键: inode 也告警!
alert "$mp inode 使用率 ${inode}% 超阈值(海量小文件?)"
fi
}
# 2) 源头治理: 自动清理过期会话小文件, 别让它无限堆积吃 inode
clean_sessions() {
[ -d "$SESSION_DIR" ] || return
local before=$(find "$SESSION_DIR" -type f | wc -l)
find "$SESSION_DIR" -type f -mtime +7 -delete # 删 7 天前的
local after=$(find "$SESSION_DIR" -type f | wc -l)
echo "清理会话小文件: $before -> $after"
}
for mp in / /var /data; do check "$mp"; done
clean_sessions
这段脚本把我这次的教训钉死在了日常巡检里:每次检查都同时看空间(pcent)和 inode(ipcent)两个维度、任一超阈值都告警,inode 这个维度再也不会被遗漏;同时自动清理过期的会话小文件,从源头掐住 inode 被海量小文件耗尽的势头。把它挂上 cron 之后,我对磁盘的"感知"就从一个维度变成了两个维度——再不会出现"仪表盘显示一切正常、系统却已经写不了文件"那种被盲区支配的恐慌了。一次盲区带来的事故,最好的纪念,就是把这个盲区永久地点亮在自己的仪表盘上。
这件事过后,我特意把团队的监控面板翻了一遍,发现不止 inode,还有好几个维度都是盲区:文件描述符使用率、连接数、各容器的内存限额用量,平时压根没人盯。我们的监控一直只覆盖了那几个最直观的指标,而真正会先爆的,往往是这些藏在角落、没人画进面板的维度。我花了两天把这些维度逐一补进了告警,那种把一面只有几盏灯的仪表盘点亮成一整墙的踏实,让我对这次 inode 事故反而有点感激——它逼我看见了自己一直视而不见的那些角落。
我也常拿这件事提醒自己:排查问题最危险的时刻,不是毫无头绪,而是一个熟悉的指标信誓旦旦地告诉你这里没问题、于是你心安理得地把整个方向排除掉。盲区之所以是盲区,正因为我们连它的存在都不知道,自然也不会去看。保持一点对自己认知边界的谦卑,遇到说不通的事多问一句是不是还有我不知道的维度,往往比埋头深挖那几个已知指标更能把人从牛角尖里捞出来。
写在最后
回头看,这场由"inode 耗尽"引发的"磁盘很空却写不了文件"事故,真正教给我的,远不止"记得敲 df -i"这一个技巧。它让我对"当一个东西出了问题、而我盯着的那个指标却显示'一切正常'时, 真正的原因, 极可能藏在一个'我视野之外、压根不知道它存在'的维度里; 我之所以百思不得其解, 不是因为我看得不够仔细, 而是因为我从一开始就不知道'还有别的东西可看'",有了一次刻骨的体会。我栽跟头,是因为我把一个'多维度的事物', 在脑子里压缩成了'单一维度'——在我的认知里, "磁盘容量"就等于"还剩多少 GB"这一个数字; 于是当这个数字显示"还很充足"时, 我就武断地把"磁盘满"这整个方向排除了;我没意识到, "容量"其实是一个由多个独立维度(能装多少字节、能装多少个文件)构成的东西, 而真正爆掉的, 恰恰是那个我从不知道其存在、因而从未去看的维度;我对着一个"显示正常"的仪表盘苦苦排查, 而真正爆红的那个仪表盘, 根本不在我的仪表盘墙上。这让我领悟到一个关于"维度、认知盲区与诊断"的深刻认知:我们对一个事物的理解, 常常被简化成了'我们熟悉的那一两个维度'; 而当问题恰恰出在'我们认知之外的维度'时, 我们会陷入一种特殊的困境——不是'找不到答案', 而是'根本没在正确的地方找, 且不知道该去哪找';这种盲区最隐蔽的地方在于: 我们熟悉的那个维度会信誓旦旦地告诉我们"这里没问题", 从而帮我们把真正的方向给排除掉, 让我们越查越偏;所以面对"反常、说不通"的问题, 与其在已知维度里反复深挖, 不如先停下来问: 这个事物, 是不是还有我不知道的维度?那个'显示正常'的指标, 衡量的真的是问题所在的那个维度吗?。这给了我一种看待"一切'诊断一个看似矛盾的问题'之事"时的清醒:每当我遇到"指标都正常、却确实出了问题"的矛盾时,要追问"我熟悉的这些指标, 是不是只覆盖了这个事物的部分维度?会不会有一个我从没关注、甚至不知道存在的维度, 才是真正出问题的地方?"——主动去拓展自己对这个事物'有几个维度'的认知, 把没在看的维度补进视野, 而不是在熟悉的维度里钻牛角尖;"意识到事物的多维性、警惕认知之外的维度、别让熟悉指标的'正常'掩盖了陌生维度的'爆红'",是做对诊断、也是突破认知盲区的关键。认清磁盘容量有空间和 inode 两个独立维度、海量小文件会在空间充足时耗尽 inode、诊断要 df-h 和 df-i 都看——这,是我用一次磁盘很空却写不了文件的事故,换来的、关于 DevOps、也关于如何突破认知盲区的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次遇到"报没空间、df -h 却显示还很空"时,第一时间想起还有 df -i 这个维度可看,那我对着那个"磁盘很空却一个文件都建不了"的诡异报错折腾的大半天,就值了。
—— 别看了 · 2026