磁盘明明满了,du 却算不出东西在哪?聊聊 df 与 du 对不上的排查思路

发布时间:2026/10/30 10:05:41
磁盘明明满了,du 却算不出东西在哪?聊聊 df 与 du 对不上的排查思路

文章目录

  • 📖 介绍 📖
  • 🖥️ 演示环境 🖥️
  • 🧭 一、先分清两把尺子各量什么
  • 🔍 二、头号元凶:文件删了,空间却没回来
    • 🧠 原理:删除的只是名字
    • 🧪 实测复现:1G 文件消失前后
  • 🧩 三、另外三种容易混淆的次因
    • 📦 文件系统保留块
    • 🗂️ 挂载点遮盖
    • 🧮 inode 耗尽
  • 🛡️ 四、一条顺手的排查路线 + 两个预防习惯
  • ⚓️ 相关链接 ⚓️

📖 介绍 📖

NAS 或者服务器用久了,很多人都被同一个怪现象卡住过:系统天天喊磁盘快满了,告警一条接一条,可你拿常规的目录体积统计命令挨个算下来,所有目录加起来离「满」差得远。想删点东西腾空间,又不知道该删谁,只能干瞪眼。

这类「两个统计口径打架」的问题,在 Ubuntu、Debian、群晖、飞牛这类 NAS 系统的 SSH 终端里都可能出现,而且绝大多数情况下不是统计命令算错了,而是两把尺子量的根本不是同一样东西。这篇文章就把这层窗户纸捅破:先讲清两个命令的统计口径差在哪,再带你定位最常见的元凶——「已经删掉、却还被某个进程死死攥着」的大文件,最后补上另外三种容易混淆的次因和一套排查顺序,下次再遇到就能五分钟收工,不至于对着告警干着急。

🖥️ 演示环境 🖥️

  • 💻 实测主机:OpenClaw 容器宿主环境(Linux x64,overlay 根分区 898G)
  • 🧪 复现方式:本机创建 1G 大文件、由长驻进程持有句柄后删除,逐步采集两把尺子的读数
  • 🌐 网络条件:本地环境,命令全程离线可用
  • 🙅 本文边界:命令在主流 Linux 发行版通用,NAS 品牌差异只影响界面不影响原理

📢注意:本文内容为个人学习笔记,仅供参考。附:读者须知

🧭 一、先分清两把尺子各量什么

先说结论:磁盘占用告警读的是内核维护的文件系统层面数据,而目录统计命令只数目录树里还能被看见的文件。两者天然存在三处口径差。

第一处:inode 元数据。目录统计默认只累加文件内容的大小,而文件系统层面还要为每个文件、每个目录存一份元数据,小文件极多的场景下,这部分开销能差出好几个 G,上万个零碎文件累积下来就是几百 K 到几 M 的量级。

第二处:挂载点遮盖。如果某个子目录在统计之后又被挂载了别的磁盘或网络存储,那么统计走目录树时会漏掉被遮住的部分,而文件系统层面读到的是整块盘的真实占用。

第三处,也是最常见的一处:已删除但没释放的文件。这是两把尺子分歧的头号来源,也是这篇文章的主角,值得单独一节讲透。

🔍 二、头号元凶:文件删了,空间却没回来

🧠 原理:删除的只是名字

Linux 的文件删除机制是「引用计数」:rm只是把目录里的文件名摘掉,只要还有进程握着这个文件的打开句柄,内核就不会真正回收磁盘块。对进程来说,文件还能照常读写;对文件系统来说,这些块仍然「已占用」;但对目录树来说,这个文件已经不存在了——所以目录统计完全看不见它,文件系统层面却一分不少地算着。

典型中招场景特别多:应用把日志写满几个 G,运维rm掉想立刻腾空间,结果写入进程还握着旧文件的句柄,空间纹丝不动,进程反而继续往一个看不见的文件里猛写;还有被意外中断的备份任务、容器里被删的大镜像缓存、数据库进程持有的临时表空间,全是同一个病根。

🧪 实测复现:1G 文件消失前后

空口无凭,我在本机完整走了一遍。先造一个 1G 的文件,再启动一个持续读它的长驻进程:

fallocate-l1G big.logtail-fbig.log>/dev/null2>&1&

接着删文件,然后分别看两个口径的读数:

rm-fbig.logdu-sh/tmp/dfdu-demo# 输出:0(目录自身占块近似为空)df-h/# 已用空间几乎不变

目录统计显示该目录为 0——文件确实从目录树上消失了;但文件系统层面的已用空间没有下降。这时用一条命令就能把隐形文件揪出来:

lsof+L1|grepbig.log# tail 532223 ... 1073741824 ... (deleted)

+L1的含义是「列出链接数已小于 1、也就是已经没有文件名、但仍被进程持有的文件」。输出里的1073741824正好是 1G(1073741824 字节),进程名tail,状态标着(deleted)——凶手现形。

处置只需要让那个进程放手。我把这个长驻进程结束掉之后再看文件系统读数,已用空间立刻回落,前后差值与被持有的文件大小一致,空间当场还回来了:

kill532223df-h/

如果握着句柄的是业务服务,不想硬杀,优雅的做法是重启对应服务(systemd 系用systemctl restart),或者对它执行日志轮转配置里标准的重载动作,让它自己关掉旧句柄。

🧩 三、另外三种容易混淆的次因

排查时如果长驻句柄这条路查完没有收获,再按顺序过一遍下面三种情况。

📦 文件系统保留块

ext4 这类文件系统会默认预留大约 5% 的空间给根用户,这部分既不显示在目录统计里,普通用户也不能写。盘越大绝对值越惊人,150G 的盘就是 7 个 G 起步。用tune2fs -l /dev/你的分区 | grep -i reserved能看到具体数值,这只是解释口径差的背景知识,一般不建议动它。

🗂️ 挂载点遮盖

先跑df -h看清楚到底哪块分区在报警,再进那块分区的挂载目录用du -x统计。-x参数会跳过其他文件系统,保证两把尺子量的是同一块盘,不会把挂在子目录里的另一块盘的数据错算进来,也不会漏掉被遮住的目录。

🧮 inode 耗尽

还有一类「假满」:块空间还有富余,但 inode 用光了,写入照样报空间不足。用df -i一眼可见,常见于海量小文件场景(邮件队列、会话缓存、拆碎的图片切片)。确认是这类问题后,清理方向就从小文件堆积的目录入手,而不是盯着大文件找了。

🛡️ 四、一条顺手的排查路线 + 两个预防习惯

把上面的内容串成一条路线,遇到「盘满但算不出来」直接照着走:

df-h# 确认是哪块分区、真实占用多少df-i# 顺手排除 inode 耗尽du-x--max-depth=1/挂载点2>/dev/null|sort-rh|head# 逐层下钻lsof+L1|sort-k9-n-r|head# 查已删未释放(老版本列位可能不同,先 lsof +L1 | head 肉眼确认列位)

四个命令从上到下,绝大多数案例在第四步就能定位元凶。

预防层面有两个习惯值得养成。其一,清理正在被写入的大日志不要用删除,改用截断:truncate -s 0 文件名或echo "" > 文件名,句柄还在、文件大小归零,空间立刻释放,进程也不受影响。其二,让长驻服务的日志都走日志轮转机制管理,轮转配置自带「复制后截断」的处理方式,从源头避免「删了也白删」的循环。

⚓️ 相关链接 ⚓️

  • XiaoqiangClub
  • AskUbuntu:为什么 df 和 du 显示不同的磁盘用量
  • Server Fault:磁盘满了但 du 统计对不上,如何继续排查
  • Unix & Linux:df 与 du 差异定位实例