你如何修复 Django 测试中的内存泄漏? 更新第二次更新第三次更新

问题描述

最近我开始在 Django (3.1) 测试中遇到一些问题,我最终找到了某种内存泄漏。 我通常使用 --parallel=4 运行我的套件(目前大约 4000 个测试),这导致大约 3GB 的高内存水印(从 500MB 左右开始)。 不过,出于审计目的,我偶尔会使用 --parallel=1 运行它 - 当我这样做时,内存使用量不断增加,最终超过 VM 分配的 6GB。

我花了一些时间查看数据,结果很明显,罪魁祸首是 Webtest - 更具体地说,它的 response.htmlresponse.forms:测试用例期间的每个调用可能会分配一些MB(通常为两个或三个)不会在测试方法结束时释放,更重要的是,甚至在 TestCase 结束时也不会释放。

我已经尝试了我能想到的所有方法 - gc.collect()gc.DEBUG_LEAK 向我展示了大量可收藏的物品,但它根本没有释放任何内存;在各种 delattr()TestCase 属性等上使用 TestResponse 导致根本没有变化,等等。

我真的很无知,所以任何解决这个问题的指针(除了编辑使用 WebTest 响应的数千个左右的测试,这真的不可行)将非常感激。

(请注意,我也尝试使用 guppytracemallocmemory_profiler,但都没有给我任何可操作的信息。)


更新

我发现我们的一个 EC2 测试实例不受此问题的影响,因此我花了更多时间试图解决这个问题。 最初,我试图找到“合理”的潜在原因——例如,缓存模板加载器,它在我的本地 VM 上启用并在 EC2 实例上禁用——但没有成功。 然后我全力以赴:我复制了 EC2 virtualenv(使用 pip freeze)和设置(复制了 dotenv),并检查了测试在 EC2 上正常运行的相同提交。

等等! 内存泄漏仍然存在

现在,我正式放弃并将使用 --parallel=2 进行未来的测试,直到某个绝对的大师可以为我指明正确的方向。


第二次更新

现在即使使用 --parallel=2 也存在内存泄漏。我想这在某种程度上更好,因为它看起来越来越像是系统问题而不是应用程序问题。没有解决,但至少我知道这不是我的错。


第三次更新

感谢 Tim Boddy 对 this question回复,我尝试使用 chap 找出导致内存增长的原因。不幸的是,我无法正确“读取”结果,但看起来某些非 Python 库实际上是导致问题的原因。 所以,这就是我在运行几分钟后看到的分析核心的结果,我知道会导致泄漏:

chap> summarize writable
49 ranges take 0x1e0aa000 bytes for use: unkNown
1188 ranges take 0x12900000 bytes for use: python arena
1 ranges take 0x4d1c000 bytes for use: libc malloc main arena pages
7 ranges take 0x3021000 bytes for use: stack
139 ranges take 0x476000 bytes for use: used by module
1384 writable ranges use 0x38b5d000 (951,439,360) bytes.
chap> count used
3144197 allocations use 0x14191ac8 (337,189,576) bytes.

有趣的一点是,非泄漏 EC2 实例显示的值与我从 count used 获得的值几乎相同 - 这表明那些“未知”范围是实际的猪。 summarize used输出支持这一点(显示前几行):

Unrecognized allocations have 886033 instances taking 0x8b9ea38(146,401,848) bytes.
   Unrecognized allocations of size 0x130 have 148679 instances taking 0x2b1ac50(45,198,416) bytes.
   Unrecognized allocations of size 0x40 have 312166 instances taking 0x130d980(19,978,624) bytes.
   Unrecognized allocations of size 0xb0 have 73886 instances taking 0xc66ca0(13,003,936) bytes.
   Unrecognized allocations of size 0x8a8 have 3584 instances taking 0x793000(7,942,144) bytes.
   Unrecognized allocations of size 0x30 have 149149 instances taking 0x6d3d70(7,159,152) bytes.
   Unrecognized allocations of size 0x248 have 10137 instances taking 0x5a5508(5,920,008) bytes.
   Unrecognized allocations of size 0x500018 have 1 instances taking 0x500018(5,242,904) bytes.
   Unrecognized allocations of size 0x50 have 44213 instances taking 0x35f890(3,537,040) bytes.
   Unrecognized allocations of size 0x458 have 2969 instances taking 0x326098(3,301,528) bytes.
   Unrecognized allocations of size 0x205968 have 1 instances taking 0x205968(2,120,040) bytes.

这些单实例分配的大小与我在开始/停止测试时在测试运行程序中添加resource.getrusage(resource.RUSAGE_SELF).ru_maxRSS调用所看到的增量非常相似 - 但它们不被识别为 Python 分配,所以我的感觉。

解决方法

首先,非常抱歉:我错误地认为 WebTest 是导致此问题的原因,而原因确实出在我自己的代码中,而不是库或其他任何东西中。

真正的原因是一个 mixin 类,我不假思索地添加了一个 dict 作为类属性,比如

class MyMixin:
    errors = dict()

由于此 mixin 以几种形式使用,并且测试生成了大量形式错误(添加到 dict 中),因此最终占用了内存。

虽然这本身并不是很有趣,但有一些要点可能对未来遇到同类问题的探索者有所帮助。除了我和其他开发人员之外,其他所有人可能都很清楚 - 在这种情况下,您好,其他开发人员。

  1. 同一提交在 EC2 机器和我自己的 VM 上具有不同行为的原因是远程机器中的 branch 尚未合并,因此引入泄漏的提交是没有毒害环境。 这里的要点是:确保您正在测试的代码相同,而不仅仅是提交。
  2. 低级内存分析在某些情况下可能会有所帮助,但这不是你半天就能学会的技能:我花了很长时间试图理解分配和对象以及其他任何东西,但没有更接近解决方案。
  3. 这种错误的代价可能非常高 - 如果我少做几百个测试,我就不会以 OOM 错误告终,而且我可能根本不会注意到这个问题。直到它投入生产,也就是说。 如果有一种将这种结构标记为潜在有害的结构,也可以通过某种短绒/静态分析来解决这个问题。不幸的是,没有一个(我能找到)。
  4. git bisect 是你的朋友,只要你能找到一个真正有效的提交。