【问题标题】:How to track a memory-usage growing object in a python script如何在 python 脚本中跟踪内存使用增长的对象
【发布时间】:2020-07-31 08:18:32
【问题描述】:

我有一个 python 脚本,它的内存使用量(在 top 中观察到)在脚本运行时无限增长。

我的脚本不应该存储任何东西。它只是接收一些请求(REST API),处理请求并返回一些结果。预期的行为是内存使用量保持不变。

我正在寻找一个合适的内存分析器工具,它可以让我确定正在增长的对象在代码中的位置。

我查看了FIL-profiler,但它似乎旨在考虑“内存使用峰值”,我认为这不是我的情况。

谢谢

EDIT1:我想试一试pyrasite,但我无法成功。由于本项目最后一次更新是2012年5月,可能是现在的python已经不兼容了(我用的是3.8)

EDIT2:我在this post 之后尝试了Pympler。我在脚本中添加了以下代码:

from pympler import muppy, summary
all_objects = muppy.get_objects()
sum = summary.summarize(all_objects)
summary.print_(sum)

这个输出:

    types |   # objects |   total size
========= | =========== | ============
     dict |        7628 |      3.15 MB
      str |       24185 |      2.71 MB
     type |        1649 |      1.39 MB
    tuple |       13722 |      1.35 MB
     code |        7701 |      1.06 MB
      set |         398 |    453.06 KB
....

没有显示任何可疑的大对象(脚本在运行时会累积达到 GB 规模的内存使用量)

编辑3: 我尝试使用tracemalloc:我在脚本的开头放了一个tracemalloc.start(),然后在脚本的结尾,但在停止之前(因此内存使用量明显非常高,根据top),我做一个

snapshot = tracemalloc.take_snapshot()
display_top(snapshot)

这会显示内存使用量最大的代码行。但是,与在top 中观察到的相比,仍然没有任何意义。 我也尝试了 gc.collect(),但这没有效果。

【问题讨论】:

标签: python memory-leaks profiler


【解决方案1】:

我无法利用任何建议的内存分析器。原来问题出在库ujson中一个可能的bug,它是用C写的。 我想这就是为什么所有那些 python 内存分析器在这里都帮不上忙的原因。 我想我必须关闭这个问题。剩下的问题是:是否有任何 python 工具可以跟踪 C 模块中发生的内存问题?

【讨论】:

    【解决方案2】:

    假设您使用的是顶部的“VIRT”列,您无法从该数字推断出您的 Python 对象数量正在增长,或者至少增长到足以解释虚拟地址空间的总大小的假设。

    例如,您是否知道 python 在其线程代码下使用 pthreads?这很重要,因为 pthreads 将“ulmimit -s”* 1K 的值作为默认堆栈大小。因此,python 中任何新线程的默认堆栈大小在某些 Linux 变体上通常为 8MB 甚至 40MB,除非您明确更改进程父进程中的“ulimit -s”值。许多服务器应用程序是多线程的,因此即使有 2 个额外的线程也会导致比您从 Pymler 输出中显示的更多的虚拟内存。您必须知道进程中有多少线程以及默认堆栈大小是多少,才能了解线程对总 VIRT 值的影响。

    另外,在它自己的分配机制下,python 混合使用了 mmap 和 malloc。在 malloc 的情况下,如果程序是多线程的,在 linux 上 libc malloc 将使用多个 arenas,它们一次保留 64MB 范围(称为堆),但只使这些堆的开头可读可写并留下尾部在需要内存之前无法访问这些范围。那些不可访问的尾部通常很大,但就进程的提交内存而言根本不重要,因为不可访问的页面不需要任何物理内存或交换空间。尽管如此,“VIRT”中的顶部计数整个范围,包括范围开头的可访问开始和范围末尾的不可访问开始。

    例如,考虑一个相当小的 python 程序,其中主线程启动了 16 个附加线程,每个线程在 python 分配中不使用太多内存:

    import threading
    
    def spin(seed):
        l = [ i * seed for i in range(64) ]
        while True:
           l = [ i * i % 97 for i in l ]
    
    for i in range(16):
        t = threading.Thread(target=spin, args=[i])
        t.start()
    

    我们不希望该程序产生这么大的进程,但这是我们在顶部看到的,仅查看一个进程,它显示了超过 1GB 的 VIRT:

    Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie
    Cpu(s):  1.0%us,  1.9%sy,  3.3%ni, 93.3%id,  0.5%wa,  0.0%hi,  0.0%si,  0.0%st
    Mem:  264401648k total, 250450412k used, 13951236k free,  1326116k buffers
    Swap: 69205496k total, 17321340k used, 51884156k free, 104421676k cached
    
      PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                                                      
     9144 tim       20   0 1787m 105m  99m S 101.8  0.0  13:40.49 python                                                                                                                                       
    

    我们可以通过获取正在运行的程序的实时内核(例如使用 gcore)并使用开源 chap 打开生成的内核,从而了解此类程序(以及您的程序)的高 VIRT 值可以在https://github.com/vmware/chap 找到并运行如下所示的命令:

    chap> summarize writable
    17 ranges take 0x2801c000 bytes for use: stack
    14 ranges take 0x380000 bytes for use: python arena
    16 ranges take 0x210000 bytes for use: libc malloc heap
    1 ranges take 0x1a5000 bytes for use: libc malloc main arena pages
    11 ranges take 0xa9000 bytes for use: used by module
    1 ranges take 0x31000 bytes for use: libc malloc mmapped allocation
    2 ranges take 0x7000 bytes for use: unknown
    62 writable ranges use 0x28832000 (679,682,048) bytes.
    

    从上面我们可以看出,最大的单次使用内存是17个栈。如果我们使用“describe writable”,我们会看到其中 16 个堆栈每个占用 40MB,这完全是因为我们忽略了将堆栈大小显式调整为更合理的值。

    我们可以对不可访问(不可读取、不可写入或可执行)区​​域执行类似的操作,并看到 16 个“libc malloc 堆尾保留”范围占用了将近 1GB 的 VIRT。事实证明,这对于进程的已提交内存并不重要,但它确实对 VIRT 数做出了相当可怕的贡献。

    chap> summarize inaccessible
    16 ranges take 0x3fdf0000 bytes for use: libc malloc heap tail reservation
    11 ranges take 0x15f9000 bytes for use: module alignment gap
    16 ranges take 0x10000 bytes for use: stack overflow guard
    43 inaccessible ranges use 0x413f9000 (1,094,684,672) bytes.
    

    之所以有 16 个这样的范围,是因为每个旋转的线程都在重复进行分配,这导致 libc malloc(因为它被 python 分配器使用)在后台运行,从而划分出 16 个竞技场。

    您可以对只读内存执行类似的命令(“summarize readonly”),或者您可以通过将“summarize”更改为“describe”来获取任何命令的更多详细信息。

    我不能确切地说当您在自己的服务器上运行它时会发现什么,但是 REST 服务器似乎很可能是多线程的,所以我猜这将是一个不平凡的贡献到 TOP 显示的号码。

    这仍然不能解释为什么这个数字会继续攀升,但如果您查看这些数字,您就可以知道下一步该往哪里看。例如,在上面的摘要中,由 python 处理而不使用 mmap 的分配不会占用那么多内存,只有 3.5MB:

    14 ranges take 0x380000 bytes for use: python arena
    

    在您的情况下,数字可能更大,因此您可以尝试以下任一方法:

    describe used
    describe free
    summarize used
    summarize free
    

    请注意,上述命令还将涵盖本机分配(例如由共享库完成的分配)以及 python 分配。

    在您自己的程序中执行类似的步骤应该可以让您更好地了解您所看到的那些“顶级”数字。

    【讨论】:

    • 感谢详细的解释!我会看看这个。实际上我只是在考虑“%MEM”列。
    • 我认为 %MEM 与当前进程使用的物理内存的比例有关。基于与 RES 的明显线性关系,我得到了这种感觉。这通常不是进程增长的一个很好的指标,因为一个进程可能有很多当前已换出的已提交地址空间。
    • 嗯,就我而言,%MEM 是一个非常具体的指示。让程序在我的笔记本电脑上运行 1 小时,内存使用率会达到 100%,然后开始交换,然后崩溃......
    猜你喜欢
    • 1970-01-01
    • 2018-08-28
    • 2011-11-07
    • 2017-02-11
    • 1970-01-01
    • 1970-01-01
    • 2010-12-13
    • 1970-01-01
    • 2011-06-17
    相关资源
    最近更新 更多