KernelRanch
返回文章列表

磁盘写满的四步定位:df 找出挂载点、du 找出目录、lsof 找出已删除未释放、df -i 找出 inode

2026-11-10 #运维

磁盘写满时最耗时的分支是"删了文件但空间没回来",其次是"看哪个目录都只有几十兆,df 却报 100%"。我按四步走:df -h 先确认是哪个挂载点和哪个维度(块还是 inode),du -x 在同一个文件系统内逐层下钻,lsof +L1 抓已删除但仍被进程持有的文件,最后 df -i 排除 inode 耗尽。每一步都能证伪上一步,走完这四步还没结论,才需要考虑快照、日志文件系统保留块这类更冷的原因。

四步顺序与每步的判断

步命令看什么结论反证(这步正常意味着)
1df -hUse% 接近 100% 的挂载点锁定文件系统,记下 Mounted on空间充足,问题不在块占用
1bdf -iIUse%IUse% 高而 Use% 低即 inode 耗尽inode 充足,排除小文件海量问题
2du -xhd1 <挂载点>第一层里最大的目录逐层下钻到具体目录目录合计远小于 df 的 Used,进入第 3 步
3lsof +L1(deleted) 行、PID、NODE有进程持着已删除文件无输出,空间确实没被占用
4清理kill 进程或让它重开文件空间回收—

可复制的四步:

df -h                                  # 1. 哪个挂载点满了
df -i                                  # 1b. 是块满还是 inode 满
du -xhd1 /var 2>/dev/null | sort -h    # 2. -x 不跨文件系统,逐层下钻
lsof +L1 | head -20                    # 3. 已删除未释放(+L1 = link count < 1)
stat -Lc 'size=%s blocks=%b' /proc/<pid>/fd/<n>   # 4. 核实真实占用

一次 1 GiB 的实测:df 和 du 差了整整 1 GiB

在 debian:12-slim 容器里(overlay 根,1007 GiB)写入 1 GiB,再让一个进程以只读方式持有它并删除:

# 写入前
used_kb_before=18662976
# dd 1 GiB + sync 后
used_kb_after_write=19711556      # 差 1048580 KB ≈ 1 GiB
# 以只读 fd 持有后 unlink
used_kb_after_unlink=19711556     # 一点没回
$ du -sh /tmp/demo
4.0K /tmp/demo                   # 目录已经是空的
$ lsof +L1
COMMAND  PID USER  FD  TYPE DEVICE   SIZE/OFF NLINK NODE NAME
sleep   3898 root  3r  REG   0,97 1073741824     0 45751 /tmp/demo/big.bin (deleted)
$ stat -Lc 'size=%s blocks=%b' /proc/3898/fd/3
size=1073741824 blocks=2097160
# kill 3898 之后
used_kb_after_kill=18662976      # 精确回到写入前的值

这组数字说明两件事:du 报 4.0K 是对的(目录里确实没有文件了),df 报 1 GiB 被占也是对的(inode 还活着,块没释放)。两者不矛盾,差的正是那个 (deleted) 文件。NLINK 为 0 是 lsof +L1 能抓到它的原因。

四个坑

第一,把 df 与 du 的差额当成 du 算错。只要差额恰好等于某个"删掉的大文件",基本就是已删除未释放。判断依据是 df 的 Used 在删除前后没有变化,而不是 du 的数字。

第二,du 不加 -x。默认会跟着进入其他文件系统,把 bind mount、overlay 下层、容器挂载点反复统计,结果可能比 df 大得多或者小得多。统一用 du -xhd1,只在当前文件系统内下钻。需要看真实占块而非表观大小时,注意 du 默认报的是设备占用,-b/--apparent-size 报的是表观大小,稀疏文件两者能差出几个数量级。

第三,只看 lsof 的 SIZE/OFF 列判断占用。这一列读的是文件当前的 inode 大小,如果这个文件曾被 O_TRUNC 打开过,它就会显示 0,而 df 上仍可能有历史占用。真正可信的是 /proc/<pid>/fdinfo 和 df 的前后差值。我第一次做这个实验时就踩了:用 exec 3>file 打开,重定向把 1 GiB 文件截成了 0,df 随之回落,看上去像"删除即释放",换成 exec 3<file 才复现出真正的持有场景。

第四,df -h 很空但写入失败,是 inode 用完了。本机 df -i / 显示 Inodes 67108864, IUsed 88674, IUse% 1%,容器里这个数字来自 overlay 下层,未必代表你自己的数据盘。inode 耗尽的典型来源是小文件堆积(session 文件、邮件队列、/var/spool 下的临时文件),此时 du -sh 总量不大,但 df -i 已经 100%。

小结

四步的价值在于每步都有明确的证伪条件:df 正常就不必修磁盘,df -i 正常就不用找小文件,du 合计与 df 对得上就不用查 lsof,lsof 没有 (deleted) 行就说明空间确实不在这些进程手里。这样走下来,绝大多数"空间去哪了"能在两分钟内收敛到具体目录或具体进程。

清理时优先让进程重开文件(kill -HUP、重启服务、日志轮转),而不是直接 kill -9。对于不方便重启的进程,truncate -s 0 /proc/<pid>/fd/<n> 也能释放块,但这属于应急手段,事后要补上日志轮转或 copytruncate 之类的配置,否则同样的问题会再来一次。

还有一类高频来源值得单独提:被 tail -f、docker logs -f、日志采集 agent 打开着的日志文件。运维删掉文件以为腾出了空间,采集进程的 fd 还指着旧 inode,df 一点都不降。判断方法和上面完全一样,lsof +L1 的 COMMAND 列会直接写出是哪个采集器。此时正确处理是重启采集器或让它重开文件,而不是继续删别的文件。

inode 上限则要在建模阶段就确认:df -i 的 IUsed% 和 IFree 是这个文件系统创建时定下的,事后不能在线扩容(部分文件系统支持,代价不小)。小文件密集的服务(缓存目录、邮件队列、CI 的临时工作区)应当单独挂一个 inode 充足的卷,避免把根文件系统一起拖进 inode 耗尽。

数据来源与检索时间

来源URL对应数据
man7.org · df(1)https://man7.org/linux/man-pages/man1/df.1.html-i 列 inode 用量、-h、--output 字段、-t 限定文件系统类型
man7.org · du(1)https://man7.org/linux/man-pages/man1/du.1.html-x 不跨文件系统、-d 深度、-b/--apparent-size 与设备占用的区别
man7.org · lsof(8)https://man7.org/linux/man-pages/man8/lsof.8.htmllsof 列出打开文件、-p 限定进程、输出列含义(FD/TYPE/DEVICE/NLINK/NODE)
man7.org · free(1)https://man7.org/linux/man-pages/man1/free.1.html页缓存与 buff/cache 的关系(判断"内存被缓存吃掉"时的对照)

检索时间:2026-11-10(Asia/Shanghai)。

核验说明

  • 真跑:1 GiB 写入、只读 fd 持有后 unlink、df 差值、du -sh、lsof +L1、stat -L /proc/<pid>/fd/3、kill 后空间回收,全部在 debian:12-slim 容器内实测,数值为原始输出。
  • 真跑:一次失败的经验也保留在文中——用 exec 3>file 会截断文件,导致 df 回落、lsof 的 SIZE/OFF 显示 0;换成 exec 3<file 才复现出空间不回收。
  • 真跑:df -i / 的 inode 总量与使用率(67108864 / 88674 / 1%)为容器内实际输出。
  • 仅依据文档,未在本机运行:inode 耗尽场景未复现。容器内执行 mount -t tmpfs tmpfs /mnt 返回 permission denied(无 CAP_SYS_ADMIN),无法创建可独立耗尽的文件系统;该节结论依据 df(1) 的 -i 语义与一般经验,并已标注。