3

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

我花了一些时间查看数据,很明显罪魁祸首是,不知何故,Webtest - 更具体地说,它的response.htmland response.forms:在测试用例期间的每个调用可能会分配几个 MB(通常是两个或三个),这些没有得到在测试方法结束时发布,更重要的是,甚至在TestCase.

我已经尝试了所有我能想到的东西——gc.collect()gc.DEBUG_LEAK我展示了很多可收藏的物品,但它根本没有释放任何记忆;使用delattr()各种TestCaseTestResponse属性等导致根本没有变化等。

我真的是束手无策,因此任何解决此问题的指针(除了编辑使用 WebTest 响应的数千个左右的测试,这实际上是不可行的)将不胜感激。

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


更新

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

瞧!内存泄漏仍然存在

现在,我正式放弃并将--parallel=2用于未来的测试,直到一些绝对的大师可以指出我正确的方向。


第二次更新

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


第三次更新

感谢 Tim Boddy 对这个问题的回复,我试图用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 分配,因此我的感觉。

4

1 回答 1

0

首先,一个巨大的道歉:我错误地认为 WebTest 是造成这种情况的原因,而原因确实在我自己的代码中,而不是库或其他任何东西。

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

class MyMixin:
    errors = dict()

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

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

  1. 相同的提交在 EC2 机器和我自己的 VM 上具有不同行为的原因是远程机器中的分支尚未合并,因此引入泄漏的提交并没有污染环境。这里的要点是:确保您正在测试的代码是相同的,而不仅仅是提交。
  2. 低级内存分析在某些情况下可能会有所帮助,但这不是您半天就能掌握的技能:我花了很长时间试图弄清分配和对象等等,但没有更接近解决方案。
  3. 这种错误的代价可能非常高——如果我少做几百个测试,我就不会出现 OOM 错误,而且我可能根本不会注意到这个问题。直到它投入生产,就是这样。这也可以通过某种 linter/static 分析来解决,如果有一个将这种结构标记为潜在有害的。不幸的是,没有一个(我能找到)。
  4. git bisect是你的朋友,只要你能找到一个真正有效的提交。
于 2021-03-23T13:01:52.087 回答