某天早上收到告警:根分区使用率 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 掉日志的动作很可能就是我自己做的:日志涨太快,第一反应是删掉。现在改成先看哪个进程在写它,再决定是轮转还是截断——删文件从来不是释放空间的正确姿势。
评论