• Moges
  • 2026-10-29

Linux 磁盘没满,为什么一个字节都写不进去(inode 耗尽排查)

那天的电话很典型:内部管理系统登不上去。页面能打开,登录框也正常,输账号密码,转一圈,回到登录页。换浏览器、清缓存、换账号都试过,用户的描述很准确——"永远登录不上"。

最后查出来的原因,和登录、和这套系统、和数据库都没有关系:这台 Linux 服务器上有一个没人管的邮件服务,把文件系统的 inode 用完了。而那时候 df -h 显示磁盘还有大半空闲。

这类故障在 Linux 上最有迷惑性的地方就在这里:报错说"No space left on device",人去看磁盘空间,空间充足;再去找大文件,找不到;于是开始怀疑应用、怀疑数据库、怀疑密码。真正该看的那条命令,往往要绕很久才想起来。

这篇把 Linux 上这个过程和机制完整拆一遍,包括三组可以自己复现的实测,以及一套下次遇到"写不进去"可以照着走的判断顺序。

Linux 磁盘没满,为什么一个字节都写不进去(inode 耗尽排查)

一、故障是从"登不上"开始的

页面能打开,说明 Web 服务活着;登录框能显示,说明应用还能响应请求。这两点排除了"服务挂了"。接下来输账号密码点登录,转一圈回到登录页——这个现象指向会话(session):登录成功后要写一个会话文件,之后每次请求凭它认人。会话写不进去,登录这件事在逻辑上就完成不了:密码校验通过了,会话没建起来,下一次请求仍然认为你没登录,于是把你送回登录页。

顺着这个思路查下去:重启应用服务,没用;看应用日志,日志里没有报错;查数据库连接,正常;直接跑登录相关查询,能出结果;账号状态和密码都没问题。

事后回看,有一个细节当时被忽略了——日志里不是没有报错,是报错写不出去。写不进日志的系统,看起来特别健康。

二、第一轮误判:以为磁盘满了

"写不进去"最常见的理由是磁盘满了,于是先看空间:

df -h

结果出乎意料:根分区还有几十个 G 余量,使用率不到六成。既然空间够,那就不是磁盘的问题——当时我们就是这么判断的,这个判断让排查多绕了将近一个小时。

接着做的事是找"谁把空间吃掉了":翻大文件、看日志目录、看上传目录、看数据库文件。du 一层层扫下去,最大的目录也就几个 G,加起来对不上任何异常。而系统那时候还在不停地报"写不进去"。

真正让人回头的是一个很朴素的疑问:如果空间真的够,为什么创建不了一个新文件?

顺着这个问题试了另一条命令:

df -i
Filesystem      Inodes   IUsed    IFree IUse% Mounted on
/dev/vda1      6553600 6553600        0  100% /

inode 用完了,100%。而同一时刻 df -h 依然显示空间充裕。两条命令,两个世界。

三、inode 是什么,为什么它也会用完

Linux 的 ext4 文件系统里,磁盘上有一块区域专门记录"文件清单",每个文件在清单里占一条记录,这条记录的编号就是 inode(索引节点)。一个文件实际占两样东西:

数据块——文件内容本身占的地方,df -h 统计的是这个;inode——文件名、权限、属主、大小、时间、数据块位置等元信息,df -i 统计的是这个。

所以"空间够不够"和"还能不能建新文件"是两个独立的问题。空间够、inode 用完,结果就是:已有的文件照常读,一个字也写不进去新的,报错还是那句最容易误导人的话——No space left on device。这句话里既没有 "disk" 也没有 "inode",它只说"设备上没有空间了"。

两种文件系统的差别也值得记住:ext4 的 inode 总数在格式化那一刻就定下来,默认大约每 16KB 空间给一个 inode,一块 100G 的盘大概六百多万个,这个数不能在线调大;xfs 的 inode 是动态分配的,一般不会因为文件数耗尽,但删文件后空间回收的表现和 ext4 不同(延迟分配与预分配)。

关键结论:inode 的消耗速度只跟"文件个数"有关,跟"文件大小"无关。一个 0 字节的空文件和一个 4G 的视频,占的 inode 完全一样。这就解释了为什么 du 扫不出异常——吃掉 inode 的那些文件,加起来可能只有几百兆,在几十 G 余量面前完全不值一提。

四、六十几万个小文件,从哪来的

按目录统计文件数(只读操作,inode 满的时候照样能跑):

du --inodes -d 1 /var | sort -rn | head

结果很干脆:/var/spool/postfix 一个目录占了六十几万个 inode,其余所有目录加起来不到它的零头。

postfix 是邮件服务。这台机器上跑着一套内部管理系统,邮件服务是当初装系统时按"默认全装"留下的——面板带了、系统带了,谁也没去关它。它平时安安静静,不占 CPU、不占内存,df -h 上看不出任何动静,所以从来没人注意过。

它是怎么把 inode 吃光的,三件事就够说清:

第一,邮件队列是一个"每封邮件一个文件"的地方。投递不出去就留在 deferred(延迟队列)里等重试。postfix 按哈希把它分成多层子目录,初衷就是"别让一个目录里堆几十万个文件"——这个设计本身就说明,队列堆积是被预料到的、需要防的问题。

第二,投不出去的邮件会一直重试。默认策略是隔一段时间重试一次,越往后间隔越长,一封邮件能在队列里躺好几天。

第三,也是最要命的:退信会生退信。系统里有定时任务,某些任务的输出会以邮件形式发给本地账号;如果这个账号不存在、或本地投递失败,邮件系统就生成一封"退信"通知原发件人;这封退信如果也投不出去(比如收件人是 root@主机名,主机名解析不了,或者外发被网络策略挡住),它又进队列、又重试、又生成新的退信。一封变两封,两封变四封。

这是典型的雪崩式增长,而且是无声的:没有告警、没有日志异常、CPU 内存都正常,只有 inode 在一点一点往下掉。等它掉到 0,所有需要"新建文件"的操作同时停摆——写不了会话文件,登录永远完不成;写不了日志,出错了连原因都留不下来;验证码图片生成不了;上传附件、导出报表、临时文件全部失败。

用户看到的仍然只是"登不上"。一个系统里最先坏掉的,往往是最不显眼的那个写操作。

把队列目录认清楚,下次一眼就能判断:

目录含义能不能删
maildrop / incoming刚接收、还没落盘不要删
active正在投递的不要删
deferred投递失败、等重试的可以删(先删最老的)
bounce退信处理中谨慎,正在生成的别删
corrupt / hold损坏或人工挂起的确认后可删

平时看队列,三条命令够用:

mailq                       # 列出队列
postqueue -p | tail -1      # 只看汇总:-- 632847 Kbytes in 612304 Requests
postsuper -d ALL deferred   # 清空延迟队列(谨慎,先看清数量)

那次汇总行给出的数字是六十多万封。这个数量不是一天涨起来的——起初每天几十封,投不出去的越攒越多,退信又生新的退信,最后每天新增几千封。它涨了几个月,而这段时间里没有任何一个监控项能看见它。

五、先把写入恢复,再谈根治

inode 满了只治一件事:删。但"删"在队列目录上有风险——直接 rm -rf 会把 postfix 自己维护的目录结构、属主和权限一起删掉,服务重启后大概率不正常;删正在投递的文件会把队列状态弄乱。当时的处置顺序是这样的:

第一步,留证据。先把 df -i、队列规模、目录清单各存一份,再动手。删完再想复盘,数据就没了。

df -i > /root/inode-before.txt
mailq | tail -3 > /root/mailq-before.txt
du --inodes -d 1 /var/spool/postfix | sort -rn > /root/postfix-inodes.txt

第二步,脚本分批清,不一把梭。不用 postsuper -d ALL,原因是它会把 active 和 incoming 里本来能正常送达的邮件一起删掉;几十万个文件一次删,IO 会把已经不健康的机器压到假死。做法是先删最没价值的:deferred 里最早的那批(躺了几天、早就没意义的退信),以及 corrupt(损坏队列)。

#!/bin/bash
Q=/var/spool/postfix/deferred
BATCH=2000
DRY=1          # 1=只统计不删;确认后改 0

echo "清理前:" && df -i / | tail -1

for d in "$Q"/*/; do
  cnt=$(find "$d" -type f -mtime +2 | wc -l)
  [ "$cnt" -eq 0 ] && continue
  echo "$d 待清理 $cnt 个"
  [ "$DRY" -eq 1 ] && continue
  find "$d" -type f -mtime +2 -print0 | xargs -0 -n "$BATCH" rm -f
done

echo "清理后:" && df -i / | tail -1

几个细节:-mtime +2 只删两天前的,刚进来还在重试的不动;-print0 加 xargs -0 处理文件名里的特殊字符;-n 2000 分批,避免单次删除把 IO 打满;DRY=1 先跑一遍看清单,确认目录和数量合理再改 0 真删。执行过程中用 df -i 盯着,看到使用率往下走就说明有效。

第三步,立刻验证写入恢复。不用等用户反馈,自己造一个文件:touch /tmp/inode-check,能创建就说明写入链路通了,然后才去看登录。

第四步,停掉邮件服务,这才是根治。清队列只是把水位降下来,服务还在跑,退信还会继续生。新系统用 systemctl disable --now postfix;没有 systemd 的老系统用 chkconfig postfix off 加 service postfix stop。停完隔十几分钟再看 df -i,不应再有量级变化。

为什么这一步算根治:问题的源头不是"队列满了",而是"这台机器根本不需要一个邮件服务"。它不承担业务功能,只是安装时的默认产物,唯一作用是替定时任务发一些没人看的邮件,发不出去就攒着。停掉它,等于关掉一条没人使用、却在持续生产垃圾的流水线。

反面情况也要说清楚:如果业务确实要发信(告警通知、找回密码),就不能一停了之,而要做三件事——配一个能真正投递出去的中继、给队列设上限和清理策略、把队列长度纳入监控。能发出去的信不会堆队列,所有队列堆积本质都是"发不出去还在发"。

六、三组实测:把这件事复现出来

把关键机制单独复现一遍,三组都在容器里做,不动生产环境。

实验一:空间还剩一半,inode 用光,写入失败。用 50M、只给 800 个 inode 的临时文件系统(tmpfs 的 nr_inodes 正好能限定 inode 数):

mkdir -p /mnt/a
mount -t tmpfs -o size=50m,nr_inodes=800 tmpfs /mnt/a
for i in $(seq 1 900); do touch /mnt/a/f_$i 2>/dev/null || break; done
df -h /mnt/a ; df -i /mnt/a ; touch /mnt/a/zzz
建成的文件数: 799
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           50.0M    0  50.0M   0% /mnt/a
Filesystem    Inodes  Used  Avail Use% Mounted on
tmpfs            800   800      0 100% /mnt/a
touch: /mnt/a/zzz: No space left on device

空间使用率 0%、inode 使用率 100%、报错 No space left on device。三行放在一起,就是整篇文章要讲的事。799 个文件全是 0 字节,但它们把 800 个 inode 用掉了 799 个。

实验二:文件删了,空间没回来。这条和 inode 是两种不同的"删了也没用",排查时特别容易混:

mkdir -p /mnt/b
mount -t tmpfs -o size=10m tmpfs /mnt/b
dd if=/dev/zero of=/mnt/b/big.bin bs=1M count=5 2>/dev/null
sh -c 'exec 3</mnt/b/big.bin; sleep 30' &
sleep 1 ; rm -f /mnt/b/big.bin
df -h /mnt/b ; ls -l /proc/$!/fd | grep big
--- rm 之后(空间没回来)---
tmpfs    10.0M  5.0M  5.0M  50% /mnt/b
--- 谁还占着这个已删文件 ---
lr-x------ 1 root root 64 Oct 29 00:55 3 -> /mnt/b/big.bin (deleted)

文件被进程打开着,rm 掉,目录里看不到,但空间不释放,直到进程退出或关闭句柄。这正是"df 说满了、du 却找不到大文件"的最常见原因(lsof +L1 或 /proc/<pid>/fd 能看到 (deleted))。而 inode 耗尽时,删文件是立刻有效的,删一个回来一个——所以处置方式完全不同:前者要重启那个进程,后者只能删。

实验三:分批删,立即恢复。删掉一半后,使用率从 100% 掉到 50%,touch 退出码立刻变成 0。

--- 清理前 ---
tmpfs   800  800    0  100% /mnt/a
本轮删除 400 个文件
--- 清理后 ---
tmpfs   800  400  400   50% /mnt/a
--- 再写一个文件 ---
touch 退出码: 0

生产环境上就是这个效果:不是"等它自己好",是删下去就见效。

七、怎么提前发现,而不是等用户来报

这次故障之所以拖到"用户打来电话"才被发现,是因为监控里只有 CPU、内存、磁盘空间,没有 inode。三件事补上:

一,df -i 进监控,和 df -h 并列,使用率到 80% 告警。inode 耗尽的过程通常是缓慢爬升然后突然见底,80% 给的是处置窗口。

二,队列长度进监控。用邮件服务的机器,看 mailq | tail -1 的数量,或直接数 deferred 下的文件。这个数持续上涨就是"发不出去还在发"的信号,比 inode 更早暴露问题。

三,按目录建 inode 基线。记一次正常情况下各主要目录有多少文件,出问题时直接对比。/var/spool、/tmp、会话目录、缓存目录、上传临时目录是高发区。

还有一条容易被忽略:inode 告警不能用"空间够不够"来替代。一块盘空间用了 90% 但文件很少,inode 可能只用 5%;另一块盘空间用了 30%、inode 已经 95%。这两种状态在 df -h 上一个看着更危险、一个看着更安全,实际正好相反。

八、几个错误动作,和为什么不能那么做

rm -rf /var/spool/postfix。队列目录的结构、属主、权限都是 postfix 自己维护的。删掉整棵树,服务重启后大概率起不来,或者起得来但队列状态错乱。要清就清里面的文件,保留目录树。

直接删 active 和 incoming。这两个是正在投递和刚进来的队列,删这里会丢掉本来能正常送达的邮件。先删 deferred 和 corrupt。

一条 rm 删六十几万个文件。参数过长直接报 Argument list too long;就算用 find -delete 跑,删除动作本身要更新目录和日志,IO 被打满,本来 inode 满了就已经很脆,很容易拖到假死。

只想扩容。加盘、扩分区解决的是空间,不解决 inode。ext4 扩容会按固定比例补 inode,缺的是几百万个,扩一倍空间才多一倍,通常不够;而且扩容期间业务还在持续产生垃圾,等于一边加水一边漏水。

用 tune2fs 调 inode 参数。ext4 的 inode 总数在格式化时就定了,不能在线增加,tune2fs 能调的项解决不了"数不够"。要大幅提高只能重建文件系统,那是带停机的迁移,不是应急手段。

把服务留着、只挂个定时清理脚本。这是我们一开始差点做的选择:既然清一次能好,就每天清一次。这等于承认一条没人用的流水线继续生产垃圾,然后用扫地机器人应对。清理脚本该有,作为兜底,但根治是停服务。

九、这件事和"最小化安装"是同一件事

我们在应用环境搭建那篇里写过装系统的原则:最小化安装,不需要的服务不装;装完还要回头做减法,把默认带上来又用不上的东西关掉。这次故障就是那条原则的反面教材,而且它暴露了一个平时看不见的成本——多装一个服务,成本不止是内存和 CPU,还有它在无人看管时的行为。

邮件服务在 df -h 上完全隐形,在 top 上几乎看不见,但它每天在往队列里写文件。等到它把 inode 用光,表现出来的是"业务系统登录不上"——一个和邮件毫无关系的症状。

同一逻辑也适用于安全侧:机器上多一个监听端口、多一个能发信的服务,就是多一个可被利用的入口。安全加固里讲关闭不需要的服务、收敛攻击面,和这件事其实是同一个动作的两面——关掉不需要的服务,同时解决"资源被悄悄消耗"和"攻击面偏大"。

判断标准一句话:这个服务如果今天挂掉,业务会不会受影响?不会,就该考虑停掉;不确定,先查它最近有没有实际工作(邮件看队列和日志,其他服务看连接数和日志),确认没在干活再停。

十、下次遇到"写不进去",按这个顺序走

把这次的路径压成一张判据表。顺序很重要,因为前两步会把人带偏,第三步才是这次的真凶:

顺序看什么命令结果说明什么
1空间df -h满了 → 逐层找大文件
2谁占着已删文件lsof +L1有 (deleted) → 重启对应进程即释放
3inodedf -i100% → 本文这种情况,只能删
4谁在造文件du --inodes -d1 /var定位目录,再往下钻
5写入通了吗touch /tmp/x能建 → 链路已通,再查应用
6会不会复发10 分钟后再看 df -i继续涨 → 源头没停,回第 4 步

两个提醒:第 1 步和第 3 步要一起做,别因为 df -h 好看就跳过 df -i;应用日志里"没有报错"要当成线索,不当成结论。

这类故障单看每一环都不复杂,难的是它出现在错误的位置上——所有表面现象都在指向应用,而真凶在系统层、在一个没人记得装过的服务里。我们摩杰斯做这类应急处理时,第一步永远是先把"写入能不能成功"这件事确认清楚,再往上查,因为一个连日志都写不进去的系统,它自己说什么都不能信。这类问题通常一次现场处置加一轮服务收敛就能根治,剩下的功夫在监控上——把 inode、队列长度、目录文件数这三项看见,同类事故就不会等到用户打电话。

Professional mold, automation parts manufacturing industry management software
Small and Medium-sized Enterprise ERP, MES, and MRP Solutions
Trade Inventory SaaS Management System, Linux Email Server Solution

Tel: 18625220688

Solutions

Mold Manufacturing Digitalization
Focused on mold shop production management, significantly shortening delivery cycles
ERP Systems for the Sheet Metal Industry
End-to-end management of laser cutting, bending, and welding
Custom Automation Equipment Management
Integrated quotes, production progress, and cost accounting
Tooling & Fixture Management Systems
Fully digital control of the inspection fixture production process

Resource Downloads

MES Whitepaper

In-depth Guide to Mold Manufacturing Execution Systems

Product Documentation

Moges PMS System Feature Guide

FAQ

Answers to all your questions

Contact Us

Now
Talk to Us

14 years of industry experience, focused on the digital transformation of mold, sheet metal, and custom automation equipment manufacturing

186 2522 0688

Headquartered in Kunshan, with on-site service within a 2-hour radius

Contact
Product & Service Inquiry

Free Online Consultation on Products & Solutions

Get a Quote

FAQ

Q:磁盘明明还有空间,为什么会写不进去?
A:因为文件系统除了"空间",还有一项资源叫 inode——每个文件占一条记录,记录用完就建不了新文件,报错仍然是 No space left on device。这种情况用 df -h 看不出异常,必须用 df -i 才看得见。

Q:df -h 显示还有几十 G,df -i 却是 100%,这说明什么?
A:说明文件个数把 inode 用光了,而不是空间被占满。此时已有文件照常读取,但任何新建文件的操作都会失败:写会话、写日志、生成验证码、上传附件、导出报表全部停摆。

Q:怎么快速找出是哪个目录占的 inode?
A:du --inodes -d1 /var | sort -rn 先看一级目录,再往下钻两三层基本就能定位。老版本 coreutils 没有 --inodes,可以退一步用 find <目录> -xdev -type f -printf '%h\n' | sort | uniq -c | sort -rn | head。这两个命令都只读,inode 满了也照样能跑。

Q:为什么最先表现出来的是"登录不上"?
A:因为登录依赖会话文件。密码校验通过了,但会话文件写不进去,登录这件事在逻辑上就完成不了,下一次请求仍然认为你没登录,于是把你送回登录页。同理,日志也写不进去——所以排查时看到的是"没有报错",实际是报错留不下来。

Q:df 说满了,du 却找不到大文件,是同一回事吗?
A:不是。那是另一种情况:文件已经被 rm 掉了,但仍有进程打开着它,空间要等进程退出才释放。用 lsof +L1 或看 /proc//fd 里的 (deleted) 能确认。区别在于处置方式——句柄问题重启进程就解决,inode 问题只能删文件。

Q:inode 用完了,扩容能解决吗?
A:只能按比例缓解。ext4 扩容时新增空间会按格式化时的比例补 inode,缺的是几百万个这种量级,扩容通常补不上;而且扩容期间业务还在持续产生垃圾,等于一边加水一边漏水。真要大幅提高 inode 密度只能重建文件系统,那是带停机的迁移。

Q:xfs 会不会遇到同样的问题?
A:一般不会,xfs 的 inode 是动态分配的。但它有别的表现要注意:删了文件之后,df 上的可用空间可能不会立刻变化(延迟分配与预分配),容易出现"删了没释放"的错觉。

Q:邮件队列能不能直接把目录删掉?
A:不要。队列目录的结构、属主、权限由 postfix 自己维护,删整棵树会让服务重启后异常或队列状态错乱。正确做法是先删 deferred 里最早的、以及 corrupt 目录里的文件,保留目录结构;active 和 incoming 里是正在投递的,不要动。

Q:停掉邮件服务会不会影响业务?
A:要看这台机器有没有真的在发信。判断方法是看队列有没有长期积压、日志里是不是全是重试和退信——如果一封都没投递成功过,它就没有存在价值。反之如果业务依赖发信(找回密码、告警通知),不能一停了之,要配可用的中继、给队列设上限,并把队列长度纳入监控。

Q:怎么提前发现 inode 要满了?
A:三件事:df -i 和 df -h 并列进监控,使用率 80% 告警;用邮件服务的机器把队列长度(mailq | tail -1)纳入监控;给主要目录建一次 inode 基线,出问题时直接对比。/var/spool、/tmp、会话目录、缓存目录、上传临时目录是高发区。

Q:这件事和"最小化安装"有什么关系?
A:直接关系——那台机器上的邮件服务是装系统时按默认全装留下的,从来没人用过。多装一个服务的成本不只是内存和 CPU,还有它在无人看管时的行为。装完做减法,把不需要的服务关掉,同时解决资源被悄悄消耗和攻击面偏大两个问题。

Q:这类故障一次能处理完吗?
A:通常可以。清队列恢复写入,再停掉不需要的服务根治,一次现场处置加一轮服务收敛就够。剩下的功夫在监控上:把 inode、队列长度、目录文件数三项看见,同类事故就不会等到用户打电话。我们摩杰斯的应急处理也是按"先恢复业务、再定位源头、最后补监控"这个顺序做的。

Latest News

Online Subscription