根分区爆满却找不到大文件?深度解析 Linux df 与 du 不一致的经典故障

在 Linux 运维工作中,有一个非常经典的"诡异"问题:df -h 显示根分区已经快写满了,使用率冲到 90%+,但用 du 逐层扫描目录,却怎么都找不到占用空间的大文件,可见文件加起来才十几二十 GB,几十 GB 的空间像凭空"消失"了。

本文就结合一次真实的 Tomcat 日志导致的根分区告警,完整拆解问题的现象、排查思路、底层原理和完整解决方案。

一、问题现象

本次故障发生在一台业务应用服务器上,核心现象非常典型:

  1. 执行 df -h 查看磁盘,根分区 /dev/sda1 总容量 99G,已用 89G,使用率 94%,仅剩 5.9G 可用,随时有写满导致业务异常的风险。
  2. 执行 du -sh /* 2>/dev/null 统计根目录下所有一级目录的大小,可见目录总占用仅约 20GB,与 df 的统计结果存在近 70GB 的巨大差值。
  3. 独立挂载的 1.2TB 数据盘 /data 占用正常,排除数据盘溢出的影响。

简单说就是:文件系统说空间被用了,但目录里看不到对应的文件

二、逐层排查定位

遇到 dfdu 统计不一致,优先从两个最高概率的方向排查:一是挂载点覆盖,二是已删除但未释放的文件。

1. 先排除挂载点覆盖

挂载点覆盖是另一个高频诱因:如果在挂载数据盘之前,根分区下的 /data 目录就已经写入了大量文件,挂载新磁盘后,原根分区的文件会被上层文件系统完全遮盖,du 扫描不到,但仍占用根分区空间。

排查方式是在业务低峰期卸载数据盘,查看原生目录大小。本次场景中数据盘使用正常,因此排除该原因。

2. 定位已删除未释放的文件

这是此类问题最高概率的根因。执行以下命令,查看所有已被删除、但仍被进程持有句柄的文件:

bash 复制代码
lsof | grep deleted

输出结果中,大量 java 进程条目指向同一个文件:

arduino 复制代码
java  31293  root  1w  REG  8,1  18317905473  547934  /home/.../logs/catalina.out (deleted)
java  31293  root  2w  REG  8,1  18317905473  547934  /home/.../logs/catalina.out (deleted)

可以看到:

  • 文件 catalina.out 已经被标记为 deleted,文件名已从目录中删除;
  • 文件大小约 17GB,完全对应上了空间差值;
  • 该文件属于 Tomcat 的 Java 进程,PID 为 31293。

3. 确认文件真实大小

/proc/PID/fd/ 目录下是进程打开的文件描述符符号链接,直接 stat 只会显示链接本身的大小,需要加 -L 参数跟随链接读取目标文件:

bash 复制代码
stat -L /proc/31293/fd/1

输出确认文件大小为 18317905473 字节,约 17.06GB,与 lsof 结果完全一致。

4. 误区澄清:PID 与 TID

lsof 输出中会出现大量 PID 相同、第三列数字不同的条目,这不是多个进程,而是同一个进程内的多个工作线程

  • 第 2 列是 PID(进程ID):整个 Tomcat 服务只有 1 个主进程 31293;
  • 第 3 列是 TID(线程ID):Java 是多线程程序,上百个工作线程共享同一个文件句柄。

所有条目的 inode 都相同,说明指向同一个文件实体,只需要处理一次即可,不需要逐个线程操作。

三、根因深度解析

1. df 与 du 的统计原理差异

两者统计口径完全不同,是出现差值的底层前提:

  • df:直接读取文件系统的超级块(Super Block),统计文件系统层面已分配的数据块总数,反映的是磁盘块的真实占用情况。只要数据块被占用,无论文件名是否存在,都会被统计进去。
  • du :遍历目录树,逐个累加目录中可见文件的大小,统计的是目录层面能看到的文件总大小。如果文件名被删除了,du 就扫描不到。

2. Linux 文件删除的本质

很多人误以为 rm 命令会直接删除文件数据,这是错误的。Linux 中一个文件由三部分组成:

  1. 目录项(dentry):文件名 + inode 编号,存在于目录结构中;
  2. inode:文件的元数据(权限、大小、时间戳、数据块指针等);
  3. 数据块:真正存储文件内容的磁盘块。

rm 命令做的事情非常简单:删除目录项,将对应 inode 的硬链接计数减 1。只有同时满足以下两个条件,文件才会被真正删除、数据块才会被释放:

  • inode 的硬链接计数为 0(没有任何文件名指向它);
  • 进程打开计数为 0(没有任何进程持有该文件的句柄)。

3. 本次故障的完整链路

  1. 运维人员手动执行 rm catalina.out 删除日志,试图释放空间;
  2. rm 删除了文件名(目录项),du 遍历目录时看不到这个文件了;
  3. 但 Tomcat 进程仍在运行,持续向日志文件写入内容,进程持有文件的打开句柄;
  4. 硬链接数虽为 0,但进程打开数不为 0,因此磁盘数据块不会被释放;
  5. df 统计的是数据块占用,因此仍显示这 17GB 为已用状态。

这就是"文件删了,空间却没释放"的完整原理。

四、解决方案

1. 应急方案:不重启业务,即时释放空间

如果业务不能中断,可以通过进程文件描述符直接截断文件,无需重启 Tomcat,空间立即释放。

原理:/proc/PID/fd/ 是内核提供的进程文件描述符映射,即使文件名已删除,通过描述符仍可以直接操作文件实体。

执行命令:

bash 复制代码
# 截断标准输出对应的文件描述符
> /proc/31293/fd/1

fd 1fd 2 指向同一个文件实体,截断其中一个即可。执行完成后,立即验证:

bash 复制代码
# 查看根分区使用率是否回落
df -h /

# 确认文件大小已归零
stat -L /proc/31293/fd/1 | grep Size

注意:该方式只能释放空间,不会重新生成 catalina.out 文件。后续新日志会继续写入这个"无名文件",要完全恢复正常日志输出,仍需在业务窗口重启 Tomcat。

2. 根治方案:从源头避免问题复现

(1)规范日志清理方式

绝对不要用 rm 删除正在写入的日志文件。正确的清空方式是截断文件,保留文件名,不影响进程继续写入:

bash 复制代码
> catalina.out

(2)配置日志自动切割

使用系统自带的 logrotate 工具,配置自动按天切割日志,保留指定天数,避免单文件无限膨胀。

新建配置文件 /etc/logrotate.d/tomcat

bash 复制代码
/home/apache-tomcat-*/logs/catalina.out {
    daily
    rotate 7
    compress
    missingok
    notifempty
    copytruncate
}
  • daily:每天切割一次;
  • rotate 7:保留最近 7 份日志;
  • copytruncate:先复制再截断原文件,无需重启 Tomcat 即可完成切割,适合生产环境。

这个问题本质上是对 Linux 文件系统原理的考察。理解了文件的存储结构和删除逻辑,再遇到类似的"磁盘空间凭空消失"问题,就能快速定位、从容处理。

相关推荐
魏祖潇1 小时前
framework 整合实战——DDD/TDD/SDD 三件套在 framework 仓的真实落地
人工智能·后端
神奇小汤圆2 小时前
责任链模式 + 策略模式:优雅处理多级请求的方式
后端
神奇小汤圆2 小时前
没啃透无锁队列,高并发底层你只懂了皮毛!
后端
大鸡腿同学2 小时前
大模型是怎么训练出来的?
后端
lizhongxuan3 小时前
判断一个人懂不懂 agent harness
后端
非洲农业不发达3 小时前
windows终端体验大升级,让你拥有macos级别的美化
前端·后端
妙码生花4 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十七):登录接口完善,登录页接口整合,解决跨域
前端·后端·ai编程
SamDeepThinking4 小时前
从源码到代码:MyBatis-Flex 与 MyBatis-Plus 的逐项对比
java·后端·程序员