记一次 PDF 处理 Bug 引发的磁盘爆满事故

记一次 PDF 处理 Bug 引发的磁盘爆满事故

一个看似无害的异常日志,因定时任务的疏忽差点让服务器沦陷。


一、背景

最近线上运行着一个处理 PDF 文件的服务,通过定时任务(Crontab)周期性执行,负责解析用户上传的 PDF 并提取关键信息。某天突然收到服务器磁盘告警,登录服务器时卡顿严重,甚至 SSH 连接都反复超时,凭借经验——大概率是磁盘写满了。


二、排查过程

1. 艰难地登入服务器

在网络延迟和频繁超时的夹缝中,终于以“龟速”登上服务器。第一件事就是确认磁盘:

df -h

输出如下(类似):

Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1       40G   40G   0   100% /

40G 的系统盘被吃干榨净。接下来找出元凶。

2. 定位大文件

du -sh /* | sort -hr | head -5

很快,/tmp 目录以几十 G 的体量高居榜首。进入 /tmp,发现大量以日期时间命名的日志文件,类似 pdf_error_20240515.log,单文件竟有十几 G。

3. 日志内容分析

查看日志,清一色的 PDF 解析异常堆栈:

java.io.IOException: Error reading PDF ... 
Caused by: org.apache.pdfbox.exception.WrappedIOException ...

而且日志时间间隔非常密集,几乎每秒都在疯狂写入。至此,初步判断是 PDF 解析服务在定时任务中反复重启运行,且把异常日志定向到了 /tmp

4. 追踪定时任务

查看 crontab:

crontab -l

发现了类似这样的一条:

*/1 * * * * cd /opt/pdf-processor && python process.py

再查看 process.py,其内部调用了一个 Java PDF 解析命令,大致逻辑如下:

import os
os.system("java -jar pdf-parser.jar /data/pdfs/ > /tmp/pdf_error_$(date +%Y%m%d).log")

问题顿时明朗——


三、根因分析

  1. 没有使用 &nohup 守护
    定时任务每分钟触发一次,但由于脚本内命令是前台阻塞运行且耗时不短,上次任务未结束,下次任务又启动,导致多个实例同时写同一个日志文件。

  2. 异常日志无限追加
    > 重定向每次打开都会截断,但这里用了带日期的动态文件名,导致每天生成一个文件。然而当 PDF 批量处理时,单个异常日志就足以撑满磁盘。

  3. PDF 解析本身存在 Bug
    对于某些损坏或非标准格式的 PDF,解析库持续抛出异常,进程并没有退出,而是继续“卡死”在异常循环中,持续写日志,形成恶性循环。

  4. 日志未轮转,目录选择不当
    /tmp 一般是系统盘,空间有限;且没有配置 logrotate 或脚本内部的日志清理逻辑。


四、解决方案

应急处理

# 删除 /tmp 下的大日志文件(紧急时可直接 rm)
rm -rf /tmp/pdf_error_*.log
# 立即杀掉所有残留进程
pkill -f pdf-parser

磁盘恢复后,服务恢复正常。

根治方案

1. 修复 PDF 解析 Bug

  • 完善异常处理,捕获所有 IOException 并记录有限次错误后主动退出。
  • 增加超时机制(如处理单文件超过 5 分钟则跳过)。

2. 优化定时任务脚本

  • 使用 flock 或 lock file 确保单实例运行:
*/5 * * * * flock -n /tmp/pdf.lock -c 'cd /opt/pdf-processor && python process.py'
  • 将任务间隔从每分钟延长至合理周期(如 5 分钟),并增加任务完成检查。

3. 日志规范化

  • 不要将日志写在 /tmp,应放在独立挂载点或应用目录。
  • 使用 >> 追加 + logrotate 轮转,或直接集成到 Log4j / Logback 等框架。
  • 当错误日志达到一定阈值(如 100M)时,主动清空或停止写。

4. 监控与告警

  • 添加磁盘使用率告警(如 >80% 通知)。
  • 对关键进程数量、日志文件大小进行监控。
  • 实施 /tmp 目录大小上限策略(如 systemd tmpfiles.d 限制)。

五、经验总结

  • 任何会写日志的定时任务都必须考虑日志爆炸:磁盘 I/O 不仅能拖慢系统,还能让服务器直接不可达。
  • 避免在 /tmp 下存储长期运行的结果:很多 Linux 发行版会对 /tmp 定期清理,但不足以预防突发写入。
  • “有 & 无 &”的教训:虽然本次不是 & 的问题,但类似场景中,如果不将长任务放入后台或使用 nohup,Crontab 会因等待而阻塞后续触发,引发进程堆积。
  • 异常处理不是可有可无的装饰:生产中任何一个未捕获的异常都可能成为压死骆驼的最后一根稻草。

这次的磁盘爆满事件,幸好发生在测试环境(或是及时被发现),但足以让我们警醒——魔鬼总是在细节中。分享出来,希望大家不要再踩同一个坑。

posted @ 2026-05-19 16:55  BeginnerY  阅读(18)  评论(0)    收藏  举报