Python aiohttp内存泄漏排查与tracemalloc实战
本文记录了一次使用aiohttp进行异步开发时遇到的内存泄漏问题。通过复现高并发POST请求场景,发现multidict库特定版本存在内存未释放现象。文章详细演示了如何利用Python标准库tracemalloc定位内存增长源头,最终定位至web_response.py第56行及CIMultiDict类,并指出该问题已在multidict 4.5.2版本中修复。
近期工作中开始深入使用Python协程相关技术,涉及aiohttp、aiomysql、aioredis等模块。在实际应用过程中遇到诸多问题,本文重点整理一次内存泄漏问题的排查与解决过程。
在常规Python开发中,内存管理往往容易被忽视(当然这可能也受限于个人技术深度),相比之下C/C++开发者对此更为敏感。然而,一旦Python程序出现内存泄漏,处理起来将极为棘手。
最近的一次代码实践中便遭遇了此类问题,庆幸的是最终确认并非业务代码逻辑错误,而是所依赖的第三方库存在内存管理缺陷。下文将通过简化代码模拟该内存异常现象,并完整记录排查解决步骤,旨在为遇到同类问题的开发者提供参考。
一、问题复现
此次问题源于使用aiohttp构建接口服务。复现过程相对直观:我们构建一个接收POST请求的服务端,并配合一个高并发客户端进行访问,以此观察内存占用变化。
注意:该问题出现在特定库版本中:multidict==4.5.1,。在撰写本文两小时前,作者已发布4.5.2版本修复此内存问题,经测试验证该版本已无泄漏现象。
服务端代码实现如下:
from aiohttp import web
async def hello(request):
return web.json_response(await request.json())
app = web.Application()
app.add_routes([web.post('/', hello)])
web.run_app(app)
客户端并发测试代码实现如下:
import asyncio
import aiohttp
async def foo(times):
data = {'foo': 1}
async with aiohttp.ClientSession() as session:
for x in range(times):
resp = await session.post('http://localhost:8080', json=data)
if not x % 100:
print(await resp.json())
loop = asyncio.get_event_loop()
loop.run_until_complete(foo(100000))
loop.close()
由于运行环境为Linux或macOS,可通过htop工具实时监控程序内存占用。启动服务端后,初始内存占用极低。
启动客户端后再次观察,发现内存持续攀升,即便客户端运行结束,内存也未随之释放。

客户端停止后的内存状态截图显示:
若客户端持续运行,内存将无限增长直至耗尽系统资源,最终导致进程被系统强制终止。

二、内存泄漏排查过程
上述示例程序逻辑简单,仅涉及基本的POST请求处理。但在实际复杂项目中,业务逻辑繁多,定位内存泄漏点并非易事。初期尝试了memory_profiler、objgraph及graphviz等工具,均未快速定位问题。最终发现Python标准库中的tracemalloc(文档地址:https://docs.python.org/3/library/tracemalloc.html)提供了有效解决方案。
借助该模块,我迅速定位到了内存泄漏的具体位置。
参照官方文档示例,我对代码进行了改造以验证泄漏源,修改后的服务端代码如下:
from aiohttp import web
import tracemalloc
async def hello(request):
return web.json_response(await request.json())
async def get_info(request):
snapshot2 = tracemalloc.take_snapshot()
top_stats = snapshot2.compare_to(snapshot1, 'lineno')
print(top_stats)
return web.Response(text="ok")
if __name__ == '__main__':
app = web.Application()
app.add_routes(
[
web.post('/', hello),
web.get("/get_info", get_info)
]
)
tracemalloc.start()
snapshot1 = tracemalloc.take_snapshot()
web.run_app(app)
需重点关注print(top_stats)的输出结果。
新增get_info路由,启动服务端并运行客户端。客户端结束后内存依然上涨且不释放。此时通过浏览器访问get_info路由,查看控制台打印内容。输出结果按内存增长量排序,前几项即为重点关注对象(因数据量大,此处仅展示部分前缀数据)。
<StatisticDiff traceback=<Traceback (<Frame filename='/Users/zhaofan/anaconda3/lib/python3.6/site-packages/aiohttp/web_response.py' lineno=56>,)> size=116500672 (+116500672) count=300004 (+300004)>, <StatisticDiff traceback=<Traceback (<Frame filename='/Users/zhaofan/anaconda3/lib/python3.6/site-packages/aiohttp/web_response.py' lineno=604>,)> size=11400000 (+11400000) count=200000 (+200000)>, <StatisticDiff traceback=<Traceback (<Frame filename='/Users/zhaofan/anaconda3/lib/python3.6/site-packages/aiohttp/web_response.py' lineno=472>,)> size=8000000 (+8000000) count=100000 (+100000)>, <StatisticDiff traceback=<Traceback (<Frame filename='/Users/zhaofan/anaconda3/lib/python3.6/site-packages/aiohttp/web_response.py' lineno=353>,)> size=5500000 (+5500000) count=100000 (+100000)>, <StatisticDiff traceback=<Traceback (<Frame filename='/Users/zhaofan/anaconda3/lib/python3.6/site-packages/aiohttp/web_response.py' lineno=352>,)> size=5300608 (+5300608) count=100001 (+100001)>,
以第一行数据为例,清晰显示web_response.py第56行代码导致内存增长最大。对于复杂项目,此方法可快速定位内存溢出源头。深入查看该行代码:

追踪至最终行,问题指向CIMultiDict类。
class CIMultiDict(MultiDict): def _title(self, key): return key.title()
观察发现CIMultiDict继承自MultiDict,由此推断问题根源在于MultiDict相关实现。
进一步确认,问题出在MultiDict库本身,该项目维护地址为:https://github.com/aio-libs/multidict。
查阅该库仓库时,发现已有其他用户报告相同问题,并提交了对应的Bug报告。
具体Issue链接为:https://github.com/aio-libs/multidict/issues/307
值得称赞的是,国外开发者响应迅速,aio-libs团队很快回应并修复了该问题。在最新版本4.5.2中,经测试已确认不再存在内存泄漏。
三、总结
此次排查过程暴露出我在问题查找方法及解决思路上的不足。但经过此次实践,未来再遇类似问题时,能够更快速地定位与处理。
同时,强烈建议开发者熟悉https://docs.python.org/3/library/tracemalloc.html中介绍的tracemalloc库,它在内存调试方面极具价值。
以上为本次Python内存泄漏问题排查与解决过程的分享,希望能为读者提供帮助。如有任何疑问,欢迎留言交流,小编将及时回复。感谢大家对本站的支持。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













