读:df 与 du——为什么两个磁盘用量命令数字对不上
目录
半夜三点,监控告警:磁盘 80%。你登上服务器,顺手跑了 du -sh / ,结果回了个 50%。又跑了两遍,数字还是对不上。你没看错,命令也没坏。
TecMint 上有篇文章把这个现象拆得很清楚: df 和 du 量的根本是两件不同的事。数字对不上不是 bug,是 Linux 文件系统的设计使然。知道差在哪,排查方向就有了。
机制:两个命令量的不是同一个东西
df 不扫描目录,不检查文件。它直接读文件系统的超级块(superblock),问内核一句话:"当前有多少磁盘块被标记为'已分配'?"
超级块是文件系统的账本,记录着整个分区的块分配情况。内核分配一个块、释放一个块,都更新超级块。 df 只是在某个瞬间翻了一下这本账。
du 的工作方式完全相反。它从你指定的路径出发,一颗一颗地遍历目录树,统计每一个能看见的文件和目录的大小,最后加总。
df 量的是"文件系统层面"——哪些块被占了。 du 量的是"目录树层面"——哪些文件我能看见。
这两层之间的灰色地带,就是差异的来源:磁盘块在文件系统层面被标记为"已分配",但在目录树里找不到对应的文件。 df 能看到这些块, du 看不到。
最常见原因:删了文件,但进程还抓着不放
sudo lsof +L1 | sort -k7 -rn
rm 删除文件时,Linux 只做了两件事:
- 从目录里拿掉这个文件名
- 把 inode 的 link count 减 1
磁盘上的数据本身没有动。
此时 du 立刻看不到这个文件了。但如果有个进程在 rm 之前就打开了这个文件(比如 nginx 正在写 access.log ),内核不会释放那些磁盘块——进程还在用。 df 仍然把它们计为"已分配"。
直到进程关闭文件描述符(退出或重启),内核才释放磁盘块, df 的数字才降下来。
典型场景:运维看到磁盘快满了, rm 掉一个几十 GB 的日志文件, df 一看——数字完全没动。
关于这个话题,之前写过一篇Linux 删文件的真相——用 /proc 恢复被进程持有的已删除文件,讲了 lsof +L1 的输出怎么读、怎么从 /proc/PID/fd/ 把数据捞回来、怎么用 hard link 预防。本文不再重复,重点讲已删除文件之外的其他原因。
一个检测脚本:有没有已删除但未释放的文件
在说其他原因之前,先把检查封装成一个脚本。放到服务器上,半夜三点直接跑:
#!/bin/bash # check-deleted-files.sh —— 检查是否存在已删除但仍占磁盘空间的文件 set -e echo "=== 已删除但进程仍持有的文件 ===" echo TEMP=$(mktemp) sudo lsof +L1 2>/dev/null | awk ' NR>1 { size = $7 if (size ~ /^[0-9]+$/) { total += size # 从第10个字段开始拼文件名(lsof 的 NAME 列从字段10开始) name = "" for (i = 10; i <= NF; i++) name = name $i " " printf " %-10s PID=%-8s FD=%-5s %s\n", humansize(size), $2, $4, name } } function humansize(s, u, v) { u = "BKMGTP" v = s while (v >= 1024 && length(u) > 1) { v /= 1024 u = substr(u, 2) } return sprintf("%.1f%s", v, substr(u, 1, 1)) } END { if (total == 0) { print " 没有发现已删除但仍被持有的文件。" } else { printf "\n 总计占用: %s\n", humansize(total) printf " (这部分空间 df 计入,du 看不到)\n" } }' > "$TEMP" cat "$TEMP" rm -f "$TEMP"
脚本做的事:遍历 lsof +L1 的输出,把每个文件的 SIZE 列加起来,换算成人类可读的大小,末尾汇总一个总计。那个总计数字,就是 df 能看到但 du 看不到的空间。
其他让数字对不上的原因
文件系统保留块
ext 系列文件系统默认把 5% 的磁盘空间预留给 root 用户。这部分空间不属于任何文件, du 根本看不到它们;而 df 虽然不把它们列入"Used"列,但 df 的"Available"列已经扣掉了这部分——也就是说, du 加总出来的数字永远够不到 df 的"Size",差值里有 5% 是文件系统提前圈走的。
查看保留块大小(仅 ext2/3/4,XFS 用 xfs_info ,Btrfs 无直接等价命令):
sudo tune2fs -l /dev/sda1 | grep -i "reserved block"
Reserved block count: 655360 Reserved blocks uid: 0 (user root)
如果磁盘很大(TB 级),5% 是一个可观的数字。非系统盘可以考虑调低这个比例:
sudo tune2fs -m 1 /dev/sdb1
-m 1 的意思是只保留 1%。但别在根分区上这么干——/ 一旦满了连 root 都写不进去,系统就没法操作了。
隐藏挂载点
如果你先在一个目录里写满了数据,后来又在这个目录上挂载了其他分区,那么即使原来的数据还在,你已经看不到了:
# 先写数据 dd if=/dev/zero of=/tmp/bigfile bs=1M count=500 # 之后 /tmp 又挂载了独立分区 sudo mount /dev/sdb1 /tmp
此时 du -sh / 看不到 /tmp/bigfile ,因为 /tmp 现在指向的是新分区的根。但 df 看的是 / 所在的分区——那个 500MB 的文件块还在上面,占着空间。
排查方法:用 bind mount 把 / 重新暴露到一个干净路径下,再看一遍:
sudo mkdir /mnt/root_inspect sudo mount --bind / /mnt/root_inspect du -sh /mnt/root_inspect/tmp
如果数字比 du -sh /tmp 大很多,挂载点底下有被遮住的数据。
容器和 overlay 文件系统
容器运行时(Docker、Podman 等)用 overlay 文件系统把多个层叠在一起。容器的写操作存在 upper 层,删除操作用"白文件"标记。overlay 自己的空间统计机制和底层文件系统的超级块统计不完全对齐,尤其是在大量删除容器镜像层之后。
这种情况排查比前几种复杂,通常先确认前三种原因都不成立,再看是不是容器导致的。
排查三步
先找出已删除但仍占空间的文件,按大小排:
sudo lsof +L1 | sort -k7 -rn | head -10
九成情况到这一步就找到了——基本都是一个被 rm 掉的日志,nginx 或 Java 进程还抓着 fd 往里写。
然后释放空间。两条路。
能重启的话,干净利落:
sudo systemctl restart nginx
进程重启关闭所有文件描述符,内核回收磁盘块, df 马上反映。
如果生产环境不能停服,从 /proc 把文件清掉:
# 从 lsof 输出拿到 PID 和 FD,比如 PID=1423, FD=10 sudo truncate -s 0 /proc/1423/fd/10
truncate -s 0 把文件大小设为零。进程的 fd 还开着,继续往里写,但原来的空间已经释放, df 立刻见效。
警告:数据库 write-ahead log、任何进程用于 crash recovery 的文件都别碰。数据会坏。只对普通应用日志用这一招,因为丢了内容也无所谓。
最后确认空间回来了:
df -h /var/log
如果还是没降,回头看看是不是还有别的进程也持有已删除文件,或者检查保留块和隐藏挂载点。
工具速查表
| 场景 | 命令 | |
|---|---|---|
| 文件系统到底满没满 | df -h |
|
| 哪个目录吃空间最多 | du -sh /* | sort -rh | |
| 为什么删了文件空间没回来 | =sudo lsof +L1 | sort -k7 -rn= |
| 文件系统给自己留了多少余量 | =sudo tune2fs -l /dev/sdX | grep -i reserved= |
| 挂载点底下有没有藏着数据 | sudo mount --bind / /mnt/inspect && du -sh /mnt/inspect/ 挂载点路径 |
再碰到半夜三点磁盘告警,先跑检测脚本,再看工具速查表。九成情况, lsof +L1 的前几行就把答案摊在你面前。