问题背景
生产环境一台日志汇聚服务器(CentOS 7.9,数据盘 200GB,挂载在 /data)负责接收十几个业务模块的应用日志,跑着一个自研的 Java 日志收集进程和 rsyslog。凌晨 4 点半,磁盘使用率告警从 90% 一路蹿到 98%,值班同事按惯例上机清理:找到 /data/applog/ 下一个 62GB 的 collector.log,直接 rm -f 删掉,告警却迟迟不恢复。早上 8 点交接班时磁盘仍然显示 98%,新日志开始写入失败,部分业务模块报 No space left on device,问题升级到我这里。
故障现象
上机第一件事就看磁盘:
|
|
但用 du 统计目录实际占用,怎么算都对不上:
|
|
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 找被删除但仍被打开的文件:
|
|
一目了然:PID 12867 的 Java 日志收集进程还拿着 collector.log 的写句柄(FD 4,w 表示写模式),NLINK 0 表示目录项已删,SIZE/OFF 显示这个"幽灵文件"已经涨到 66GB——不但没释放,进程还在往里持续写,所以删完之后 df 不降反升。
第三步:确认进程为什么攥着句柄不放
看这个 Java 进程的启动方式和日志配置,发现它用的是自研的文件 Appender,打开日志文件后就再也没有重开逻辑。运维侧曾经给它配过 logrotate,翻 /etc/logrotate.d/collector:
|
|
问题就在这:这份 logrotate 配置既没有 copytruncate,也没有 postrotate 里通知进程重开文件。logrotate 默认行为是把 collector.log 改名成 collector.log.1 再建新文件,但 Java 进程的 FD 还指向改名后的旧 inode,继续往旧文件里写;轮转形同虚设,日志越滚越大。翻历史记录,这个配置是三个月前加的,也就是说这个文件其实已经"假轮转"了三个月,最终膨胀成 62GB 被人工 rm,又因为句柄未释放变成幽灵占用。
第四步:安全释放空间
磁盘只剩 4GB,等不了发版修代码,需要立刻止血。不能直接 kill -9——日志收集进程内存里还有未落盘的缓冲,粗暴杀掉会丢日志。有两个安全做法:
方法一(首选):通过 /proc 直接截断这个已删除文件,不用动进程:
|
|
执行完再看 df:
|
|
66GB 空间瞬间回来,业务写入恢复。进程完全无感知,只是它后续写入的偏移量还在 66GB 处——ext4 会把前面的空洞做成稀疏文件,不再占实际块,问题不大,但这个进程终归要在低峰期重启一次,让它重开一个干净的日志文件。
解决方案
止血动作汇总:
lsof +L1定位持有已删除文件句柄的进程与 FD;truncate -s 0 /proc/<pid>/fd/<fd>原地截断,立即归还空间,避免重启进程;- 当晚低峰重启日志收集进程,消除稀疏文件残留。
根治动作:
- 修正 logrotate 配置,对无重开日志能力的进程加
copytruncate:
|
|
- 推动研发在下个版本给自研 Appender 加上
SIGUSR1信号重开日志文件的能力,之后可切回postrotate+ 信号的标准方案(copytruncate在复制窗口内有极小概率丢日志,只是过渡); - 给全组同步操作规范:清理大日志一律先
> file截断或走轮转,不要直接rm正在被写入的文件。
根因分析
三层原因叠加:
- 直接原因:
rm删除了仍被进程打开的日志文件,inode 引用未归零,数据块不释放,形成"df 与 du 差 60GB"的幽灵占用; - 深层原因:logrotate 配置缺少
copytruncate/postrotate重开机制,对一个不会自己重开日志的进程做了三个月的无效轮转,把单文件养到 62GB; - 流程原因:值班清理手册里没有"确认文件是否被进程占用"这一步,直接
rm成了肌肉记忆。
预防措施
- 监控加一层:磁盘告警规则里增加
df与du差值检查(差值超过 10% 触发提示"存在已删除未释放文件"),并把lsof +L1输出接入巡检脚本; - 轮转配置审计:批量检查所有 logrotate 配置,凡目标进程无日志重开能力的,必须有
copytruncate或postrotate通知逻辑,纳入配置上线 checklist; - 单文件体积兜底:日志目录加单文件大小告警(如超过 5GB 即告警),避免"假轮转"静默养大文件;
- 操作规范落地:值班手册明确"删大文件前先
lsof确认占用;正在写入的文件用truncate/>截断而非rm"。
总结
这次故障本身不复杂,却是 Linux 运维里辨识度最高的经典题:rm 只删目录项,不删被打开的 inode,df 和 du 的差值就是那些"删而未放"的幽灵文件。两条命令就能定位——lsof +L1 找元凶,truncate -s 0 /proc/<pid>/fd/<fd> 无损止血,全程不用重启进程。更值得记住的是链路上游:一份没有 copytruncate 的 logrotate 配置对着不会重开文件的进程空转了三个月,才是把 62GB 巨型日志"养"出来的真正推手。磁盘问题的排查顺序可以固化为:df -h 看块 → df -i 看 inode → du 对账 → 差值大就 lsof +L1,四步走完,九成磁盘疑难都能落地。