Python tracemalloc 的线上内存快照采集

2次阅读

tracemalloc 必须在 python 入口第一行启动,否则无法捕获框架初始化阶段的内存分配;快照需启用 traceback_limit=25 并用 statistics(‘traceback’) 对比,过滤 site-packages 干扰,结合 gc 和 objgraph 定位真实泄漏源。

Python tracemalloc 的线上内存快照采集

tracemalloc 启动时机必须早于所有业务代码

线上内存问题往往在服务启动几小时后才暴露,但 tracemalloc 如果晚于框架初始化(比如 djangosetup()fastapiapp = FastAPI())再启用,就捕获不到关键对象的分配源头。它只记录启用后的分配行为,之前的所有内存分配完全不可见。

常见错误现象:get_traced_memory() 返回值很小,take_snapshot() 里几乎全是 <frozen importlib._bootstrap></frozen> 或空路径,说明没抓到业务逻辑的分配链。

  • 在 Python 解释器最顶层入口(如 main.py 开头第一行)调用 tracemalloc.start()
  • 避免放在 if __name__ == "__main__": 块内——WSGI/ASGI 服务器(如 gunicorn)可能不走这个分支
  • 若用 systemd 或容器启动,确认 PYTHONPATH 和入口文件加载顺序,防止被中间层 wrapper 覆盖

快照采集要带 traceback 且限制深度

默认 take_snapshot() 不包含调用,只返回每个分配地址的大小,基本没法定位到哪行代码触发了内存增长。必须显式传参启用追踪,但又不能无限制记录——线上服务扛不住全栈帧开销。

使用场景:排查某个接口反复调用后 RSS 持续上涨,需要对比两次快照中新增的 top 分配点。

立即学习Python免费学习笔记(深入)”;

  • 启动时加参数:tracemalloc.start(traceback_limit=25)(25 是实测平衡点,再高 GC 压力明显上升)
  • 采集快照务必用:snapshot = tracemalloc.take_snapshot(),不要省略括号
  • 避免在请求处理中高频调用 take_snapshot() —— 单次耗时约 1–5ms,QPS 高时会拖慢响应
  • 如果用 psutil.Process().memory_info().rss 发现内存涨了 100MB,但快照里最大单条才 2MB,说明有大量小对象累积,此时需用 Filter_traces 聚合相同 traceback

对比快照时别直接用 statistics('lineno')

statistics('lineno') 按「文件+行号」聚合,看似直观,但对动态生成代码(Jinja 模板、SQLAlchemy lazy loader、Pydantic model 实例化)极不友好——同一行可能对应 N 个语义完全不同的分配源,数据严重失真。

性能影响:当 trace 数超 10 万,statistics() 内部排序会吃掉 200+MB 临时内存,可能触发 OOM。

  • 优先用 statistics('traceback'),能保留函数名和上下文层级
  • 做 diff 时用 snapshot1.compare_to(snapshot2, 'traceback'),而非手动遍历 stat.traceback
  • 过滤干扰项:加上 Filter(inclusive=False, filename_pattern="*/site-packages/*") 排除第三方包噪音
  • 如果发现 top 项全是 dict.__init__list.__init__,说明问题在上层业务逻辑反复构造容器,而不是底层 C 扩展泄漏

线上跑着不能只靠 tracemalloc 定结论

它只能告诉你“哪些 Python 对象被分配了”,但无法区分是引用未释放、循环引用未被 GC 回收、还是底层 C 扩展(如 numpy Array、PIL Image)持有的独立内存块。线上看到 RSS 涨了 500MB,tracemalloc 只统计出 80MB,剩下那 420MB 就得换工具查。

容易被忽略的地方:gunicorn worker 进程复用导致 tracemalloc 累积统计跨请求,而 get_traced_memory() 返回的是当前总分配量,不是增量。你看到的“涨了 50MB”可能是前 1000 个请求的累计,不是最近一次请求干的。

  • 配合 gc.get_stats() 看分代回收频率,若 collected 长期为 0,说明有对象逃逸出 GC 范围
  • objgraph.show_most_common_types(limit=20) 快速看存活对象类型分布(需提前装 objgraph)
  • 对疑似 C 扩展内存,用 malloc_info()(Python 3.4+)或 cat /proc/PID/smaps_rollupAnonymousHeap 区域
text=ZqhQzanResources