在 Linux 运维工作中,有一个非常经典的"诡异"问题:df -h 显示根分区已经快写满了,使用率冲到 90%+,但用 du 逐层扫描目录,却怎么都找不到占用空间的大文件,可见文件加起来才十几二十 GB,几十 GB 的空间像凭空"消失"了。
本文就结合一次真实的 Tomcat 日志导致的根分区告警,完整拆解问题的现象、排查思路、底层原理和完整解决方案。
一、问题现象
本次故障发生在一台业务应用服务器上,核心现象非常典型:
- 执行
df -h查看磁盘,根分区/dev/sda1总容量 99G,已用 89G,使用率 94%,仅剩 5.9G 可用,随时有写满导致业务异常的风险。 - 执行
du -sh /* 2>/dev/null统计根目录下所有一级目录的大小,可见目录总占用仅约 20GB,与df的统计结果存在近 70GB 的巨大差值。 - 独立挂载的 1.2TB 数据盘
/data占用正常,排除数据盘溢出的影响。
简单说就是:文件系统说空间被用了,但目录里看不到对应的文件。
二、逐层排查定位
遇到 df 与 du 统计不一致,优先从两个最高概率的方向排查:一是挂载点覆盖,二是已删除但未释放的文件。
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 中一个文件由三部分组成:
- 目录项(dentry):文件名 + inode 编号,存在于目录结构中;
- inode:文件的元数据(权限、大小、时间戳、数据块指针等);
- 数据块:真正存储文件内容的磁盘块。
rm 命令做的事情非常简单:删除目录项,将对应 inode 的硬链接计数减 1。只有同时满足以下两个条件,文件才会被真正删除、数据块才会被释放:
- inode 的硬链接计数为 0(没有任何文件名指向它);
- 进程打开计数为 0(没有任何进程持有该文件的句柄)。
3. 本次故障的完整链路
- 运维人员手动执行
rm catalina.out删除日志,试图释放空间; rm删除了文件名(目录项),du遍历目录时看不到这个文件了;- 但 Tomcat 进程仍在运行,持续向日志文件写入内容,进程持有文件的打开句柄;
- 硬链接数虽为 0,但进程打开数不为 0,因此磁盘数据块不会被释放;
df统计的是数据块占用,因此仍显示这 17GB 为已用状态。
这就是"文件删了,空间却没释放"的完整原理。
四、解决方案
1. 应急方案:不重启业务,即时释放空间
如果业务不能中断,可以通过进程文件描述符直接截断文件,无需重启 Tomcat,空间立即释放。
原理:/proc/PID/fd/ 是内核提供的进程文件描述符映射,即使文件名已删除,通过描述符仍可以直接操作文件实体。
执行命令:
bash
# 截断标准输出对应的文件描述符
> /proc/31293/fd/1
fd 1 和 fd 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 文件系统原理的考察。理解了文件的存储结构和删除逻辑,再遇到类似的"磁盘空间凭空消失"问题,就能快速定位、从容处理。