df 显示磁盘满了,du 却找不到:一例被删除但未释放的 inode 排查
上周遇到一个很典型的问题:监控报警说根分区 96%,登录上去 df -h 确实是 96%,但 du -sh /* 加起来只有 40G,磁盘明明有 100G。差了整整一半,而且怎么排都找不到那个"大文件"。
这类"df 满了但 du 找不到"的情况,九成以上是同一个原因:文件被删了,但还有进程开着它。
一、为什么删了的文件还占空间
Unix 文件系统里,文件名和文件数据是两回事。rm 做的事只是把目录项里那个名字摘掉,把链接数减一。只要链接数还没归零,或者还有进程持有这个文件的文件描述符(fd),inode 和数据块就不会被回收——空间当然还占着。
所以 du 看不见它(它已经不在任何目录里了,du 是靠遍历目录树统计的),但 df 看得见(df 问的是文件系统本身还剩多少块)。两边数字对不上,就是这类文件在中间。
日志是最常见的受害者:应用以追加方式打开 /var/log/app.log 后一直持有 fd,运维为了"清理空间"直接 rm 掉,或者 logrotate 用了 create 模式新建了一个同名文件。老 fd 指向的还是那个已经被摘掉名字的 inode,应用继续往里写,空间继续涨——你越删越满,因为写的是那个看不见的文件。
二、怎么把它找出来
两个命令,我都用:
# 只看链接数为 0(已被删除)但仍被打开的文件
lsof +L1 2>/dev/null | sort -k7 -n -r | head -20
# 不依赖 lsof:直接扫进程 fd
for p in /proc/[0-9]*; do
ls -l "$p/fd" 2>/dev/null | grep -q deleted && \
du -shc $(ls -l "$p/fd" 2>/dev/null | grep deleted | \
awk '{print $NF}' | sed 's/(deleted)//') 2>/dev/null | tail -1
done
第二种麻烦一点,但更可靠:有些精简镜像里没有 lsof,而容器里的 /proc 是有的。定位到之后,ls -l /proc/<pid>/fd 里长这样:
lrwx------ 1 root root 64 Oct 31 03:12 7 -> /var/log/app.log (deleted)
(deleted) 这个后缀就是全部答案。
三、不重启服务也能把空间还回去
最省事的办法当然是重启进程,但生产上未必允许。另一种做法是直接把文件截断——fd 还在没关系,数据块释放掉就行:
truncate -s 0 /proc/<pid>/fd/7
这里有个坑值得记一下:很多人习惯写 : > /proc/<pid>/fd/7 或者 > /proc/<pid>/fd/7。前者在部分内核/文件系统上能生效,但它本质是"以截断方式重新打开路径",而 /proc/<pid>/fd/N 的打开语义是复制一份 fd,权限和模式并不完全等于原文件;后者干脆会因为重定向和 proc 的交互而失效。truncate -s 0 走的是 ftruncate,语义最直白,我一般优先用它。
截断后注意一件事:如果进程是按 fd 顺序写入并维护了自己的 offset,截断后 offset 不会自动归零,文件开头会变成一段 \0(稀疏洞)。对日志这种只追加的场景无所谓,对二进制文件就别这么干。
四、df 和 du 对不上,也不全是这个原因
排查时先排除几种"正常"的差异,能省很多时间:
- 不同文件系统。
du -sh /不会跨进/proc、/sys、/dev/shm(tmpfs)这些挂载点,而df是按挂载点报的。要比就得按挂载点分别比,du -x只统计同一文件系统。写到/dev/shm的文件吃掉的是内存,df -h里根本不会出现在/上。 - 硬链接。
du默认对同一 inode 只算一次,df算的是实际块数,包含稀疏文件的元数据开销。 - 文件系统预留块。ext4 默认给 root 留 5%,
df显示的可用比按块数算出来的少,这是设计如此,tune2fs -m可以调,但不建议调到 0。 - 已删除但仍打开。就是本文这种,唯一会让
df比du大出一大截的原因。
判据很简单:差距是几个 G 并且在缓慢增长 → 基本就是 deleted inode;差距固定且很小 → 预留块和 tmpfs,不用管。
五、怎么让它别再发生
根因在"文件句柄被长期持有 + 文件被替换"这个组合,所以预防也在这两点上:
logrotate用copytruncate,如果应用不支持重新打开日志文件。create模式会新建文件,老 fd 留在原地;copytruncate是复制一份再截断原文件,fd 一直有效,不会留下孤儿 inode。代价是复制和截断之间那一小段时间可能丢几行日志。- 能发信号就发信号。
postrotate里kill -HUP(或systemctl reload)让应用自己重开日志文件,比copytruncate干净,不丢日志。 - 别用手动
rm清日志。要清就truncate -s 0,或者走 logrotate 的--force。 - 加一条对账告警。按挂载点分别取
df的 used 和du -x的 total,两者差值超过阈值(比如 10G 或 5%)就报。这条比单纯"磁盘使用率 > 90%"有用得多——它能在磁盘真满之前就告诉你,有个进程正在偷偷写一个你看不见的文件。
最后补一句关于容器:容器里删文件同样有这个问题,而且因为容器生命周期短,更容易在 docker stop 之后才被发现"空间没回来"。docker ps -s 的 writable layer 大小并不含这些已删除但仍被打开的文件,真要看还得进容器的 /proc 或用 lsof。宿主机上一条 lsof +L1 通常能直接把那个 PID 揪出来——它的 cgroup 会告诉你是哪个容器。
- 点赞
- 收藏
- 关注作者
评论(0)