df 显示磁盘满了,du 却找不到:一例被删除但未释放的 inode 排查

举报
yd_276135645 发表于 2026/10/31 06:06:28 2026/10/31
【摘要】 df 说根分区 96%,du 统计只有一半,大文件怎么都找不到。本文记录被删除但仍被进程持有的 inode 怎么定位(lsof +L1 与扫 /proc/*/fd)、不重启服务如何用 truncate -s 0 释放空间,以及 logrotate 的 copytruncate 与 create 差在哪。

上周遇到一个很典型的问题:监控报警说根分区 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,不用管。

五、怎么让它别再发生

根因在"文件句柄被长期持有 + 文件被替换"这个组合,所以预防也在这两点上:

  1. logrotate 用 copytruncate,如果应用不支持重新打开日志文件。create 模式会新建文件,老 fd 留在原地;copytruncate 是复制一份再截断原文件,fd 一直有效,不会留下孤儿 inode。代价是复制和截断之间那一小段时间可能丢几行日志。
  2. 能发信号就发信号。postrotate 里 kill -HUP(或 systemctl reload)让应用自己重开日志文件,比 copytruncate 干净,不丢日志。
  3. 别用手动 rm 清日志。要清就 truncate -s 0,或者走 logrotate 的 --force。
  4. 加一条对账告警。按挂载点分别取 df 的 used 和 du -x 的 total,两者差值超过阈值(比如 10G 或 5%)就报。这条比单纯"磁盘使用率 > 90%"有用得多——它能在磁盘真满之前就告诉你,有个进程正在偷偷写一个你看不见的文件。

最后补一句关于容器:容器里删文件同样有这个问题,而且因为容器生命周期短,更容易在 docker stop 之后才被发现"空间没回来"。docker ps -s 的 writable layer 大小并不含这些已删除但仍被打开的文件,真要看还得进容器的 /proc 或用 lsof。宿主机上一条 lsof +L1 通常能直接把那个 PID 揪出来——它的 cgroup 会告诉你是哪个容器。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。