如何在python脚本中跟踪内存增长对象

2024-10-05 13:20:51 发布

您现在位置:Python中文网/ 问答频道 /正文

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

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

我正在寻找一个合适的内存分析器工具,它允许我识别代码中增长对象的位置

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

谢谢

EDIT1:我想尝试一下,但没能成功。由于该项目的最后一次更新是2012年5月,它可能不再与当前的python兼容(我使用的是3.8)

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

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)

这将显示内存使用率最高的代码行。但是,与在{}中观察到的相比,仍然没有什么。 我还尝试了gc.collect(),但没有效果


Tags: 对象内存代码脚本objectstopsnapshotmb
2条回答

我无法利用任何建议的内存分析器。事实证明,问题在于库ujson中可能存在的bug,它是用C编写的。 我想这就是为什么所有这些python内存分析器在这里帮不上忙的原因。 我想我必须结束这个问题。剩下的问题是:是否有任何python工具允许跟踪C模块中发生的内存问题

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

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

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

例如,考虑一个相当小的Python程序,其中主THTEAD启动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打开生成的内核、可在https://github.com/vmware/chap找到的开源和运行如下所示的命令来理解此类程序(以及您的程序)的高VIRT值:

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个堆栈。如果我们使用“descripe writable”,我们会看到其中16个堆栈每个占用40MB,这都是因为我们忽略了显式地将堆栈大小调整为更合理的大小

我们可以对不可访问(不可读、不可写或不可执行)区域执行类似的操作,并看到16个“libc malloc heap tail reservation”范围占用了几乎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个领域

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

我不能确切地说,当您在自己的服务器上运行它时,您会发现什么,但REST服务器似乎很可能是多线程的,所以我猜这将是TOP向您展示的数字的一个非常重要的贡献

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

^{pr5}$

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

describe used
describe free
summarize used
summarize free

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

在你自己的程序中遵循类似的步骤应该会让你更好地理解你看到的那些“顶级”数字

相关问题 更多 >

    热门问题