我的服务器突然报磁盘空间不足、写不了任何文件,可我 df 一看磁盘明明还有一大半空闲,百思不得其解,排查半天才发现真正被耗尽的不是磁盘空间、而是一个我从来没关注过的东西——inode 的深度复盘

我有台服务器某天服务集体报错 No space left on device——写日志失败、创建临时文件失败、连 touch 一个新文件都失败,字面意思再清楚不过:磁盘满了。我赶紧 df -h 想清理,却懵了:磁盘空间明明还剩一大半、使用率才 40% 多,哪里满了?可系统就铁了心说没有剩余空间,任何创建新文件都失败。我一度怀疑磁盘坏了、文件系统损坏、权限问题,折腾好久没头绪。直到老同事提醒我 df -i 看过没,一敲真相大白:磁盘空间确实还很空,但 inode 已经 100% 用满了。原来一个缓存目录堆了几百万个几字节的会话小文件、把 inode 耗尽了,而每创建一个文件都要消耗一个 inode,inode 没了哪怕还有大把空间也建不了任何新文件。复盘才懂:一块磁盘的容量不是能存多少字节一个维度,它还有另一个我从没在意的维度——能存多少个文件,由 inode 数量决定,两者各有总量各自独立耗尽,任一满了都报 No space left on device。海量小文件占空间极少却各吃一个 inode,于是空间充足 inode 却耗尽。我只盯着 df -h 一个维度,看它有余量就把磁盘满整个排除了。正解是 df -i 确认、清理过期小文件释放 inode、源头用集中存储如 Redis 和定期清理治理小文件,并把 inode 和空间一起纳入监控。这篇复盘从故障现场讲到 inode 是什么为何和空间各自独立耗尽、怎么诊断,再到清理与源头治理的完整正解,以及内存限额、CPU load、文件描述符、连接池等同类盲区,和容量是多维的、真正爆的常是认知之外的维度、诊断要警惕熟悉指标的正常掩盖陌生维度爆红的认知。

我的服务器突然报磁盘空间不足、写不了任何文件,可我 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 和空间一起纳入监控。

我立下的几条规矩

这场"磁盘很空却写不了文件"的事故,换来了我做运维时,刻进骨子里的几条铁律:

  1. 磁盘容量有两个独立维度:空间(字节,df -h)和 inode(文件数,df -i),各自独立耗尽。
  2. No space left on device 可能是空间满、也可能是 inode 满——两个都要查,别只看 df -h。
  3. 海量小文件占空间极少,却各吃一个 inode,会在空间充足时把 inode 耗尽。
  4. inode 耗尽扩磁盘空间没用;应急清理过期小文件,治本从源头减少小文件产生。
  5. session/cache/临时文件优先用集中存储(Redis 等),必须落地就配定期清理。
  6. 监控告警必须同时覆盖空间使用率和 inode 使用率,别让 inode 成为盲区。
  7. 排查一切"资源不足却指标充足"的问题,先怀疑这个资源有我没在看的另一个维度。

附:我现在用来"双维度盯磁盘 + 自动清理小文件"的脚本

这是我后来沉淀下来的一段巡检脚本,把这次踩坑的两条教训(空间和 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
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的服务调用下游一切正常,可一到高峰期就大量报连接失败、报错说没有可用端口了,我的机器明明负载不高、内存也充足,排查半天发现是几万个处于 TIME_WAIT 状态的连接把本地端口耗光了的深度复盘

2026-6-3 4:27:30

技术教程

我把大模型当成一个同样的输入必然给同样输出的普通函数来用,做了缓存、写了断言固定结果的测试,结果缓存老是不命中、测试三天两头挂,排查半天才明白大模型本质是概率采样、压根不保证每次输出一字不差的深度复盘

2026-6-3 4:38:21

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