Ubuntu Cron Job批处理进程完成后无法自动释放内存问题求解
Ruby批处理服务器内存突增问题排查与解决方案
已确认现象
- 专用批处理EC2实例历史运行稳定,内存峰值长期低于20%,无代码发布、配置变更的前提下突发内存持续上涨至80%-90%
- 执行
killall -o 1h ruby(终止运行时长超过1小时的Ruby进程)后内存直接回落至10%,可确认内存占用来自执行结束后未正常退出/未释放资源的Ruby进程 - 相同Rake任务代码在配置更低的测试环境运行无异常,排除显性代码逻辑内存泄漏的可能
核心根因排查方向
按出现概率从高到低排序:
- 孤儿/僵尸Ruby进程残留
绝大多数无变更突发的内存上涨都是这个原因。如果Rake任务中用了并行处理gem、主动fork子进程做异步操作,主进程退出时没有主动wait回收子进程资源,子进程会变成孤儿进程被init进程接管,持续持有内存不释放。生产环境任务数据量、执行频次远高于测试环境,残留进程积累速度快,通常1-2周就会把内存占满;测试环境数据量小、任务执行少,残留进程少很难复现问题。
直接执行ps aux | grep ruby | grep -v grep即可验证,输出中STAT列带Z标记、运行时长远超单批处理任务预期时长的进程就是残留进程。 - Ruby GC未主动归还内存给操作系统
2.x版本及更早的MRI Ruby默认不会主动把GC回收后的空闲内存还给操作系统,加上glibc的内存池机制,如果生产实例内存规格远大于测试环境,Ruby进程会持续持有已申请的内存页,哪怕任务逻辑执行完、对象已经被回收,进程常驻内存也不会下降。
验证方式:找一个已经执行完核心逻辑的Ruby进程,执行kill -CONT <进程PID>手动触发全量GC,如果进程内存占用明显下降,就属于这个问题。 - 进程阻塞在收尾流程无法正常退出
如果任务逻辑里存在无超时的IO操作——比如第三方接口调用无超时、数据库连接死锁、日志写入阻塞,会导致核心逻辑跑完后进程卡在收尾步骤,一直挂在后台占内存。测试环境依赖的内部服务、第三方接口响应稳定,很少出现超时阻塞,因此不会复现问题。
落地解决方案
按优先级执行:
- 临时止血:在现有批处理调度脚本中增加自动回收规则,比如cron任务执行完后追加
pkill -f "ruby.*rake" -o 30m,自动终止运行超过30分钟的残留Ruby进程,快速避免内存打满影响任务运行。 - 修复孤儿进程问题:排查所有Rake任务中的fork、并行执行逻辑,所有子进程执行完成后必须调用
Process.wait回收资源,禁止无资源回收的裸fork操作;如果是第三方并行gem导致的残留,升级到最新稳定版本,或替换为自带进程回收的实现。 - 修复GC不归还内存问题:在Rake任务入口统一配置内存相关参数,强制任务执行完成后回收内存归还系统:
# 在Rakefile最顶部添加以下配置 ENV["MALLOC_ARENA_MAX"] = "2" # 限制glibc内存池数量,大幅降低常驻内存 ENV["RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR"] = "1.2" # 提高老年代GC触发频率 at_exit do GC.start GC.compact if RUBY_VERSION >= "2.7.0" # 任务退出前主动触发全量GC和内存整理 end - 修复进程阻塞问题:给所有外部IO操作加上超时限制:数据库连接配置
connect_timeout参数、HTTP调用设置明确的超时时间、日志输出开启同步写入,避免进程卡在收尾流程无法退出。 - 长期兜底:增加进程运行时长监控,凡是运行时长超过单任务最大预期值的Ruby进程直接触发告警,避免残留进程持续积累。
内容的提问来源于stack exchange,提问作者Dipak Panchal
相关产品推荐
相关产品推荐

