磁盘还有空间,却写不进任何文件
这是每一个运维人都遇到过的灵异事件: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 或重开句柄的方式,而不是简单删除。
预防:别等出事了才排查
比起事后救火,更好的做法是提前监控。几个实操建议:
- 把
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 - 给日志和缓存目录配 logrotate 或定时清理任务。
- 临时目录挂 tmpfs 或设自动清理,比如 systemd 的
systemd-tmpfiles,可以配置/tmp按时间自动清理。 - 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% 的磁盘相关故障,是每个站长工具箱里必须常备的利器。