Rspec运行500个Spec占用超4GB内存致CircleCI进程被杀原因排查
这种本地跑全量Spec正常,但CI上进程被内存超限干掉的情况真的很棘手,我来分享几个实战中有用的排查方向,帮你定位问题:
拆分Spec范围,缩小排查目标
500+个Spec全量跑很难定位到具体哪部分出问题,建议按模块(比如models、controllers、services)或者标签把Spec分成几组,在CircleCI上分批执行。比如运行rspec spec/models/,看这组的内存占用是否正常,逐步找到内存暴涨的那一批,再进一步单个运行该组里的Spec,锁定到具体的测试用例。在CI中添加内存监控日志
你可以在CircleCI的运行脚本里加入实时内存监控,这样能看到内存是缓慢增长还是突然飙升,以及对应到哪段测试。比如在启动rspec前,先启动一个后台脚本记录内存:# 启动rspec并记录进程ID rspec spec/ & RSPEC_PID=$! # 每隔3秒记录一次内存占用,直到rspec进程结束 while kill -0 $RSPEC_PID 2>/dev/null; do echo "$(date +%Y-%m-%dT%H:%M:%S) - Memory usage: $(ps -o rss= -p $RSPEC_PID) KB" >> memory_log.txt sleep 3 done跑完后把
memory_log.txt作为CI产物保存,下载后就能对应到内存暴涨的时间点和当时运行的Spec。检查测试间的全局状态清理
很多Ruby测试的内存泄漏都源于测试之间没有重置全局状态:- 数据库测试数据没有彻底清理:比如用了
let!创建持久化数据,但没在after(:each)里销毁,或者DatabaseCleaner的策略不对(比如:transaction在某些场景下不生效) - 类变量、全局变量被修改后未重置:比如某个测试修改了
MyClass.class_variable_set(:@@cache, {}),但测试结束后没清空,导致后续测试不断往里面加数据 - 外部服务连接未关闭:比如Redis、Elasticsearch的客户端连接,测试后没有断开,连接池越来越大
可以尝试切换DatabaseCleaner到:truncation策略,或者在after(:suite)里手动重置所有可能的全局状态,看内存是否下降。
- 数据库测试数据没有彻底清理:比如用了
排查Gem或自定义代码的内存泄漏
有些Gem在测试环境下可能存在内存泄漏,比如FactoryBot如果频繁创建大量关联对象且未清理,或者某些日志、监控Gem在测试中积累了大量数据。你可以尝试临时注释掉非核心的Gem,或者禁用部分自定义的单例类、缓存逻辑,再跑测试看内存变化。另外,也可以检查是否有循环引用的对象(比如模型之间的双向关联未正确处理),这类问题在大量测试积累后会导致内存暴涨。在CI中运行轻量内存分析工具
虽然CI资源有限,但可以用memory_profiler这类工具生成内存分析报告。你可以在spec_helper.rb里加入以下代码:if ENV['PROFILE_MEMORY'] require 'memory_profiler' MemoryProfiler.report(top: 30, allow_files: ['spec/', 'app/']) do RSpec::Core::Runner.run(ARGV) end.save('memory_profile.html') end然后在CircleCI的运行命令里加上
PROFILE_MEMORY=true,跑完后把memory_profile.html作为产物下载,分析哪些对象占用了最多的内存,比如是否有大量未被回收的ActiveRecord实例。对齐本地和CI的环境配置
本地和CI的Ruby版本、Gem版本、数据库版本、环境变量可能存在差异,这些差异可能导致内存占用不同。比如CI用的是Ruby 3.2而本地是3.1,或者某个Gem在CI的版本有内存泄漏问题。你可以把本地环境调整得和CI完全一致(比如用相同的Docker镜像),看是否能复现内存超标的问题,这样在本地排查会更高效。
内容的提问来源于stack exchange,提问作者Ahmad Rezk

