我将根据我最近的经验扩展 Brett 的答案。 Dozer package 是 well maintained,尽管取得了一些进步,例如在 Python 3.4 中将 tracemalloc
添加到 stdlib,但它的 gc.get_objects
计数图表是我解决内存泄漏的首选工具。下面我使用dozer > 0.7
,它在撰写本文时尚未发布(好吧,因为我最近在那里贡献了几个修复)。
示例
让我们看一个重要的内存泄漏。我将在这里使用Celery 4.4 并最终发现一个导致泄漏的功能(并且因为它是一种错误/功能类型的东西,它可以被称为纯粹的错误配置,由于无知)。所以有一个 Python 3.6 venv 我pip install celery < 4.5
。并有以下模块。
demo.py
import time
import celery
redis_dsn = 'redis://localhost'
app = celery.Celery('demo', broker=redis_dsn, backend=redis_dsn)
@app.task
def subtask():
pass
@app.task
def task():
for i in range(10_000):
subtask.delay()
time.sleep(0.01)
if __name__ == '__main__':
task.delay().get()
基本上是一个调度一堆子任务的任务。会出什么问题?
我将使用procpath
来分析 Celery 节点的内存消耗。 pip install procpath
。我有 4 个终端:
-
procpath record -d celery.sqlite -i1 "$..children[?('celery' in @.cmdline)]"
记录 Celery 节点的进程树统计信息
-
docker run --rm -it -p 6379:6379 redis
运行 Redis,它将作为 Celery 代理和结果后端
-
celery -A demo worker --concurrency 2
使用 2 个工作人员运行节点
-
python demo.py
最终运行示例
(4) 将在 2 分钟内完成。
然后我使用sqliteviz (pre-built version) 来可视化procpath
的记录器。我将celery.sqlite
放在那里并使用此查询:
SELECT datetime(ts, 'unixepoch', 'localtime') ts, stat_pid, stat_rss / 256.0 rss
FROM record
在 sqliteviz 中,我使用X=ts
、Y=rss
创建了一个折线图跟踪,并添加了拆分变换By=stat_pid
。结果图为:
任何与内存泄漏作斗争的人都可能非常熟悉这种形状。
查找泄漏对象
现在是dozer
的时间了。我将展示非仪器案例(如果可以的话,您可以以类似的方式检测您的代码)。要将 Dozer 服务器注入目标进程,我将使用 Pyrasite。有两点需要了解:
- 要运行它,ptrace 必须配置为“经典 ptrace 权限”:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
,这可能存在安全风险
- 目标 Python 进程崩溃的可能性非零
需要注意的是:
-
pip install https://github.com/mgedmin/dozer/archive/3ca74bd8.zip
(我上面提到的应该是0.8)
-
pip install pillow
(dozer
用于制图)
pip install pyrasite
之后我可以在目标进程中获取 Python shell:
pyrasite-shell 26572
并注入以下内容,这将使用 stdlib 的 wsgiref
的服务器运行 Dozer 的 WSGI 应用程序。
import threading
import wsgiref.simple_server
import dozer
def run_dozer():
app = dozer.Dozer(app=None, path='/')
with wsgiref.simple_server.make_server('', 8000, app) as httpd:
print('Serving Dozer on port 8000...')
httpd.serve_forever()
threading.Thread(target=run_dozer, daemon=True).start()
在浏览器中打开http://localhost:8000
应该会看到如下内容:
之后,我再次从 (4) 运行 python demo.py
并等待它完成。然后在 Dozer 中,我将“Floor”设置为 5000,这就是我看到的:
与 Celery 相关的两种类型随着子任务的调度而增长:
celery.result.AsyncResult
vine.promises.promise
weakref.WeakMethod
具有相同的形状和数字,一定是由同一事物引起的。
寻找根本原因
此时,从泄漏类型和趋势来看,您的情况可能已经很清楚了。如果不是,Dozer 对每种类型都有“TRACE”链接,它允许跟踪(例如查看对象的属性)选择的对象的引用者 (gc.get_referrers
) 和引用者 (gc.get_referents
),然后再次遍历图继续该过程。
但是一张图片说一千个字,对吧?所以我将展示如何使用objgraph
来呈现所选对象的依赖关系图。
pip install objgraph
apt-get install graphviz
然后:
- 我再次从 (4) 运行
python demo.py
- 在推土机中我设置了
floor=0
,filter=AsyncResult
- 然后点击“TRACE”,应该会产生
然后在 Pyrasite shell 中运行:
objgraph.show_backrefs([objgraph.at(140254427663376)], filename='backref.png')
PNG 文件应包含:
基本上有一些Context
对象包含一个名为_children
的list
,而该对象又包含许多celery.result.AsyncResult
的实例,这些实例会泄漏。在 Dozer 中更改 Filter=celery.*context
是我所看到的:
所以罪魁祸首是celery.app.task.Context
。搜索该类型肯定会引导您找到Celery task page。在那里快速搜索“孩子”,它是这样说的:
trail = True
如果启用,请求将跟踪此任务启动的子任务,并且此信息将与结果一起发送 (result.children
)。
通过设置trail=False
来禁用跟踪:
@app.task(trail=False)
def task():
for i in range(10_000):
subtask.delay()
time.sleep(0.01)
然后从 (3) 重新启动 Celery 节点,从 (4) 重新启动 python demo.py
,显示此内存消耗。
问题解决了!