Python内存泄漏排查实战:从异常栈到内存分析工具

一颗松果
一颗松果
发布时间:2026-10-12| 3人浏览过
原创

本文总结了一次Python服务内存泄漏的完整排查过程。通过对比新旧代码与依赖,锁定异常处理逻辑为根源:全局异常实例未回收导致内存持续攀升。文章详细演示了如何利用objgraph和pympler等工具定位泄漏变量,并通过traceback调用链分析确认问题,最终提供修复方案及进阶排查思路。

首先明确本次问题的具体现象:

  • 1. 服务于13号完成一次上线发布,但从23日起,内存使用量持续上升并触发预警。重启实例后,内存攀升的速度反而加快。
  • 2. 该服务同时部署在A、B两种不同芯片架构上。除模型推理部分外,预处理和后处理逻辑共享同一套代码。结果显示,B芯片服务器出现内存泄漏告警,而A芯片服务器运行完全正常。

排查思路一:分析新旧源码及二方库依赖差异

基于上述两个关键条件,初步推测13号的版本更新引入了问题。更新可能涉及以下两个层面:

  • 自研业务代码
  • 二方依赖库代码

从这两个角度展开排查:

  • 一方面,分别使用Git对比历史版本信息,并借助BeyondCompare工具对比两个版本的源码差异。重点审查了A、B两款芯片各自独立处理的代码段,均未发现明显异常。
  • 另一方面,通过执行pip list命令对比两个镜像包中的二方依赖包,发现仅有pytz时区工具库的版本发生了变更。

经过深入分析,认为该依赖包导致内存泄漏的可能性极低,因此暂时搁置此线索。

至此,试图通过对比新旧版本源码差异来定位内存泄漏问题的路径似乎陷入僵局。

排查思路二:监测新旧版本内存变化差异

目前Python社区常用的内存检测工具包括pympler、objgraph、tracemalloc 等。

首先,利用objgraph工具对新旧服务中的TOP50变量类型进行了观察和统计。

objgraph的常用命令如下:

# 全局类型数量
objgraph.show_most_common_types(limit=50)

# 增量变化
objgraph.show_growth(limit=30)


为了更直观地观测变化曲线,我对上述功能进行了简单封装,使统计数据直接输出到csv文件中,便于后续观察。

stats = objgraph.most_common_types(limit=50)
stats_path = "./types_stats.csv"
tmp_dict = dict(stats)
req_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
tmp_dict['req_time'] = req_time
df = pd.DataFrame.from_dict(tmp_dict, orient='index').T

if os.path.exists(stats_path):
    df.to_csv(stats_path, mode='a', header=True, index=False)
else:
    df.to_csv(stats_path, index=False)


如下图所示,使用一批正常图片在新旧两个版本上运行了1个小时,系统状态非常稳定,各类变量的数量没有任何波动。

此时,联想到自己通常在转测或上线前,会准备一批异常格式的图片进行边界验证测试。

虽然这些异常情况在上线前肯定已经由测试同学验证过,但抱着死马当活马医的心态,还是顺手用这批异常图片进行了测试。

平静的数据曲线就此被打破,如下图红框所示:dict、function、method、tuple、traceback等重要数据类型的数量开始持续攀升。

与此同时,镜像内存也在不断增加,且没有任何收敛的迹象。

由此,虽然无法直接确认这是否就是线上问题的根源,但至少定位到了一个潜在的bug。此时回头检查日志,发现了一个奇怪的现象: 正常情况下,特殊图片导致的异常,日志应该输出如下信息,即check_image_type方法在异常栈中只应打印一次。

但现状是check_image_type方法循环重复打印了多次,且重复次数随着测试次数的增加而变多。

重新审查了这块区域的异常处理代码。

异常类的声明如下:

抛出异常的代码如下:

问题所在

经过思考,大致理清了问题的根源:

这里的每个异常实例实际上被定义为了全局变量,而在抛出异常时,抛出的正是这个全局变量。当此全局变量被压入异常栈并处理完成后,并不会被垃圾回收机制回收。

因此,随着错误格式图片调用次数的不断增加,异常栈中的信息也会不断累积。而且由于异常对象中还包含了请求图片的信息,因此每次这类请求都会导致MB级别的内存增加。

但这部分代码上线已久,如果线上确实是这里导致的问题,为何之前没有任何异常?而且为何在A芯片上也没有出现任何问题?

带着以上两个疑问,我们进行了两项验证:

首先,确认了之前的旧版本以及A芯片上同样会出现此问题。

其次,我们查看了线上的调用记录,发现最近刚好新接入了一位客户,且该客户大量使用类似问题的图片调用某局点(该局点大部分为B芯片)的服务。我们在部分线上实例的日志中也观测到了同样的现象。

由此,上述两个疑问基本得到了解释。修复此bug后,内存溢出问题不再出现。

进阶排查思路

按理说,问题解决到这个地步似乎可以收工了。但我问了自己一个问题:如果当初没有打印这一行日志,或者开发人员偷懒没有把异常栈全部打印出来,那应该如何去定位?

带着这样的问题,我继续研究了objgraph、pympler 工具。

前文已经定位到在异常图片情况下会出现内存泄漏,因此重点来看下此时有哪些异样情况:

通过如下命令,我们可以观察到每次异常出现时,内存中增加了哪些变量以及增加的内存具体情况。

1. 使用objgraph工具

objgraph.show_growth(limit=20)

2. 使用pympler工具

from pympler import tracker
tr = tracker.SummaryTracker()
tr.print_diff()

通过如下代码,可以打印出这些新增变量来自哪些引用,以便进一步分析。

gth = objgraph.growth(limit=20)
for gt in gth:
    logger.info("growth type:%s, count:%s, growth:%s" % (gt[0], gt[1], gt[2]))
    if gt[2] > 100 or gt[1] > 300:
        continue
    objgraph.show_backrefs(objgraph.by_type(gt[0])[0], max_depth=10, too_many=5,
                           filename="./dots/%s_backrefs.dot" % gt[0])
    objgraph.show_refs(objgraph.by_type(gt[0])[0], max_depth=10, too_many=5,
                       filename="./dots/%s_refs.dot" % gt[0])
    objgraph.show_chain(
        objgraph.find_backref_chain(objgraph.by_type(gt[0])[0], objgraph.is_proper_module),
        filename="./dots/%s_chain.dot" % gt[0]
    )


通过graphviz的dot工具,将上面生成的graph格式数据转换成如下图片:

dot -Tpng xxx.dot -o xxx.png


这里,由于dict、list、frame、tuple、method等基本类型数量太多,观测难度较大,因此这里先做了过滤处理。

内存新增的ImageReqWrapper的调用链

内存新增的traceback的调用链:

虽然带着前面的先验知识,使我们很自然地就关注到了traceback和其对应的IMAGE_FORMAT_EXCEPTION异常。 但通过思考为何上面这些本应在服务调用结束后就被回收的变量却没有被回收,尤其是所有的traceback变量在被IMAGE_FORMAT_EXCEPTION异常调用后就无法回收等这些现象;同时再做一些小实验,相信很快就能定位到问题根源。

另,关于 python3中 缓存Exception导致的内存泄漏问题,我们可以看看这篇文章:https://www.jb51.net/article/231759.htm

至此,我们可以得出结论如下:

由于抛出的异常无法被回收,导致对应的异常栈、请求体等变量都无法被回收,而请求体中由于包含图片信息,因此每次这类请求都会导致MB级别的内存泄漏。

另外,研究过程中还发现python3自带了一个内存分析工具tracemalloc,通过如下代码就可以观察代码行与内存之间的关系,虽然可能未必精确,但也能大概提供一些线索。

import tracemalloc

tracemalloc.start(25)
snapshot = tracemalloc.take_snapshot()
global snapshot
gc.collect()
snapshot1 = tracemalloc.take_snapshot()
top_stats = snapshot1.compare_to(snapshot, 'lineno')
logger.warning("[ Top 20 differences ]")
for stat in top_stats[:20]:
    if stat.size_diff < 0:
        continue
    logger.warning(stat)
snapshot = tracemalloc.take_snapshot()

到此这篇关于python内存泄漏排查技巧总结的文章就介绍到这了,更多相关python内存泄漏排查技巧内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

参考文章:

https://testerhome.com/articles/19870?order_by=created_at& https://blog.51cto.com/u_3423936/3019476 https://segmentfault.com/a/1190000038277797 https://www.cnblogs.com/zzbj/p/13532156.html https://drmingdrmer.github.io/tech/programming/2017/05/06/python-mem.html https://zhuanlan.zhihu.com/p/38600861

 

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。