Linux 磁盘空间排查实战:df 显示已满但 du 找不到大文件怎么办

前言:磁盘告警了,但你什么都删不掉

个人站长最怕半夜收到服务器磁盘告警短信:磁盘使用率 100%,网站打不开,后台也进不去。更气人的是,你登录服务器执行 df -h,明明显示根分区已经满了,可是用 du 挨个目录统计,加起来却远远不到总量,怎么都找不到那几十 G 的空间到底被谁吃掉了。这种情况我遇到过不止一次,几乎每个站长都会踩一回。本文把磁盘空间"神秘消失"的常见原因和排查思路完整梳理一遍,照着操作就能把空间找回来。

一、先搞清楚 df 和 du 为什么对不上

排查之前必须理解一个基本概念:df(disk free)统计的是文件系统层面已使用的块数量,而 du(disk usage)是逐个遍历目录、累加文件大小算出来的结果。两者统计口径不同,出现几 MB 的误差很正常,但如果差距达到几个 G,那就说明有文件存在于文件系统上,却不在任何目录树的可见路径里。这些文件就是磁盘空间丢失的元凶,最常见的有四类:被删除但未释放的文件、inode 耗尽、被挂载点掩盖的目录、以及失控的日志文件。

二、头号元凶:已删除但被进程占用的"幽灵文件"

这是最经典的一种情况。你执行 rm 删除了一个大文件,du 也看不到了,但磁盘空间却没有释放。原因是某个进程(比如 Nginx、MySQL、PHP-FPM 或者你跑着的采集脚本)仍然持有这个文件的文件描述符,文件的数据块还实实在在占着磁盘。Linux 只有在所有打开该文件的进程都关闭句柄之后,才会真正回收空间。排查命令如下:

# 查看被删除但仍然被占用的文件
lsof +L1
# 或者只关注 deleted 的文件
lsof | grep deleted

输出结果里会列出进程名、PID 和文件路径(路径末尾带 (deleted) 标记)。最常见的场景是 Nginx 的 access.log 或 error.log 被 rm 删掉后没有执行 reload,导致日志句柄一直挂在老文件上;MySQL 的 binlog 或 undo 文件被误删也常出现这种情况。确认是哪个进程后,最干净的做法是重启该服务或者执行 kill -HUP 进程PID 让它重新打开日志文件,空间立刻就会释放。如果进程不能重启(比如 MySQL 正在跑业务),可以先把文件内容清空(> /proc/PID/fd/对应的句柄 或者用 truncate),也能马上腾出空间。

三、被忽略的 inode 耗尽

有时候 df -h 显示空间明明还有剩余,但网站就是报"磁盘已满",新建文件失败。这时候要查 inode:

df -i

inode 是文件系统用来记录文件元数据(权限、所有者、数据块位置)的结构,每个文件或目录都要占用一个。如果分区里塞满了大量小文件(比如邮件队列、PHP session、缓存的碎片文件、某个程序不断生成的空文件),inode 就会被耗尽,此时即使磁盘还有空闲空间也无法创建任何新文件。常见的罪魁祸首是 /tmp 目录、PHP-FPM 的 session 目录、以及被垃圾评论插件或采集程序刷出来的海量小文件。排查方法:

# 找出哪个目录文件数量最多
for d in /*; do echo "$d: $(find $d -xdev -type f 2>/dev/null | wc -l)"; done | sort -t: -k2 -rn | head

定位之后清理小文件即可,平时也可以把 PHP session 目录改到 tmpfs(内存盘)上,从根上避免 inode 耗尽。

四、被挂载点"掩盖"的目录

第三种情况比较隐蔽。假如你的 /var/www 挂载了一块数据盘,而 /var/www/html 里本来就有一些老文件,挂载之后这些老文件并没有消失,只是被新挂载的内容"盖住"了。你用 du 去统计 /var/www/html 看到的是新盘的内容,老文件占用的空间在 du 里完全看不到,但 df 却会计入它们。更常见的是有人把某个目录 mount --bind 到了别处,或者 Docker 的容器层叠加导致重复计费。排查命令:

# 查看所有挂载点,确认有没有异常挂载
findmnt -o TARGET,SOURCE,FSTYPE,SIZE,USED
# 用 du 的 -x 参数跳过其他文件系统,只看当前分区
du -xhd1 / | sort -hr | head

如果发现某个挂载点下面有"看不见"的空间,可以先卸载该挂载点(umount),再对原目录做一次 du,把被掩盖的文件清理掉,然后重新挂载。

五、日志黑洞:nohup.out 和容器日志

第四类元凶是失控的日志。很多站长习惯用 nohup python3 run.py > nohup.out 2>&1 & 跑脚本,nohup.out 只追加不轮转,跑上几个月能膨胀到几十 G。Docker 容器更夸张,容器的 stdout 日志默认全部写进 /var/lib/docker/containers 下的 json 文件,一个打印日志频繁的容器一天就能吃掉几个 G。排查时直接看最大的文件:

# 找出全盘最大的 20 个文件
find / -xdev -type f -size +500M -exec ls -lh {} \; 2>/dev/null | sort -k5 -rh | head -20

找到之后对症下药:nohup 脚本改用 logrotate 管理输出,或者用 systemd 的 journald 统一接管;Docker 则在启动参数里加上 --log-opt max-size=50m --log-opt max-file=3,或者在 docker-compose.yml 里配置 logging 限制,一劳永逸。

六、提升效率的小工具:ncdu 与 du 的妙用

前面提到用 du -xhd1 / 逐层定位大目录,这个方法虽然有效但比较费时,目录层级一多要反复执行好几轮。推荐一个效率神器 ncdu(NCurses Disk Usage),一条命令就能交互式地浏览整个磁盘的占用情况,按大小排序,回车进入目录,d 键直接删除,比纯命令行高效得多:

apt install ncdu
ncdu /

进入界面后默认按占用大小降序排列,最大的目录一目了然,用方向键逐层下钻,几秒钟就能定位到问题目录。ncdu 默认会跳过其他挂载点(-x 参数行为),跨盘统计时注意区分。另外几个实用技巧:du -sh * | sort -rh | head 可以快速列出当前目录下最大的几个子项;du --max-depth=2 -h /var | sort -rh | head 可以限定统计深度避免输出刷屏;统计 /proc 这类虚拟文件系统时要加 -x 跳过,否则会得到无意义的巨大数字。把这些工具组合起来,磁盘占用的排查速度能提升一倍以上。

七、实战案例:一次典型的磁盘告警排查

拿我遇到的一个真实案例来说明完整流程。某天凌晨网站突然报错,后台提示写入数据库失败,登录服务器执行 df -h 看到根分区使用率 100%,可用空间为 0。第一反应是删缓存,但 du -sh /var/www/* 统计下来网站目录总共才用了 8G,和 df 显示的 40G 总量差距巨大。接着执行 lsof +L1,果然发现两个进程的问题:一个是 MySQL 的 binlog 文件被之前的手动清理脚本误删,但 mysqld 进程还开着句柄,占着 12G 空间;另一个是 Nginx 的 access.log 被 logrotate 执行失败后残留的旧句柄,占了 5G。确认之后执行 systemctl reload nginx 释放了日志句柄,MySQL 因为正在跑业务不能重启,就用 truncate 把对应的 /proc 文件描述符指向的文件清空,空间当场释放了 17G。随后 df -i 检查 inode,发现 /tmp 下有十几万个 PHP session 残留文件,一并清理。整个排查过程不到十分钟,但如果没有这套思路,光靠瞎删文件可能删错东西。

八、预防方案:让磁盘告警不再发生

排查只能救急,预防才是根本。推荐三件套:第一,所有日志纳入 logrotate 管理,Nginx、PHP-FPM、MySQL 的日志统一配置每天轮转、保留七天、超过 100M 就压缩;第二,写一个简单的监控脚本放到 crontab 里每小时跑一次,磁盘使用率超过 85% 或者 inode 使用率超过 90% 就通过邮件或者钉钉/企业微信机器人告警,代码如下:

#!/bin/bash
# /usr/local/bin/disk_check.sh
USE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
INO=$(df -i / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USE" -gt 85 ] || [ "$INO" -gt 90 ]; then
    echo "磁盘告警: 空间使用率 ${USE}%, inode 使用率 ${INO}%" | \
      curl -s -X POST -d "text=磁盘告警&desp=空间${USE}% inode${INO}%" \
      "https://你的webhook地址" >/dev/null
fi

第三,养成定期检查的习惯,每个月手动跑一次 df -hdu -xhd1 /lsof +L1 三项检查,把隐患消灭在告警之前。磁盘空间是服务器最基础的资源,学会这套排查方法,比任何优化技巧都更能保证网站稳定运行。

九、完整排查流程与预防建议

总结一套五分钟排查流程:第一步 df -hdf -i 确认是空间还是 inode 问题;第二步 lsof +L1 找幽灵文件;第三步 du -xhd1 / 逐层定位大目录;第四步 find -size +500M 找大文件;第五步检查挂载点和 Docker 日志目录。平时做好三件预防事:给日志加上 logrotate 轮转、写一个磁盘使用率监控脚本超过 85% 就告警、定期检查 inode 使用率。做到这几点,磁盘告警就再也不会让你半夜爬起来删文件了。

Last modification:August 13th, 2026 at 09:13 am

Leave a Comment