df 说磁盘满了,du 却查不出大文件,问题在被删掉的文件
前阵子家里那台跑同步任务的小机突然报磁盘满。登录上去一看:
$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/sda2 98G 93G 0 100% /
可用空间直接是 0,服务开始报 No space left on device。我下意识去 du 找大文件:
$ du -sh /* 2>/dev/null | sort -rh | head
28G /home
3.1G /var
1.2G /usr
...
全部加起来也就三十多 G,跟 df 报的 93G 差了一大截,中间那六十多 G 凭空消失了。第一次遇到的时候我愣了好一会儿,还以为 df 坏了。
先搞清楚它俩不是一回事
后来想明白了,这两个命令看的东西根本不同:
df问的是文件系统:"你一共分配出去多少块?"——它读的是超级块里的统计。du是老老实实顺着目录树,把能遍历到的文件大小一个个加起来。
所以只要存在"占着块、但在目录树里找不到"的文件,两边就对不上。这种情况主要有几种,最常见的是第一种。
元凶:被 rm 了但还被进程攥着的文件
Linux 里 rm 只是把目录项摘掉(unlink),文件的 inode 和数据块要等所有打开它的进程都关闭之后才会真正释放。如果某个进程还握着这个文件描述符,那块空间就一直占着,既不算自由块,也不在 du 的遍历范围里。
找它最直接的办法:
$ lsof -nP +L1
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
python3 2841 root 6w REG 8,2 68719476736 0 918273 /home/sync/task.log (deleted)
一行就把真相说明白了:(deleted) 是关键标志,SIZE/OFF 那列 68719476736 字节,算下来正好 64G,跟消失的空间基本对得上。
+L1 的意思是"列出链接数小于 1 的文件",也就是已经没有目录项指向它的。如果系统上 lsof 版本老不支持这个参数,用下面这种也行:
lsof -nP 2>/dev/null | grep '(deleted)'
效果一样,就是输出杂一些。
我这台的实际情况是:同步脚本的日志跑了一个多月没做轮转,涨到 64G。我清理磁盘的时候顺手 rm 掉以为完事了,结果写日志的 python 进程一直没重启,fd 还开着,空间一分没回来。
怎么把空间要回来
办法一:重启进程。 最干净。进程一退,fd 关闭,inode 引用归零,空间立刻释放。能停服务就用这个。
办法二:别重启进程,直接把 fd 截断。 有些服务不能随便停,可以这么干:
# 先看这个进程开了哪些已删除的 fd
ls -l /proc/2841/fd | grep deleted
# 假设看到 6 -> /home/sync/task.log (deleted)
# 把内容清空(注意是截断,不是删除)
: > /proc/2841/fd/6
执行完再 df -h,空间马上回来了。这个操作不影响进程继续持有 fd,它还会往里写,只是文件从 0 开始。
不过这招有个前提:文件是以写方式打开的,而且没有用 O_APPEND 之外的奇怪标志。而且话说回来,截断别人正在写的文件属于应急手段,事后还是应该把日志轮转配好,不然过段时间又是一样的问题。
从根上避免:别用 rm,用 truncate
踩过这次之后,我清理日志再也不用 rm 了,改成:
truncate -s 0 /home/sync/task.log
# 或者
: > /home/sync/task.log
这样 inode 还是那一个,进程手里的 fd 依然有效,文件大小瞬间归零,磁盘空间立刻回收,一行命令全解决。
如果是 nginx、php-fpm 这类自己管日志的服务,用 logrotate 的话记得加 copytruncate:
/home/sync/task.log {
daily
rotate 7
compress
missingok
notifempty
copytruncate
dateext
}
copytruncate 的原理是先复制一份再截断原文件,好处是不需要给进程发信号,对那些不支持重新打开日志的程序特别友好。缺点是复制和截断之间有一小段时间窗,极端情况下可能丢几行日志——对家庭场景这点损失完全能接受。
另外两种对不上的情况
如果 lsof +L1 什么都没查出来,还有两个常见可能。
一是挂载点把原来的文件遮住了。 比如 /mnt/data 下面本来就堆了几十 G 东西,后来你在同一个目录挂了个新盘,那么原来那些文件谁也访问不到,但空间照样占着,du 也扫不到。验证方法:
mkdir -p /tmp/rescue
mount --bind / /tmp/rescue
du -sh /tmp/rescue/mnt/data
用 bind 挂载绕开子挂载点,就能看见"被遮住"的那部分真实体积了。
二是 inode 用光了,不是空间用光了。 表现很像:写文件时报 No space left on device,但 df -h 明明还有剩余。这时候看:
df -i
如果 IUse% 是 100%,说明是海量小文件把 inode 耗光了,常见于没清理的缓存目录、邮件队列、或者容器镜像的 layer 目录。定位大目录:
find / -xdev -type d -size +100M 2>/dev/null
小结
现在遇到"磁盘满",我的排查顺序基本固定了:
df -h确认哪个挂载点满了;df -i排除 inode 耗尽;du -sh *找不到就lsof -nP +L1找被删未释放的文件;- 还是没有,考虑挂载点遮蔽,用 bind 挂载看一眼。
九成的情况在第 3 步就能收尾。这个坑本质上就是"删文件"和"释放空间"在 Linux 上是两件事,想明白了之后就很难再被它坑到了。
- 点赞
- 收藏
- 关注作者
评论(0)