磁盘没满却写不进去:inode 耗尽排查实战,df -i、find -xdev 定位小文件与 lsof +L1 找已删除句柄

磁盘还有空间,却写不进任何文件

这是每一个运维人都遇到过的灵异事件:df -h 显示磁盘才用了 60%,还有几十个 G 的剩余空间,但网站突然报错"写入失败"、日志停止滚动、数据库报 No space left on device。第一反应是磁盘满了,可明明还有空间;再一看,连 touch 一个空文件都失败。

这时候真正的元凶往往是 inode 耗尽。文件系统不只记录"数据块"的占用,还要为每个文件分配一个 inode(索引节点),用来存储文件的权限、时间戳、数据块位置等元信息。inode 的数量在格式化文件系统时就固定下来了,一旦用光,哪怕数据块再空,也无法创建新文件——甚至连一个字节都写不进去。

本文讲清楚 inode 是什么、怎么排查耗尽、怎么找到"元凶文件"、以及各种盘满场景(inode 满、删除文件仍占用、目录被删但空间没释放)的排查思路。这是个人站长必须掌握的一项基础排障技能。

先分清两种"磁盘满"

df -h 看的是数据块(block)的使用情况,单位是容量。而 inode 的使用情况要单独用 df -i 看:

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        40G   24G   14G  64% /

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

上面这个例子就是典型:容量还有 14G,但 inode 已经 100% 用满。IUse% 一旦到 100%,系统就会拒绝创建任何新文件。IUsed 和 IFree 两个值必看:IFree 为 0 就是彻底耗尽。

如果 df -i 显示 inode 正常,那问题就在数据块上,接着往下看后面几节。如果 df -i 是满的,直接跳到"找出占用 inode 的元凶"。

inode 是怎么被用光的

一个文件占一个 inode,一个目录也占一个 inode。所以以下几种情况最容易把 inode 吃光:

  • 海量小文件:临时文件、会话文件、缓存碎片。PHP 的 session 目录、某些论坛的缓存、/var/lib/php/sessions 有时能攒出几十上百万个小文件。
  • 邮件队列堆积:/var/spool/postfix/maildrop 或 /var/mail 里如果有一堆退信,小文件数量惊人,很多 CentOS 服务器就是这样被撑爆的。
  • 程序疯狂创建日志或锁文件,且从不清理。
  • cron 任务每分钟生成一个文件,跑一年就是几十万个。
  • 恶意攻击:有的攻击会利用上传漏洞往临时目录塞大量文件,或者制造死循环创建文件消耗 inode。

个人站尤其是 WordPress 或者论坛站,插件和缓存机制很容易产生海量小文件,所以 inode 耗尽并不罕见。

第一步:确认 inode 使用情况

# 看所有挂载点的 inode
df -i

# 只看某个目录所属文件系统
df -i /var/www

# 用 stat 看单个文件系统的详细信息
stat -f /

stat -f 的输出更详细:

$ stat -f /
  File: "/"
    ID: ... Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 10485760   Free: 3670016    Available: 3670016
Inodes: Total: 2621440    Free: 0

看到 Inodes: Free: 0 就板上钉钉了。

第二步:找出占用 inode 最多的目录

这是最关键的一步。用 find 递归统计每个顶层目录下的文件数量:

# 统计根目录下每个一级目录的文件数(含子目录内所有文件)
for dir in /*; do
  echo -n "$dir: "
  find "$dir" -xdev -type f 2>/dev/null | wc -l
done

参数 -xdev 很重要:它表示不跨越文件系统边界。因为 /proc、/sys、挂载的其它分区不算在同一个 inode 池里,不加这个参数会统计一堆无关文件,还可能特别慢。

如果一级目录看不出来,就往深一层钻。定位到罪魁祸首所在的目录后,再按下面的方式列出子目录的文件数:

# 在当前目录下,统计每个子目录的文件数并排序
for d in */; do
  echo -n "$d: "
  find "$d" -xdev -type f 2>/dev/null | wc -l
done | sort -t: -k2 -rn | head -20

常用的"重灾区"目录,可以直接先查这几个:

find /var/spool -xdev -type f 2>/dev/null | wc -l
find /var/lib/php -xdev -type f 2>/dev/null | wc -l
find /tmp -xdev -type f 2>/dev/null | wc -l
find /var/log -xdev -type f 2>/dev/null | wc -l
find /var/cache -xdev -type f 2>/dev/null | wc -l

第三步:清理与善后

找到元凶后,先别急着 rm -rf,务必确认这些文件是可以删的。

场景一:session 文件堆积。 PHP session 有回收机制,但如果 session.gc_probability 配得不合理,或者站点流量大又没人清理,就会堆积。可以安全删除过期 session:

# 删除 24 小时未访问的 session 文件
find /var/lib/php/sessions -type f -mtime +1 -delete

# 或者删除 2 小时前的
find /var/lib/php/sessions -type f -cmin +120 -delete

场景二:邮件队列堆积。 先看队列大小,确认真的是退信再清:

# 看积压数量
ls /var/spool/postfix/maildrop | wc -l

# 确认后清空(谨慎!确认没有正常邮件在里面)
postsuper -d ALL     # 清空 postfix 队列
# 或直接清理 maildrop 目录

场景三:日志碎片。 有些程序每天生成一个日志文件且不轮转,攒了几年。用 logrotate 规范起来,或者清理过期日志:

find /var/log -type f -name "*.log.*" -mtime +30 -delete

删除大量小文件时,rm -rf 可能会卡很久(因为它要逐个系统调用 unlink)。如果目录下文件数以百万计,用 find ... -delete 或者 rsync 空目录覆盖法更快:

# 快速清空一个海量小文件目录
mkdir /tmp/empty
rsync -a --delete /tmp/empty/ /path/to/huge/dir/

如果 inode 根本没满,但提示空间不足

那就是数据块的问题了。这时候排查思路完全不同。

关键命令:用 du 找出谁在占空间。

# 从根目录逐层下钻,找出最大的目录
du -xhd1 / | sort -h

# 进入最大的目录继续
du -xhd1 /var | sort -h

# 找出当前目录下最大的 20 个文件
du -ah . 2>/dev/null | sort -h | tail -20

-x 同样是不跨文件系统,-d1 表示只统计一层深度。sort -h 按人类可读的容量排序。

常见的空间大户:数据库文件、网站日志、备份文件、Docker 的 /var/lib/docker、某些程序的缓存。Docker 尤其能吃空间,可以看:

docker system df
docker system prune -a --volumes   # 清理无用镜像、容器、卷(确认后再执行)

一个经典陷阱:删了文件空间却没释放

这是个必考知识点。场景是这样的:你发现一个大日志文件 5G,rm 掉了,结果 df -h 一看,空间一点没变。为什么?

因为在 Linux 里,文件被进程打开时,rm 只是删除了目录项(unlink),真正的数据块要等所有打开该文件的进程都关闭之后才会释放。如果 Nginx、MySQL 或者某个程序还持有这个文件的句柄,空间就"悬空"了。

排查方法是用 lsof 找"已删除但仍被打开"的文件:

# 列出所有状态为 deleted 但仍被占用的文件
lsof | grep deleted

# 或者用 lsof 的 +L1 选项(链接数小于 1,即已删除)
lsof +L1

# 找出占用空间最大的几个
lsof +L1 2>/dev/null | awk '{print $7, $1, $2, $NF}' | sort -rn | head

输出里会显示进程名、PID 和文件路径。正确的处理方式不是直接 kill 进程(可能影响线上服务),而是让进程重新打开日志文件。最优雅的做法是:

# 找到持有句柄的进程 PID 后,用信号让它重新打开日志
kill -USR1      # 大多数服务(nginx 等)约定 USR1 表示重开日志

# 或者清空文件而不是删除(推荐做法)
: > /var/log/bigfile.log    # 或者 truncate -s 0 /var/log/bigfile.log

记住这个教训:清理正在被写入的日志,用 > file 截断,而不是 rm 删除。 前者立刻释放空间,后者要等进程关闭句柄。这也是为什么正确的日志清理应该靠 logrotate,它用的是 copytruncate 或重开句柄的方式,而不是简单删除。

预防:别等出事了才排查

比起事后救火,更好的做法是提前监控。几个实操建议:

  1. 把 df -i 加入监控。 大多数监控只看 df -h,忽略了 inode。一个简单的脚本就能提前告警:
    #!/bin/bash
    THRESHOLD=90
    df -i --output=pcent,target | tail -n +2 | while read pct target; do
      num=${pct%\%}
      if [ "$num" -ge "$THRESHOLD" ]; then
        echo "WARN: $target inode usage ${pct}"
      fi
    done
  2. 给日志和缓存目录配 logrotate 或定时清理任务。
  3. 临时目录挂 tmpfs 或设自动清理,比如 systemd 的 systemd-tmpfiles,可以配置 /tmp 按时间自动清理。
  4. maildrop 队列做监控,服务器如果没配好邮件发送,很容易积压。

总结

"磁盘没满却写不进去"这个经典问题,排查套路其实很清楚:先 df -i 看 inode,再 df -h 看容量,两者至少有一个是满的。inode 满就找小文件重灾区,容量满就用 du 逐层下钻。如果删了文件空间没释放,就用 lsof +L1 找被占用的已删除文件,用 : > file 或 kill -USR1 正确地让服务释放句柄。

把这几个命令练熟:df -i、find -xdev | wc -l、du -xhd1 | sort -h、lsof +L1。它们几乎能解决 99% 的磁盘相关故障,是每个站长工具箱里必须常备的利器。

Last modification:October 2nd, 2026 at 09:25 pm

Leave a Comment