【问题标题】:How do I find what is using memory in a Python process in a production system?如何在生产系统的 Python 进程中找到正在使用内存的内容?
【发布时间】:2010-09-13 13:41:38
【问题描述】:

我的生产系统偶尔会出现内存泄漏,我无法在开发环境中重现。我在开发环境中使用了 Python memory profiler(特别是 Heapy)并取得了一些成功,但它无法帮助我解决无法重现的问题,而且我不愿意使用 Heapy 来检测我们的生产系统,因为它需要一段时间才能完成它的工作,而且它的线程化远程接口在我们的服务器中无法正常工作。

我想我想要的是一种方法来转储生产 Python 进程(或至少 gc.get_objects)的快照,然后离线分析它以查看它在哪里使用内存。 How do I get a core dump of a python process like this?一旦有了,我该怎么做?

【问题讨论】:

  • 你的生产平台是什么,*nix 还是 NT?

标签: python memory-leaks coredump


【解决方案1】:

使用 Python 的 gc 垃圾收集器接口和 sys.getsizeof() 可以转储所有 Python 对象及其大小。以下是我在生产环境中用于解决内存泄漏问题的代码:

rss = psutil.Process(os.getpid()).get_memory_info().rss
# Dump variables if using more than 100MB of memory
if rss > 100 * 1024 * 1024:
    memory_dump()
    os.abort()

def memory_dump():
    dump = open("memory.pickle", 'wb')
    xs = []
    for obj in gc.get_objects():
        i = id(obj)
        size = sys.getsizeof(obj, 0)
        #    referrers = [id(o) for o in gc.get_referrers(obj) if hasattr(o, '__class__')]
        referents = [id(o) for o in gc.get_referents(obj) if hasattr(o, '__class__')]
        if hasattr(obj, '__class__'):
            cls = str(obj.__class__)
            xs.append({'id': i, 'class': cls, 'size': size, 'referents': referents})
    cPickle.dump(xs, dump)

请注意,我只保存来自具有__class__ 属性的对象的数据,因为这些是我唯一关心的对象。应该可以保存完整的对象列表,但您需要注意选择其他属性。另外,我发现获取每个对象的引用者非常慢,所以我选择只保存引用者。无论如何,崩溃后,生成的腌制数据可以这样读取:

with open("memory.pickle", 'rb') as dump:
    objs = cPickle.load(dump)

于 2017 年 11 月 15 日添加

Python 3.6 版本在这里:

import gc
import sys
import _pickle as cPickle

def memory_dump():
    with open("memory.pickle", 'wb') as dump:
        xs = []
        for obj in gc.get_objects():
            i = id(obj)
            size = sys.getsizeof(obj, 0)
            #    referrers = [id(o) for o in gc.get_referrers(obj) if hasattr(o, '__class__')]
            referents = [id(o) for o in gc.get_referents(obj) if hasattr(o, '__class__')]
            if hasattr(obj, '__class__'):
                cls = str(obj.__class__)
                xs.append({'id': i, 'class': cls, 'size': size, 'referents': referents})
        cPickle.dump(xs, dump)

【讨论】:

  • 看起来 sys.getsizeof 有一些重要的限制需要牢记:docs.python.org/library/sys.html#sys.getsizeof 即,2.6 中的新功能和“如果对象不提供检索大小的方法将返回默认值。否则将引发 TypeError。”
  • @keturn 对同一个文件多次使用 pickle.dump() 意味着我们可以通过在腌制对象之前不将整个对象列表存储在内存中来节省大量内存。这对我的程序很有帮助,因为当我开始转储时它已经耗尽了内存
  • 很好的答案。更新,而不是 psutil.Process(os.getpid()).get_memory_info().rss 在 Python 2.7 上应该是 psutil.Process(os.getpid()).memory_info().rss
  • 你应该添加'import psutil'
【解决方案2】:

我将根据我最近的经验扩展 Brett 的答案。 Dozer packagewell maintained,尽管取得了一些进步,例如在 Python 3.4 中将 tracemalloc 添加到 stdlib,但它的 gc.get_objects 计数图表是我解决内存泄漏的首选工具。下面我使用dozer > 0.7,它在撰写本文时尚未发布(好吧,因为我最近在那里贡献了几个修复)。

示例

让我们看一个重要的内存泄漏。我将在这里使用Celery 4.4 并最终发现一个导致泄漏的功能(并且因为它是一种错误/功能类型的东西,它可以被称为纯粹的错误配置,由于无知)。所以有一个 Python 3.6 venvpip 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 个终端:

  1. procpath record -d celery.sqlite -i1 "$..children[?('celery' in @.cmdline)]" 记录 Celery 节点的进程树统计信息
  2. docker run --rm -it -p 6379:6379 redis 运行 Redis,它将作为 Celery 代理和结果后端
  3. celery -A demo worker --concurrency 2 使用 2 个工作人员运行节点
  4. 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=tsY=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 pillowdozer 用于制图)
  • 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=0filter=AsyncResult
  • 然后点击“TRACE”,应该会产生

然后在 Pyrasite shell 中运行:

objgraph.show_backrefs([objgraph.at(140254427663376)], filename='backref.png')

PNG 文件应包含:

基本上有一些Context 对象包含一个名为_childrenlist,而该对象又包含许多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,显示此内存消耗。

问题解决了!

【讨论】:

  • 这是很棒的调试@saaj!从您的最小示例来看,它看起来是 celery 4.4 跟踪功能中的错误/泄漏,您知道这是否曾报告给 celery?
  • @tutuDajuju 谢谢。关于报告行为/错误/泄漏,鉴于使用 Celery 的案例的多样性,我不确定它是否会被视为错误。跟踪功能被描述为跟踪此任务启动的子任务,这意味着必须在任务期间保留一些对象。后者可能是错误/泄漏或按预期工作,具体取决于视角。所以我没有报告它,当我自己面对这种行为时,确实找到了一个现有的报告。
【解决方案3】:

您能否在生产站点上记录流量(通过日志),然后在装有 python 内存调试器的开发服务器上重新播放? (我推荐推土机:http://pypi.python.org/pypi/Dozer

【讨论】:

  • 可能值得一试。有各种考虑因素,例如我将使用多少磁盘写入 i/o,以及排列正确的数据库快照以处理记录的输入,但如果我们能够让它工作,它将是一个非常有用的工具。
【解决方案4】:

Make your program dump core,然后使用gdb 在足够相似的盒子上克隆程序实例。有special macros帮助调试gdb中的python程序,但是如果你可以让你的程序同时serve up a remote shell,你可以继续程序的执行,然后用python查询它。

我从来没有这样做过,所以我不能 100% 确定它会起作用,但也许这些指针会有所帮助。

【讨论】:

  • 安装检修孔会非常有帮助,是的。遗憾的是,这不是 Twisted 应用程序。
  • 请参阅here 如何在(例如)Flask 应用程序中使用检修孔。
【解决方案5】:

我不知道如何转储整个 python 解释器状态并恢复它。这会很有用,我会密切关注这个答案,以防其他人有想法。

如果您知道内存泄漏的位置,可以添加检查对象的引用计数。例如:

x = SomeObject()
... later ...
oldRefCount = sys.getrefcount( x )
suspiciousFunction( x )
if (oldRefCount != sys.getrefcount(x)):
    print "Possible memory leak..."

您还可以检查引用计数是否高于对您的应用来说合理的某个数字。更进一步,您可以修改 python 解释器以通过将 Py_INCREFPy_DECREF 宏替换为您自己的宏来进行此类检查。不过,这在生产应用中可能有点危险。

这是一篇文章,其中包含有关调试这类事情的更多信息。它更适合插件作者,但大部分都适用。

Debugging Reference Counts

【讨论】:

  • 不幸的是,“内存泄漏的想法”是我想要得到的。不过,这种寻找高引用计数的方法可能很有用。
【解决方案6】:

gc module 具有一些可能有用的功能,例如列出垃圾收集器发现无法访问但无法释放的所有对象,或者列出所有正在跟踪的对象。

如果您怀疑哪些对象可能泄漏,weakref 模块可以方便地查明是否/何时收集对象。

【讨论】:

  • 是的。如果我能弄清楚如何获取 gc.get_objects 之类的东西并将其导出以供以后分析。我认为我不能为此使用泡菜,因为并非所有东西都可以泡菜。
【解决方案7】:

Meliae 看起来很有希望:

这个项目类似于 heapy(在“guppy”项目中),试图了解内存是如何分配的。

目前,它的主要区别在于它将计算内存消耗的汇总统计等任务与实际扫描内存消耗的任务分开。它这样做是因为我经常想弄清楚我的进程中发生了什么,而我的进程正在消耗大量内存(1GB 等)。它还可以显着简化扫描程序,因为我在尝试分析 python 对象内存消耗时不分配 python 对象。

【讨论】: