那天凌晨三点,手机突然响起刺耳的告警声。迷迷糊糊抓起来一看,监控系统显示服务器/dev/vda2分区使用率突破95%阈值。作为运维人员,这种深夜告警最让人头皮发麻——这意味着线上服务随时可能因为磁盘写满而崩溃。
我立刻用笔记本连上跳板机,第一反应是执行df -h确认情况。输出结果让我倒吸一口冷气:
bash复制Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 12G 36G 24% /
/dev/vda2 200G 190G 0 100% /data
/data目录是专门存放业务日志和临时文件的挂载点,200G空间居然被吃得干干净净。更诡异的是,业务量最近很平稳,按常理不该出现这种突增。
我的第一反应是业务日志暴增,于是用du命令扫描/data目录:
bash复制du -sh /data/* | sort -rh | head -10
结果出乎意料——最大的日志文件才2G,所有文件加起来也不到50G。这说明空间不是被常规文件占用的,一定有隐藏的"空间杀手"。
这时我想起Linux的一个特性:当进程打开文件后,即便文件被删除,只要进程不退出,磁盘空间也不会释放。立即执行:
bash复制lsof -n | grep deleted
输出让我恍然大悟:
bash复制java 12345 user 1w REG 253,2 8589934592 123456 /data/temp.log (deleted)
java 12345 user 2w REG 253,2 8589934592 123457 /data/error.log (deleted)
几十个被标记为"deleted"的日志文件,每个都占用了数GB空间。这些文件被Java进程打开后,虽然程序调用了删除操作,但由于文件句柄未释放,空间始终被占用。
通过ps -ef | grep 12345确认,这些进程是我们自研的数据处理服务。查看代码发现开发人员使用了如下写法:
java复制FileWriter writer = new FileWriter("temp.log");
// 业务处理代码
writer.close();
问题就出在这里——如果业务处理过程中抛出异常,close()方法不会被调用,导致文件句柄泄漏。日积月累,这些"僵尸文件"最终吃光了所有空间。
面对已经爆满的磁盘,我采取了以下步骤:
kill -9终止问题进程df -h确认空间释放:bash复制/dev/vda2 200G 30G 170G 15% /data
为了防止问题复发,我们实施了以下措施:
java复制try (FileWriter writer = new FileWriter("temp.log")) {
// 业务代码
}
bash复制lsof -n | grep deleted | wc -l
当进程打开文件时,内核会维护一个"文件描述符"结构体,包含文件位置、权限等信息。即使文件被unlink(删除),只要还有进程持有描述符,文件数据块就不会真正释放。这就是所谓的"delete-but-not-released"状态。
我们后来用strace工具跟踪了问题进程的系统调用:
bash复制openat(AT_FDCWD, "/data/temp.log", O_WRONLY|O_CREAT, 0666) = 3
unlink("/data/temp.log") = 0
write(3, "data..."..., 4096) = 4096
可以看到文件在打开后立即被删除,但进程仍在持续写入。这种模式常见于临时文件处理场景。
bash复制ncdu /data
这个交互式工具能直观显示目录大小分布
bash复制df -ih
有时候空间没满但inode耗尽也会引发类似问题
对于顽固的磁盘占用问题,可以:
auditd监控文件系统事件fatrace实时跟踪文件访问bpftrace编写自定义的磁盘监控脚本记得有一次我们发现某台机器每隔几天就会磁盘爆满,最后用bpftrace脚本捕获到是某个定时任务在疯狂生成core dump文件。这种问题不用系统级工具根本无从查起。
经过这次教训,我们团队制定了新的运维规范:
最近半年,类似的磁盘告警再没出现过。这也让我明白,好的运维不是天天救火,而是通过制度和工具把问题消灭在萌芽状态。