拆开的机械硬盘:主轴、盘片与悬停在上面的磁头
运维 5 分钟

磁盘满了,但 du 找不到大文件

df 说根分区 100%,du 加起来只有 13G。差的 27G 藏在「已经被 rm、但进程还开着」的文件里。

某天早上收到告警:根分区使用率 100%,服务开始报「No space left on device」。

按常规思路清点了一遍:

$ df -h /
Filesystem  Size  Used  Avail  Use%  Mounted on
/dev/vda1    40G   40G     0   100%  /

$ du -sh /* 2>/dev/null | sort -h | tail -3
1.1G  /usr
2.3G  /var
8.7G  /opt

加起来不到 13G,可 df 说 40G 全用完了。两个工具报的数字差了 27G,这就是问题的入口。

一、先搞清楚两个数字为什么不一样

这两个命令查的根本不是一回事:

df     问的是文件系统:这个块设备一共多少块、还剩多少块(看超级块)
du     问的是目录树:从某个目录往下走,把每个文件占的块加起来(看 inode 和目录项)

只要出现「某个文件占了空间,但它不在任何目录里」,df 算得到、du 算不到,差额就出现了。所以 27G 的差距不是算错了,而是真有东西 du 看不见。

二、最常见的一种:被删掉但还开着的文件

用 lsof 列出所有链接数为 0 的打开文件,也就是「名字已经从目录里删掉、但还有进程握着它」的文件:

$ lsof +L1
COMMAND    PID USER   FD   TYPE DEVICE    SIZE/OFF NLINK   NODE NAME
mysqld    2841 mysql   12u   REG  253,1 12884901888     0 262155 /var/lib/mysql/ib_logfile0 (deleted)
docker-co 3902 root    7w   REG  253,1  7516192768     0 263901 /var/lib/docker/.../json.log (deleted)

找到了:一个 12G 的文件和一个 7.5G 的文件,加起来正好对上那 27G(还有零头是别的小文件)。

这是 Linux 上一个很容易反直觉的行为:

  • rm 只是把目录项删掉,把 inode 的链接数减 1
  • 只要还有进程打开着这个 inode,它的数据块就不会被回收
  • 这个文件的目录项已经没了,所以 du 遍历目录树时永远碰不到它
rm 之前:
  目录项 /var/log/app.log  ──►  inode 263901  ──►  数据块 7.5G
                                    ▲
                                    └── 进程的 fd 也指着它

rm 之后:
  (目录项没了)
                               inode 263901 ──► 数据块 7.5G   ← 空间还占着
                                    ▲
                                    └── 进程的 fd 还指着它

最典型的场景就是日志:进程一直往日志文件里写,运维发现日志太大随手 rm 掉,磁盘却没释放——因为进程的 fd 从来没关过。

三、还有几个不那么常见的原因

上面那个原因占了大多数,但排查时最好一起过一遍,避免方向找错:

  • 文件被挂载点挡住了。 挂载之前如果往挂载目录里写过东西,挂上去之后这些文件就被盖住了。遇到这种情况,把设备 umount 掉,或者在别处重新挂一遍看看。
  • 保留块。 ext4 默认给 root 保留 5% 的空间。df 的 Use% 是按普通用户视角算的,可能显示 100% 而实际上 root 还有余量。想让这块小一点可以 tune2fs -m 1 /dev/vda1。
  • inode 用完了。 磁盘还有空间,但 inode 号已经分光,同样会报 No space left。这种要看 df -i,现象通常是「一堆很小的文件」。

四、处理:怎么把空间立刻还回来

最省事的办法是重启那个进程,fd 一关,空间立刻释放。但生产上未必能直接重启,尤其是数据库。

更好的做法是直接把那个 fd 清空,进程完全不用停:

# 拿到 pid 和 fd 号(lsof 输出里的 NAME 之前那列,比如 7w → fd 7)
truncate -s 0 /proc/2841/fd/7

# 或者用重定向,效果一样
: > /proc/2841/fd/7

原理是:/proc/<pid>/fd/<n> 是内核给这个打开的 fd 开的一个入口,往它写等同于操作那个文件本身。把它截断成 0 字节,占用的块就还给了文件系统,而进程依然拿着同一个 fd 继续用,不会崩。

这一步做完再回头看,数字就对上了:

$ df -h /
/dev/vda1    40G   13G    27G   33%  /

五、小结

「df 满了但 du 找不到大文件」,本质是两个工具的视角不同:一个看文件系统,一个看目录树。对不上就说明有文件脱离了目录树却仍被引用。

  • 先算差额:df 的已用量减去 du 的合计,差额有多大、值得不值得查
  • 查被删但仍打开的文件:lsof +L1,或者 find /proc/*/fd -ls 2>/dev/null | grep deleted
  • 释放空间用 truncate -s 0 /proc/<pid>/fd/<n>,不必重启服务
  • 根治的办法还是别 rm 日志——用 logrotate 加 copytruncate,或者让写日志的进程自己支持重新打开文件

另外,那个 rm 掉日志的动作很可能就是我自己做的:日志涨太快,第一反应是删掉。现在改成先看哪个进程在写它,再决定是轮转还是截断——删文件从来不是释放空间的正确姿势。

评论