妙手回春:内存泄漏诊断案例分析

存储 存储软件
虽然 Python 自带垃圾回收机制,替开发人员管理内存,并不意味着 Python 程序没有内存泄露之虞。实际上,Python 程序内存泄露问题时有发生——程序跑着跑着,占用内存越来越多,最后只能动用重启大法释放内存……

[[403536]]

本文转载自微信公众号「小菜学编程」,作者fasionchan。转载本文请联系小菜学编程公众号。

虽然 Python 自带垃圾回收机制,替开发人员管理内存,并不意味着 Python 程序没有内存泄露之虞。实际上,Python 程序内存泄露问题时有发生——程序跑着跑着,占用内存越来越多,最后只能动用重启大法释放内存……

由于内存分配回收工作已被 Python 接管,内存泄露问题排查起来相对来说也比较晦涩。正常情况下,引用计数 机制确保对象没有引用时释放,而 标记清除 则解决了 循环引用 的问题,理论上不存在内存泄露的可能性。

那么,Python 程序内存泄露问题一般是如何造成的呢?程序员的失误是其中的主要原因,最常见的是下面两点:

  • 容器泄露 ,使用容器对象存储数据,但数据只进不出,没有清理机制,容器便慢慢变大,最后撑爆内存;
  • __del__ 魔术方法误用,如果对象实现了 __del__ 魔术方法,Python 就无法用标记清除法解决循环引用问题,这必然带来内存泄露风险;

既然内存泄露无法完全避免,当 Python 程序发生内存泄漏时,又该如何排查呢?

本节,我们将以一个简单的案例,详细讲解预防、排查、解决 Python 内存泄露问题的 方法论 。

工欲善其事,必先利其器。在这个过程中,我们将利用一些趁手的工具(例如 objgraph 等)。只有选择正确工具,掌握工具正确使用姿势,才能做到事半功倍。

问题服务

我们以一个存在内存泄露问题的 API 服务( service.py )作为例子,演示定位内存泄露问题的步骤:

  1. import uvicorn 
  2.  
  3. from fastapi import FastAPI 
  4. from faker import Faker 
  5.  
  6. from pyconsole import start_console_server 
  7.  
  8. faker = Faker() 
  9. cache = {} 
  10.  
  11. app = FastAPI() 
  12.  
  13. async def fetch_user_from_database(user_id): 
  14.     return { 
  15.         'user_id': faker.sha256() if user_id == 'random' else user_id, 
  16.         'name': faker.name(), 
  17.         'email': faker.email(), 
  18.         'address': faker.address(), 
  19.         'desc': faker.text(), 
  20.     } 
  21.  
  22. async def get_user(user_id): 
  23.     data = cache.get(user_id) 
  24.     if data is not None: 
  25.         return data 
  26.  
  27.     data = await fetch_user_from_database(user_id) 
  28.     cache[data['user_id']] = data 
  29.  
  30.     return data 
  31.  
  32. @app.get('/users/{user_id}'
  33. async def retrieve_user(user_id): 
  34.     return await get_user(user_id) 
  35.  
  36. if __name__ == '__main__'
  37.     start_console_server() 
  38.     uvicorn.run(app) 

这是一个基于 fastapi 框架编写的 API 服务,它只实现了一个接口:根据用户 ID 获取用户信息。API 服务由 uvicorn 启动,它是一个性能非常优秀的 ASGI 服务器。

为减少数据库访问频率,程序将数据库返回的用户数据,以用户 ID 为索引,缓存在内存中( cache 字典)。注意到,演示服务直接使用 faker 随机生成用户数据,模拟数据库查询,以此消除数据库依赖。

顺便提一下,faker 是一个生成假数据的模块,非常好用。特别是需要测试数据时,完全不用自己绞尽脑汁拼造。

服务还启动了一个远程交互式终端,以便我们可以连上服务进程,并在里面执行一些代码。交互式终端的源码可以在 github 上获得:pyconsole.py ,原理超过本节讨论范围不展开介绍。

由于例子代码非常简单,哪里内存泄露我们甚至仅凭肉眼便可看出。尽管如此,我们假装什么都不知道,来研究解决问题的思路:如何观察程序?如何运用工具来获取一些关键信息?如何分析各个线索?如何逐步接近问题的根源?

运行服务

由于服务依赖几个第三方包,启动它之前请先用 pip 安装这些依赖包,并且确保安装是成功的:

  1. $ pip install uvicorn 
  2. $ pip install fastapi 
  3. $ pip install faker 

直接执行 service.py 即可启动服务,默认它会监听 8000 端口:

  1. $ python service.py 
  2. INFO:     Started server process [76591] 
  3. INFO:     Waiting for application startup. 
  4. INFO:     Application startup complete. 
  5. INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) 

服务启动后,即可通过 8000 端口访问用户信息接口,用户 ID 可以随便给:

  1. $ curl http://127.0.0.1:8000/users/bef76936c7d22e98f3d7b4c7e1aeef524da4ec1b48f871926fee43c5ec071a2d 
  2. {"user_id":"bef76936c7d22e98f3d7b4c7e1aeef524da4ec1b48f871926fee43c5ec071a2d","name":"Patricia Johnson","email":"epatton@yahoo.com","address":"837 Jacobs Field\nGregorybury, ND 81050","desc":"Third choice air together expect account war. Seven dog safe significant. Expect exist wrong finish window there raise. Third blue and cover."

服务接口还支持随机查询,随机返回一个用户的信息:

  1. $ curl http://127.0.0.1:8000/users/random 
  2. {"user_id":"d6a55f04bab8ddec83d651bdca77f7215042b792970482213b6da56a119f18a8","name":"Evan Carter","email":"andrea79@garcia.com","address":"109 Miller Lights Apt. 843\nPort Jamie, IN 97570","desc":"Resource green allow him. Build store enough effect alone. Everybody right remember public coach book not.\nConference respond trip girl."

远程终端

我们直接执行 pyconsole.py ,以默认端口即可连接正在运行中的 API 服务进程:

  1. $ python pyconsole.py 
  2. Python 3.8.5 (default, Aug  5 2020, 18:49:57) 
  3. [GCC 5.4.0 20160609] on linux 
  4. Type "help""copyright""credits" or "license" for more information. 
  5. (ConsoleClient) 
  6. >>> 

pyconsole 用法跟 Python 交互式终端一样,但代码执行环境是在被连接的服务进程里面,因此可以看到服务内部的实时状态。我们先通过 dir 内建函数看看远程终端的名字空间都有些啥:

  1. >>> dir() 
  2. ['__builtins__''__doc__''__name__''main''sys'
  3. >>> main 
  4. <module '__main__' from 'service.py'
  5. >>> dir(main) 
  6. ['Faker''FastAPI''__annotations__''__builtins__''__cached__''__doc__''__file__''__loader__''__name__''__package__''__spec__''app''cache''faker''fetch_user_from_database''get_user''retrieve_user''start_console_server''uvicorn'

main 就是服务的 main 模块,从中还可以找到 service.py 导入的 Faker 、FastAPI 等,它定义的函数 retrieve_user 、get_user 等,还有作为全局变量存在的 cache 字典。甚至,我们还可以看到 cache 当前缓存了多少用户信息:

  1. >>> len(main.cache) 

由于我们前面通过 API 获取了 2 条用户数据,因此 cache 当前缓存了 2 条数据。当我们再次访问接口获取其他用户数据时,我们会看到 cache 缓存的用户数据会慢慢增加:

  1. >>> len(main.cache) 

pyconsole 是一个很神奇的终端,能够实时查看 Python 进程里面各种数据的状态,在排查问题时非常方便!

 

责任编辑:武晓燕 来源: 小菜学编程
相关推荐

2009-01-11 09:29:00

局域网共享拨号

2012-02-22 21:28:58

内存泄漏

2016-03-21 10:31:25

Android内存泄露

2017-11-09 16:07:00

Web应用内存

2018-10-25 15:24:10

ThreadLocal内存泄漏Java

2017-03-20 13:43:51

Node.js内存泄漏

2017-03-19 16:40:28

漏洞Node.js内存泄漏

2010-10-25 10:10:27

ibmdwJava

2020-01-03 16:04:10

Node.js内存泄漏

2012-08-13 10:14:36

IBMdW

2018-09-14 10:48:45

Java内存泄漏

2018-05-09 09:35:13

2023-12-18 10:45:23

内存泄漏计算机服务器

2024-03-11 08:22:40

Java内存泄漏

2019-01-30 18:24:14

Java内存泄漏编程语言

2020-06-08 09:18:59

JavaScript开发技术

2015-03-30 11:18:50

内存管理Android

2016-12-05 16:33:30

2021-08-05 15:28:22

JS内存泄漏

2021-08-09 09:54:37

内存泄漏JS 阿里云
点赞
收藏

51CTO技术栈公众号