Web项目内存持续上涨需先用psutil监测RSS是否线性增长,再针对性启用tracemalloc增量快照;重点排查异步任务未await、弱引用误用及隐蔽全局容器。

Web项目内存持续上涨,不是缓存预热就是真泄漏;先用 psutil 确认 RSS 是否线性增长,再决定要不要动 tracemalloc。
确认是不是真泄漏:看 RSS 趋势而非 Python 对象数
很多“内存涨了”其实是误报:JIT 预热、mmap 映射、C 扩展(如 numpy、PIL)或共享库占的内存,tracemalloc 根本看不见。真正该盯的是进程实际驻留集(RSS):
- 用
psutil.Process().memory_info().rss每 30 秒采一次,画 2 小时曲线——如果斜率稳定上升,且业务请求量没变,才值得深挖 - 别只看
objgraph.count('dict')增加了 100 个:Python 启动就带几千个 dict,关键看它是否随请求数等比例累积 - 对比不同环境:同一份代码在 A/B 芯片上 RSS 行为不一致?优先查二方包差异(比如 pytz 升级后内部缓存策略变更)
精准捕获泄漏源头:tracemalloc 必须配增量快照
tracemalloc.start() 全程开着只会被框架底层噪音淹没;Web 场景必须“请求前后拍两帧”,否则看到的全是 urllib3/response.py:217 这类无关分配点:
- 在中间件里加轻量钩子:只对带
?debug=mem的请求启用,避免线上性能抖动 - 第一次快照放请求进入前:
snapshot1 = tracemalloc.take_snapshot() - 第二次快照放响应已发出后(确保所有局部变量已出作用域),再用
snapshot2.compare_to(snapshot1, 'lineno') - 过滤掉干扰路径:
filter_traces([tracemalloc.Filter(True, '*myapp/*')]),聚焦自己写的模块
定位持有者而非分配点:objgraph 查引用链比看分配行更有效
tracemalloc 告诉你“谁 new 的”,但泄漏根因是“谁一直 hold 着不放”。比如一个 requests.Response 被塞进全局 CACHE,分配发生在 response.py,但罪魁祸首是你的 cache.py:
立即学习“Python免费学习笔记(深入)”;
- 先用
objgraph.show_growth(limit=10)找出请求前后暴增的类型(比如MyDataClass从 0 → 1200) - 挑一个实例:
leaked = objgraph.by_type('MyDataClass')[0],再跑objgraph.show_backrefs([leaked], max_depth=4) - 生成的图里箭头指向的就是“持有者”:大概率是某个 module-level list、未解绑的信号槽、或闭包里意外捕获的
self - 特别注意
functools.cache或手动 dict 缓存——如果 key 含时间戳/UUID,缓存永不命中,只增不删
容易被忽略的三个硬伤
排查时最常卡在这三点,不是工具不会用,而是逻辑盲区:
- 异步任务没 await 或没 cancel:
asyncio.create_task()启动的协程若挂起,其栈帧和局部变量会一直活着,tracemalloc也抓不到(它只管 Python 对象分配,不管 event loop 状态) - 弱引用误用:
weakref.WeakKeyDictionary的 key 被其他地方强引用了一次,整个 value 就永远不释放;用objgraph.get_referents()检查 key 是否真“弱” - 全局容器名太隐蔽:不是所有缓存都叫
CACHE,可能是_pending_tasks、__listeners或 class-level 的instances = [],得搜append(、add(、setdefault(这些动作点


















