小唐那天接到一个告警:一个后台服务的内存曲线在缓慢爬升,从启动时的两百兆,两天涨到了八百兆。
重启服务,内存回落,但过两天又涨回去。典型的慢速内存泄漏。小唐打开代码,开始凭感觉加检测。他在几个可疑的函数入口和出口分别记录了 psutil.Process().memory_info().rss,对比进出前后的差值。跑了一轮,每个函数的进出差值都在几兆以内,看起来谁都不像罪魁祸首。但服务整体就是在涨。
他又换了另一种方式,在代码里到处插入 sys.getsizeof(),检查大对象的大小。结果更离谱------sys.getsizeof返回的数字和进程实际占用的内存对不上,有些明显在膨胀的数据结构,getsizeof报出来的值几乎没变。
两天过去,内存还在涨,小唐还在猜。
同事看了一眼他的代码,说:"你别用 psutil 和 getsizeof 来回凑了。把 tracemalloc 打开,跑一段时间,取两个快照对比一下,它会告诉你哪一行代码分配的内存变多了。"
小唐照做了:
ini
import tracemalloc
tracemalloc.start()
# ... 服务运行一段时间 ...
snapshot1 = tracemalloc.take_snapshot()
# ... 再运行一段时间 ...
snapshot2 = tracemalloc.take_snapshot()
for stat in snapshot2.compare_to(snapshot1, "lineno")[:10]:
print(stat)
输出里排在第一行的,是一个他从未怀疑过的地方:一个缓存字典,每次请求都往里塞一个键,但从来没有清理逻辑。tracemalloc 精确指出了这个字典是在哪一行被创建的,以及两次快照之间它增长了多少字节。
"我之前用 psutil 看进程总内存,差值淹没在噪音里;用 getsizeof 看对象大小,又看不到引用和分配源头。tracemalloc 直接告诉我是哪一行在漏。"
他问同事:"那 memory_profiler 呢?我搜内存分析的时候也看到它,和 tracemalloc 是一回事吗?"
不是。这两个工具回答的是不同的问题:tracemalloc 回答"Python 在什么地方分配了多少内存",memory_profiler 回答"代码执行到每一行时,进程内存大概是多少"。
先看一个最直观的对比
同一个函数,两种工具的输出形态完全不同。
scss
import tracemalloc
def make_data():
return [list(range(1000)) for _ in range(500)]
tracemalloc.start()
data = make_data()
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics("lineno")[:3]:
print(stat)
tracemalloc 的输出是按代码行聚合的分配统计:
ini
/path/script.py:5: size=38.2 MiB, count=501, average=78.1 KiB
它告诉你:第 5 行分配了 38.2 MiB,涉及 501 次分配,平均每次 78.1 KiB。
memory_profiler 用同样的代码,输出是一张逐行内存增量表:
scss
Line # Mem usage Increment Line Contents
================================================
4 45.2 MiB 45.2 MiB def make_data():
5 83.1 MiB 37.9 MiB return [list(range(1000)) for _ in range(500)]
它告诉你:执行第 5 行之前进程用了 45.2 MiB,执行之后用了 83.1 MiB,增量约 37.9 MiB。
两者的数字可能接近,但含义完全不同。 tracemalloc 的 38.2 MiB 是"Python 解释器记录到的、由这一行触发的对象分配总量";memory_profiler 的 37.9 MiB 是"进程 RSS 在这一行前后发生了多大变化"。它们碰巧接近,但在复杂程序里,这两个数字可以差出几倍甚至一个数量级。
核心区别一:测量的是什么
tracemalloc 在 Python 解释器的内存分配器层面挂钩。每当 Python 分配或释放一个内存块,tracemalloc 记录下这次操作的大小和调用栈。它测量的是Python 对象占用的内存。
这意味着:
- 它能精确告诉你"哪个文件的哪一行分配了多少字节"。
- 它可以通过两次快照的差异,找到"什么在增长"。
- 它对纯 C 库通过
malloc直接分配的内存是看不见 的。一个numpy数组的数据缓冲区如果是由底层 C 库直接分配的,tracemalloc可能只记录到 Python 包装对象本身的开销,而不是整个数组的字节数。官方文档的说法很明确:它追踪的是 Python 分配的内存块。对于 NumPy 等扩展,情况取决于它们是否使用了 Python 的内存分配接口;如果绕过了,tracemalloc就追踪不到那部分数据。
memory_profiler 的机制完全不同。它通过轮询操作系统(通常借助 psutil),读取整个进程的常驻内存集(RSS) 。它不关心内存是 Python 解释器分配的,还是 C 扩展分配的,还是内存映射文件占用的。它只关心:进程此刻向操作系统要了多少内存。
这个差异带来一个关键后果:memory_profiler 的数字里包含了 Python 已经释放、但还没有归还给操作系统的内存。 Python 的内存分配器为了性能,不会在每次 del 或作用域结束时立刻把内存还给 OS,而是保留在内部空闲池里。memory_profiler 看到的是 RSS,这些"已释放但未归还"的内存仍然算在里面。tracemalloc 追踪的是分配/释放事件本身,已经释放的对象不会再出现在它的统计里。
所以你会看到同一段代码,memory_profiler 报 82MB,tracemalloc 只报 36MB 的情况------这不是谁算错了,是它们测量的对象本来就不是同一个东西。
核心区别二:输出粒度与使用方式
tracemalloc 的核心用法是快照对比。你在一个时间点拍一张快照,过一段时间再拍一张,然后比较两者:
ini
snapshot1 = tracemalloc.take_snapshot()
# ... 运行 ...
snapshot2 = tracemalloc.take_snapshot()
for stat in snapshot2.compare_to(snapshot1, "lineno")[:10]:
print(stat)
输出会按增长量排序,告诉你哪些行在两次快照之间增长了最多。这是排查慢速泄漏最直接的方式:你不需要知道"总共用了多少",只需要知道"什么在变多"。
tracemalloc 也可以直接查看当前所有分配的统计(statistics("lineno")),但它的真正威力在对比。单次快照只能告诉你"现在内存里有什么",两次对比才能告诉你"什么不该多却多了"。
memory_profiler 的核心用法是逐行装饰 。你给一个函数加上 @profile,用 python -m memory_profiler script.py 运行,它会输出每一行执行前后的 RSS 变化:
python
from memory_profiler import profile
@profile
def process():
a = [i for i in range(1000000)] # 这一行 RSS 涨了多少
b = sum(a) # 这一行 RSS 变了多少
del a # 这一行 RSS 降了多少
return b
它的输出是一张"行号---当前RSS---增量---代码"的表。你一眼就能看到哪一行让进程内存跳了一大截。
这两种粒度的差别决定了它们的适用场景:tracemalloc 适合"我不知道哪里在漏,让工具告诉我增长点";memory_profiler 适合"我大概知道是哪个函数,想看函数内部哪一行最吃内存"。
核心区别三:开销,差了一个数量级
这是选型时最实际的约束。
tracemalloc 的开销相对可控。它需要在每次分配时记录调用栈,这有成本,但通常在可接受范围内。社区中的经验值大致是:分配密集型负载下 CPU 开销约 15--25%,内存开销可能增加数倍(因为它要保存每次分配的追踪记录),但整体仍被认为"可以在生产或准生产环境临时打开"的量级。Python 官方文档也建议尽可能早地启动追踪(通过 PYTHONTRACEMALLOC 环境变量或 -X tracemalloc),并且可以通过控制保存的栈帧深度来平衡精度和开销。
memory_profiler 的开销要高得多。它需要不停地轮询进程 RSS,而且这种采样本身并不便宜。常见的说法是:内存分析可以让代码慢上 10 到 100 倍 ,具体取决于采样频率和函数调用密度。在 BLAS 密集型或高频调用的代码上,20 倍以上的减速是常见的。这意味着 memory_profiler 基本只能在开发阶段、小规模测试数据上使用,不适合直接挂到生产环境去观察一个已经上线的慢速泄漏。
这个开销差异直接决定了小唐的排查路径:他的服务已经在线上了,内存是慢慢涨的,他不可能让一个 20 倍减速的 profiler 挂在生产进程上跑两天。tracemalloc 的低开销和快照对比能力,才是匹配"在线慢速泄漏"的工具。
坑一:用 memory_profiler 追踪 numpy 的大数组分配
很多人第一反应是用 memory_profiler 看 pandas 或 numpy 操作在哪一行吃了内存。但它有一个根本限制:它只能通过轮询 OS 来观察进程地址空间的大小,而 NumPy 等库并不总是通过 Python 的内存分配接口来申请内存 。这意味着某些情况下,memory_profiler 对 numpy 内部大块分配的归因是不精确的------它能看到 RSS 涨了,但不一定能正确地把增长归到"创建这个数组"的那一行 Python 代码上。
tracemalloc 对纯 C 分配的局限同样存在,但它在 Python 层面的归因更可靠:至少它记录的分配点就是 Python 解释器实际分配包装对象和内部缓冲的位置(如果扩展库使用了 Python 的分配接口)。对于"Python 对象在泄漏"的场景,tracemalloc 的快照对比往往比 memory_profiler 的行级 RSS 增量更精准。
坑二:用 tracemalloc 看"进程总内存"
tracemalloc 的数字通常小于 你从 psutil 或 top 看到的进程 RSS。原因不止一个:它只追踪 Python 分配的部分;它不包含已释放但未归还 OS 的内存;它不包含 C 库直接 malloc 的大块内存。
所以你不能用 tracemalloc 的输出直接回答"我的进程为什么占了 1GB"。它能回答的是"Python 对象分配在什么位置增长了",而不是"进程地址空间的全部构成"。如果你需要理解进程级的总体内存,psutil 的 RSS 是起点;要分解 RSS,tracemalloc 负责 Python 层,剩下的需要其他工具(比如 memory_profiler 的行级采样、objgraph 的引用分析,或者针对 C 扩展的专门工具)。
什么时候用 tracemalloc,什么时候用 memory_profiler?
判断标准很清晰:
怀疑有慢速内存泄漏,需要定位"哪一行代码在持续分配" :用 tracemalloc。快照对比是排查泄漏的标准手段,开销可接受,可以在接近真实运行的环境中运行一段时间。
已经知道是哪个函数,想知道函数内部每一行的内存增量 :用 memory_profiler。逐行表直观,适合在开发阶段对小规模输入做一次性的函数级分析。
生产环境临时诊断 :优先 tracemalloc。它的开销相对可控,而且不需要安装第三方包(标准库自带)。memory_profiler 的 10--100 倍减速通常不适合直接挂生产。
分析 numpy/pandas 的大块内存归属 :两者都有局限。tracemalloc 能给出 Python 分配点的可靠归因,但对 C 层数据缓冲区可能低估;memory_profiler 能看到 RSS 变化,但行级归因在 C 扩展密集的代码上可能失真。通常需要结合两者,或者用专门针对数值栈的工具(如 memory_profiler 配合 --precision 调整采样,或使用 filprofiler 等专门工具)。
需要零依赖、标准库方案 :tracemalloc 是唯一选择。memory_profiler 必须 pip install。
一个正确的排查流程
小唐最终的流程是这样的:
第一步,确认是否真的是 Python 对象在泄漏。 用 psutil 观察进程 RSS 的长期趋势,确认内存单调上升。然后打开 tracemalloc,在服务启动稳定后取快照 A,运行数小时后再取快照 B,用 compare_to 看增长排名。
第二步,如果增长排名指向明确的 Python 代码行,直接审查那一行的逻辑(通常是缓存未清理、全局列表追加、循环引用等),修复后用同样的快照对比验证。
第三步,如果增长排名不明确,或者怀疑是 C 扩展的内存 ,用 memory_profiler 在测试环境对可疑函数做逐行分析,观察 RSS 增量集中在哪一段。注意用 --precision 控制采样精度,避免在高频函数上产生海量噪音。
第四步,如果两者都指向模糊 ,用 objgraph 查看对象引用图,或者用 gc.get_objects() 按类型统计数量增长,缩小范围。
核心原则是:先定位"什么在涨",再决定"用哪个工具深入"。 不要一上来就挂 memory_profiler 到生产上,那样你得到的是减速和噪音,不是答案。
回到小唐的故事
小唐用 tracemalloc 的快照对比定位到了那个只增不删的缓存字典。修复方式很简单:给缓存加一个最大容量,或者用 functools.lru_cache 替代手写的字典。
他后来在文档里写了一条:"tracemalloc 是标准库里的分配追踪器,用快照对比找增长点,适合排查慢速泄漏,开销相对可控。memory_profiler 是第三方逐行 RSS 采样器,适合在开发阶段看一个函数内部哪一行吃内存,开销大(10--100倍),别挂生产。两者测量的不是同一个东西:一个看 Python 分配,一个看进程 RSS。先想清楚你要回答的是'什么在涨'还是'哪一行涨了',再选工具。"
内存分析的工具选择,不是"哪个更准"的问题。tracemalloc 和 memory_profiler 站在两个不同的观测点上:一个从解释器内部看分配事件,一个从操作系统层面看进程足迹。用对了,它们互相补充;用错了,你会在两个对不上的数字之间反复怀疑自己。