磁盘空间为何删了大日志也不释放?一次 df 与 du 相差 60GB 的排查记录

删除大日志后 df 仍显示磁盘爆满,du 与 df 相差 60GB,根因是进程仍持有已删除文件句柄,用 lsof +L1 定位并安全释放。

问题背景

生产环境一台日志汇聚服务器(CentOS 7.9,数据盘 200GB,挂载在 /data)负责接收十几个业务模块的应用日志,跑着一个自研的 Java 日志收集进程和 rsyslog。凌晨 4 点半,磁盘使用率告警从 90% 一路蹿到 98%,值班同事按惯例上机清理:找到 /data/applog/ 下一个 62GB 的 collector.log,直接 rm -f 删掉,告警却迟迟不恢复。早上 8 点交接班时磁盘仍然显示 98%,新日志开始写入失败,部分业务模块报 No space left on device,问题升级到我这里。

故障现象

上机第一件事就看磁盘:

1
2
3
$ df -h /data
Filesystem      Size  Used Avail Use% Mounted on
/dev/vdb1       197G  193G  4.2G  98% /data

但用 du 统计目录实际占用,怎么算都对不上:

1
2
$ du -sh /data
131G    /data

df 说用了 193GB,du 说只有 131GB,凭空差了 60 多GB——恰好接近被删掉的那个大日志的体积。更诡异的是:

  • 那个 62GB 的 collector.log 确实已经不在了,ls 看不到;
  • 再删其他小日志文件,df 的 Used 一点都不降;
  • df -i 看 inode 只用了 3%,排除 inode 耗尽(那是另一类问题,之前写过);
  • 业务持续报写入失败,rsyslog 队列开始堆积。

删了文件、空间却不还回来,这 60GB 去哪了?

排查过程

第一步:确认不是文件系统统计误差

先排除低级可能。df 统计的是文件系统超级块里的分配信息,du 是遍历目录树累加文件大小,两者天然会有少量差异(预留块、元数据),但 5% 预留块(ext4 默认)对 197GB 盘也就 10GB 左右,且这台机器早就 tune2fs -m 1 调到 1% 了。60GB 的差距绝不是统计口径问题。

第二步:想到"已删除但仍被打开"的文件

Linux 下 rm 删除文件,本质是把目录项(dentry)和 inode 的链接数减一。如果还有进程持有这个文件的文件描述符,inode 的引用计数不为零,内核就不会真正释放数据块——文件从目录里消失了,但磁盘空间还被占着。这正是 df(看块分配)和 du(看目录树)出现巨大差值的最经典原因。

lsof 找被删除但仍被打开的文件:

1
2
3
$ lsof +L1 | awk '$7 > 1073741824'
COMMAND   PID  USER   FD   TYPE DEVICE   SIZE/OFF NLINK    NODE NAME
java    12867 applog  4w   REG  253,16 66571993088     0  524291 /data/applog/collector.log (deleted)

一目了然:PID 12867 的 Java 日志收集进程还拿着 collector.log 的写句柄(FD 4,w 表示写模式),NLINK 0 表示目录项已删,SIZE/OFF 显示这个"幽灵文件"已经涨到 66GB——不但没释放,进程还在往里持续写,所以删完之后 df 不降反升。

第三步:确认进程为什么攥着句柄不放

看这个 Java 进程的启动方式和日志配置,发现它用的是自研的文件 Appender,打开日志文件后就再也没有重开逻辑。运维侧曾经给它配过 logrotate,翻 /etc/logrotate.d/collector

1
2
3
4
5
6
/data/applog/collector.log {
    daily
    rotate 7
    compress
    missingok
}

问题就在这:这份 logrotate 配置既没有 copytruncate,也没有 postrotate 里通知进程重开文件。logrotate 默认行为是把 collector.log 改名成 collector.log.1 再建新文件,但 Java 进程的 FD 还指向改名后的旧 inode,继续往旧文件里写;轮转形同虚设,日志越滚越大。翻历史记录,这个配置是三个月前加的,也就是说这个文件其实已经"假轮转"了三个月,最终膨胀成 62GB 被人工 rm,又因为句柄未释放变成幽灵占用。

第四步:安全释放空间

磁盘只剩 4GB,等不了发版修代码,需要立刻止血。不能直接 kill -9——日志收集进程内存里还有未落盘的缓冲,粗暴杀掉会丢日志。有两个安全做法:

方法一(首选):通过 /proc 直接截断这个已删除文件,不用动进程:

1
2
3
4
5
6
# 先看看确认目标
$ ls -l /proc/12867/fd/4
l-wx------ 1 applog applog 64 Jul 30 08:41 /proc/12867/fd/4 -> /data/applog/collector.log (deleted)

# 截断为 0,空间立即归还
$ truncate -s 0 /proc/12867/fd/4

执行完再看 df

1
2
$ df -h /data
/dev/vdb1       197G  128G   69G  66% /data

66GB 空间瞬间回来,业务写入恢复。进程完全无感知,只是它后续写入的偏移量还在 66GB 处——ext4 会把前面的空洞做成稀疏文件,不再占实际块,问题不大,但这个进程终归要在低峰期重启一次,让它重开一个干净的日志文件。

解决方案

止血动作汇总:

  1. lsof +L1 定位持有已删除文件句柄的进程与 FD;
  2. truncate -s 0 /proc/<pid>/fd/<fd> 原地截断,立即归还空间,避免重启进程;
  3. 当晚低峰重启日志收集进程,消除稀疏文件残留。

根治动作:

  1. 修正 logrotate 配置,对无重开日志能力的进程加 copytruncate
1
2
3
4
5
6
7
/data/applog/collector.log {
    daily
    rotate 7
    compress
    missingok
    copytruncate
}
  1. 推动研发在下个版本给自研 Appender 加上 SIGUSR1 信号重开日志文件的能力,之后可切回 postrotate + 信号的标准方案(copytruncate 在复制窗口内有极小概率丢日志,只是过渡);
  2. 给全组同步操作规范:清理大日志一律先 > file 截断或走轮转,不要直接 rm 正在被写入的文件

根因分析

三层原因叠加:

  • 直接原因rm 删除了仍被进程打开的日志文件,inode 引用未归零,数据块不释放,形成"df 与 du 差 60GB"的幽灵占用;
  • 深层原因:logrotate 配置缺少 copytruncate/postrotate 重开机制,对一个不会自己重开日志的进程做了三个月的无效轮转,把单文件养到 62GB;
  • 流程原因:值班清理手册里没有"确认文件是否被进程占用"这一步,直接 rm 成了肌肉记忆。

预防措施

  1. 监控加一层:磁盘告警规则里增加 dfdu 差值检查(差值超过 10% 触发提示"存在已删除未释放文件"),并把 lsof +L1 输出接入巡检脚本;
  2. 轮转配置审计:批量检查所有 logrotate 配置,凡目标进程无日志重开能力的,必须有 copytruncatepostrotate 通知逻辑,纳入配置上线 checklist;
  3. 单文件体积兜底:日志目录加单文件大小告警(如超过 5GB 即告警),避免"假轮转"静默养大文件;
  4. 操作规范落地:值班手册明确"删大文件前先 lsof 确认占用;正在写入的文件用 truncate/> 截断而非 rm"。

总结

这次故障本身不复杂,却是 Linux 运维里辨识度最高的经典题:rm 只删目录项,不删被打开的 inode,dfdu 的差值就是那些"删而未放"的幽灵文件。两条命令就能定位——lsof +L1 找元凶,truncate -s 0 /proc/<pid>/fd/<fd> 无损止血,全程不用重启进程。更值得记住的是链路上游:一份没有 copytruncate 的 logrotate 配置对着不会重开文件的进程空转了三个月,才是把 62GB 巨型日志"养"出来的真正推手。磁盘问题的排查顺序可以固化为:df -h 看块 → df -i 看 inode → du 对账 → 差值大就 lsof +L1,四步走完,九成磁盘疑难都能落地。

使用 Hugo 构建
主题 StackJimmy 设计