要在不改动一行代码的情况下剖析 Python 内存占用,只需把 OpenResty XRay 挂到运行中的进程上。它自研的 Python GC 对象内存分布火焰图能精确指出到底是哪些对象、模块和字典占用了最多内存,并给出通向每一个的完整数据引用路径。

下面我们在一个未经修改的 gunicorn 进程上实时演示这套分析——无需 @profile 装饰器,也无需重启——再展示能持续自动暴露内存大户的报告。Python 代码中的内存泄漏和大内存占用,从此不再是谜!

找出哪个 Python 进程最占内存

运行 top 命令检查内存使用情况。可以看到,名为 gunicorn 的 Python 进程占用内存最多——RSS 超过 600MB。

top 输出显示 gunicorn Python 进程占用最多内存,RSS 超过 600MB

运行 ps 查看更多细节。这里它使用的是 Linux 发行版自带的标准 Python 3 二进制(/usr/bin/python3),运行着一个 order_service 的 gunicorn WSGI 应用。

ps 输出显示 gunicorn 进程运行的是标准的 /usr/bin/python3 二进制

用引导式分析剖析 Python 内存:定位最大的对象及其所属模块

OpenResty XRay 实时检查这个未经修改的进程。打开 Web 控制台,确认正在观察的机器无误,进入 Guided Analysis(引导式分析)页面,这里可以选择 OpenResty XRay 能诊断的各类问题。

OpenResty XRay 引导式分析的问题类型,包含 High memory usage

选择 High memory usage,选中 Python 应用以及 top 标记出的那个 gunicorn 进程(占用近 600MB 的那个),保持 PythonC/C++ 两个语言级别都选中,然后开始。经过一两轮分析后,OpenResty XRay 会自动生成一份报告。

这是现在我们要分析的问题类型,内存。

OpenResty XRay 报告中当前分析的问题类型:内存(High memory usage)

我们可以看到大部分内存是由 Libc 分配器分配的,这里超过了 580 MB。

报告显示大部分内存由 libc 分配器分配,超过 580MB

这是占用内存最多的 Python GC 对象的引用路径。很明显,这个 Python 虚拟机使用了 Libc 分配器来请求内存。

OpenResty XRay 报告:占内存最多的 #1 Python GC 对象引用路径,570MB 字典由 prev_processor 模块的 order_name_cache 持有

这个 Python 字典用于保存所有已加载的 Python 模块。

Python 解释器保存所有已加载模块的字典

这是一个名为 order_service.service.order.prev_processor 的 Python 模块。

引用路径中的 Python 模块 order_service.service.order.prev_processor

在这个模块中,有一个名为 order_name_cache 的字段。

prev_processor 模块中名为 order_name_cache 的字段

这个字段的值是一个 Python 字典。

order_name_cache 字段的值是一个 Python 字典

点击查看更多细节。

点击展开 order_name_cache 字典的更多细节

这个数据引用路径,是从这个 Python GC 对象内存分布火焰图中自动推导出来的。

Python GC 对象内存分布火焰图,高亮通向 order_name_cache 的最热调用栈

下面是对问题更详细的解释和建议。它提到了我们之前看到的 prev_processor 模块。

OpenResty XRay 报告解读:逐层拆解通向 order_name_cache 字典的 Python 引用路径

它也提到了 order_name_cache 字典。

回到数据引用路径。点击这个图标,来复制这个模块名称。

在数据引用路径上点击图标复制模块名称

在终端上,使用 find 命令来查找 Python 源文件。

在终端使用 find 命令查找 Python 源文件

粘贴我们刚刚复制的模块名称。这里,我们借用模块名称中的点,作为 grep 命令的通配符。

粘贴模块名称,借用其中的点作为 grep 通配符

复制完整的文件路径。使用 vim 编辑器打开这个源文件。您可以使用任何您喜欢的编辑器。

复制完整文件路径并用 vim 打开源文件

我们可以找到之前报告里提到的 order_name_cache 变量。

在源文件中找到报告提到的 order_name_cache 变量

我们也可以检查这个变量是如何使用的,以确定是否存在内存泄漏问题。

prev_processor.py 源码:order_name_cache 是模块级字典,handle() 对每个订单写入且从不清理

order_name_cache 是一个模块级字典,handle() 每处理一个订单就往里写入一条新条目,而没有任何地方会移除条目。正是这种无上限的增长,让进程占用了数百 MB 内存——一个经典的 Python 内存泄漏,现在你可以直接在源头修复它。

用定时报告自动监控 Python 内存占用

OpenResty XRay 也可以自动监控在线进程并展示分析报告。在 Insights 页面,你可以找到以日和周为周期的报告——同样的内存引用路径会自动浮现——所以你不必每次都手动运行引导式分析,尽管引导式分析对于应用开发和演示仍然很有用。

OpenResty XRay Insights 日报自动呈现占内存最多的 Python 引用路径

常见问题

如何在不修改代码的情况下剖析 Python 内存占用?

把 OpenResty XRay 挂到已经运行的进程上,启动一次 “High memory usage” 引导式分析即可。它直接读取运行中的 Python 进程——无需 @profile 装饰器、无需插桩、也无需重启——所以你可以在生产环境的 gunicorn 或任何未经修改的 Python 程序对外提供服务的同时完成剖析。

哪个 Python 对象或模块占用了最多内存?

引导式分析会生成 Python GC 对象内存分布火焰图,并推导出通向最重对象的数据引用路径。在本例中,它直接指向了 order_service.service.order.prev_processor 模块里的 order_name_cache 字典——正是这个字段占用了数百 MB 内存。

如何找出一个 Python 进程中最大的对象?

OpenResty XRay 会按对象保留的内存大小排序,并给出从模块一路到具体字典或值的完整引用路径。你不必再用 sys.getsizeof 一个个对象去猜,而是直接得到最大的内存占用者以及它在代码中的位置。

可以自动监控 Python 内存占用吗?

可以。“Insights” 页面会按计划对在线进程运行同样的分析,生成日报和周报,反复出现的内存大户会自动浮现,无需你每次手动发起引导式分析。

关于 OpenResty XRay

OpenResty XRay 是一个动态追踪产品,它可以自动分析运行中的应用,以解决性能问题、行为问题和安全漏洞,并提供可行的建议。在底层实现上,OpenResty XRay 由我们的 Y 语言驱动,可以在不同环境下支持多种不同的运行时,如 Stap+、eBPF+、GDB 和 ODB。

关于作者

章亦春是开源 OpenResty® 项目创始人兼 OpenResty Inc. 公司 CEO 和创始人。

章亦春(Github ID: agentzh),生于中国江苏,现定居美国湾区。他是中国早期开源技术和文化的倡导者和领军人物,曾供职于多家国际知名的高科技企业,如 Cloudflare、雅虎、阿里巴巴, 是 “边缘计算“、”动态追踪 “和 “机器编程 “的先驱,拥有超过 22 年的编程及 16 年的开源经验。作为拥有超过 4000 万全球域名用户的开源项目的领导者。他基于其 OpenResty® 开源项目打造的高科技企业 OpenResty Inc. 位于美国硅谷中心。其主打的两个产品 OpenResty XRay(利用动态追踪技术的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最适合微服务和分布式流量的全能型网关软件),广受全球众多上市及大型企业青睐。在 OpenResty 以外,章亦春为多个开源项目贡献了累计超过百万行代码,其中包括,Linux 内核、Nginx、LuaJITGDBSystemTapLLVM、Perl 等,并编写过 60 多个开源软件库。

关注我们

如果您喜欢本文,欢迎关注我们 OpenResty Inc. 公司的博客网站 。也欢迎扫码关注我们的微信公众号:

我们的微信公众号

翻译

我们提供了英文版原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!