Locust 协程模型下,如何避免全局变量导致的内存泄漏
解读
国内互联网公司在高并发压测场景普遍采用 Locust 的 gevent 协程模型,单台压测机可轻松模拟数万并发。协程切换由用户态完成,全局变量(list、dict、自定义对象、缓存、日志句柄、数据库连接池等)在 Locust 进程生命周期内常驻,若持续追加数据而不清理,内存曲线呈“只增不降”,压测 10 分钟后 OOM 被系统 kill,导致报告失真、面试直接挂。面试官想确认候选人是否理解“协程共享地址空间 + 非 GC 即时释放”这一关键点,并给出工程级、可落地的防泄漏方案。
知识点
- Python 垃圾回收机制:引用计数为主,分代回收为辅;循环引用与全局引用无法立即释放。
- gevent 协程特点:单进程、单线程、协程栈 < 10 KB,所有协程共享全局命名空间;内存泄漏会线性叠加。
- Locust 运行时序:test_start → 多 User 类实例化 → 协程并发 → 任务循环 → test_stop;全局变量在 test_start 或模块顶层初始化,test_stop 不一定被触发(手动停止或异常退出)。
- 常见泄漏源:
- 累积型 list/dict(如 self.client.response_list 误写成全局 response_list)
- 单例日志句柄未 rotate,FileHandler 缓存无限增大
- 自定义缓存、统计容器、图片/响应体快照
- 数据库/Redis 连接池被循环引用,导致连接对象无法释放
- 诊断工具:tracemalloc、objgraph、memory_profiler、Locust WebUI 的“Download Stats”+“Memory”曲线、Linux pmap。
- 防御策略:最小化全局、使用局部变量 + 结果队列、定时裁剪、弱引用、进程级隔离、自定义 cleanup hook、Docker 内存限制 + 重启策略。
答案
线上实战按“三步走”落地:
第一步:定位
在 Locustfile 顶部加三行代码,5 秒采样一次,压测 3 分钟即可看出泄漏点:
import tracemalloc, gc, threading
tracemalloc.start(25)
def _snap():
while True:
snapshot = tracemalloc.take_snapshot()
top = snapshot.statistics('lineno')[:10]
for t in top:
print(t)
threading.Event().wait(5)
threading.Thread(target=_snap, daemon=True).start()
若看到某全局 list 或 dict 持续增长,即可锁定。
第二步:改造
- 取消全局累积:把“全局结果容器”改为 Locust 内置的
events.request_success/fire+stats_exporter,或自定义queue.Queue定时消费落盘,消费后立刻queue.task_done()并del大对象。 - 缩小作用域:User 类内部用实例变量,任务结束主动
self.__dict__.clear();跨 User 共享数据用 multiprocessing.Manager 或外部时序数据库,而不是进程内全局 dict。 - 弱引用缓存:若必须缓存对象,用
weakref.WeakValueDictionary,当对象无强引用时自动弹出。 - 日志 rotate:给 FileHandler 加
RotatingFileHandler(maxBytes=50*1024*1024, backupCount=5),防止日志对象无限增长。 - 连接池复用:数据库连接池声明为
singleton + weakref.finalize,在test_stop事件里显式close();若 Locust 异常退出,依赖 Docker 的--memory=4g --restart=on-failure:3兜底重启,避免机器被拖垮。
第三步:验证
重新跑 30 分钟稳定性场景,通过 pmap -x <pid> | tail -1 观察 RSS 是否趋于平稳;同时 tracemalloc 快照 Top10 不再出现业务代码中的全局变量,即可上线。
拓展思考
- 如果压测集群采用多进程分布式(–processes 8),全局变量泄漏会乘以 8,此时应把“结果聚合”外置到 Redis Stream 或 Kafka,压测机本身做到无状态,重启即可自愈。
- 在 4 核 8 G 的容器里,单 Locust 进程常驻 2 w 协程,内存基线约 600 M;若 10 分钟涨到 2 G,基本可以判定泄漏,不必等到 OOM。可封装成 Prometheus exporter,内存增量 > 30 % 自动报警。
- 对异步接口压测时,很多工程师用
asyncio.Queue与 gevent 混用,导致事件循环对象循环引用;应统一使用 gevent.queue,避免两种协程模型交叉。 - 面试加分项:提到“在 CI 阶段加入 memory-leak-gate,用 Locust + docker stats 做回归,内存斜率 > 5 M/min 就拒绝合并”,可体现 DevOps 思维。