“古怪的 Python 内存泄漏”怎么破?

笔者曾经开发过的几个大型 Django 应用程序都在某个时候出现了内存泄漏。Python 进程缓慢地增加它们的内存消耗,直到崩溃。这一点也不好玩。即使自动重新启动进程之后,仍然会有一些宕机问题。

Python 中的内存泄漏通常发生在无限增长的模块级变量中。这可能是一个具有无穷大 maxsize 的 lru_cache 变量,也可能是一个在错误范围内声明的简单列表。

泄漏也不是只有发生在你自己写的代码中才会影响你。例如,看看 BuzzFeed 的 Peter Karp 写的这篇优秀的文章(https://docs.python.org/3/library/tracemalloc.html),他在 Python 的标准库中发现了一个内存泄漏(已经修复了!)

解决方法

下面的解决方法都会在执行了很多请求或任务之后重新启动 worker 进程。这是一个清除任何潜在的无限积累的 Python 对象的简单方法。如果您的 web 服务器、队列 worker 或类似的应用程序有此能力,但还没有被功能化,请告诉我,我会添加它!

即使您现在还没有看到任何内存泄漏,添加这些解决方法也会提高您的应用程序的弹性。

Gunicorn

如果您正在使用 Gunicorn 作为您的 Python web 服务器,您可以使用--max-requests 设置来定期重启 worker。与它的兄弟--max-requests-jitter 配合使用以防止所有 worker 同时重启。这有助于减少 worker 的启动负载。 

例如,在最近的一个项目中,我将 Gunicorn 配置为:

gunicorn --max-requests 1000 --max-requests-jitter 50 ... app.wsgi

对于此项目的流量水平、worker 数量和服务器数量来说,这将大约每1.5小时重新启动 worker。5% 的浮动足以消除重启负载的相关性。

Uwsgi

如果您正在使用 uwsgi,你可以使用它类似的 max-requests 设置。此设置在很多次请求之后,也会重新启动 worker。

例如,在以前的一个项目中,笔者在 uwsgi.ini 文件中像这样使用了这个设置:

[uwsgi]master = truemodule = app.wsgi...max-requests = 500

Uwsgi 还提供了 max-requests-delta 选项用于添加其他浮动。但由于它是一个绝对数字,所以配置起来要比 Gunicorn 更麻烦。如果你更改了 worker 的数量或 max-requests 的值,那你就需要重新计算 max-requests-delta 来保持您的浮动在一个特定的百分比。

Celery

Celery 为内存泄漏提供了几个不同的设置。

首先是 worker_max_tasks_per_child 设置。这将在 worker 子进程处理了许多任务之后重新启动它们。此设置没有浮动选项,但是 Celery 任务的运行时间范围很广,所以会有一些自然的浮动。

例如:​​​​​​​

app = Celery("inventev")app.conf.worker_max_tasks_per_child = 100

或者你正在使用 Django 设置:

CELERY_WORKER_MAX_TASKS_PER_CHILD = 100

100个任务比笔者上面建议的 web 请求数要小一些。在过去,笔者最终为 Celery 使用了较小的值,因为笔者在后台任务中看到了更多的内存消耗。(我想我还在 Celery 本身中遇到了内存泄漏。)

你可以使用的另一个设置是 worker_max_memory_per_child。这指定子进程在父进程替换它之前可以使用的最大千字节内存。它有点复杂,所以我还没用过。

如果你确实使用了 worker_max_memory_per_child 设置,那么你可能应该将其计算为总内存的百分比,并除以每个子进程。这样,如果你更改了子进程的数量或你的服务器的可用内存,它将会自动扩展。例如(未测试):​​​​​​​

import psutilcelery_max_mem_kilobytes = (psutil.virtual_memory().total * 0.75) / 1024app.conf.worker_max_memory_per_child = int(celery_max_mem_kilobytes / app.conf.worker_concurrency)

这使用psutil 来查找整个系统内存。它将最多75%(0.75)的内存分配给Celery,只有在服务器是一个专用 Celery 服务器的情况下你才会需要它。

跟踪泄漏

在 Python 中调试内存泄漏是很不容易的,因为任何函数都可以在任何模块中分配全局对象。它们也可能出现在与 C API 集成的扩展代码中。

笔者用过的一些工具:

  • 标准库模块 tracemalloc.

  • objgraph 和 guppy3 包都是在 tracemalloc 之前完成的,并尝试做类似的事情。它们都不太友好,但我以前成功地使用过它们。

  • Scout APM 用 CPython 的内存分配计数来检测每个“span”(请求、SQL 查询、模板标签,等等)。少数 APM 解决方案能做到这一点。提示:我维护着 Python 集成。

更新 (2019-09-19): Riccardo Magliocchetti在Twitter上提到,pyuwsgimemhog 项目可以解析 uwsgi 日志文件,并告诉您哪些路径正在泄漏内存。很简洁!

其他一些有用的博文:

  • Buzzfeed Tech撰写了一篇《如何在生产Python web服务上使用tracemalloc 的指南》。

  • Fugue 的文章(https://www.fugue.co/blog/diagnosing-and-fixing-memory-leaks-in-python.html)也使用了 tracemalloc。

  • 在 Benoit Bernard 的“古怪的 Python 内存泄漏”帖子中,(https://benbernardblog.com/tracking-down-a-freaky-python-memory-leak/#monitoringmemoryusingperformancemonitor}他使用各种工具来跟踪C层面的泄漏。

结束啦!

祝您内存泄漏更少!

—Adam

英文原文:https://adamj.eu/tech/2019/09/19/working-around-memory-leaks-in-your-django-app/

Python ,go,k8s资料请添加Amywechat:17812796384

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章