我有一个Django应用程序,它表现出一些奇怪的垃圾收集行为。有一个视图特别是每次调用时都会显着增加VM大小 - 达到某个限制,此时使用率会再次下降。问题是,在达到这一点之前需要相当长的时间,事实上,运行我的应用程序的虚拟机没有足够的内存供所有FCGI进程占用尽可能多的内存。
我花了最近两天的时间来研究这个并学习Python垃圾收集,我认为我确实理解现在正在发生的事情 - 大部分时间。使用时
gc.set_debug(gc.DEBUG_STATS)
然后,对于单个请求,我看到以下输出:
>>> c = django.test.Client()
>>> c.get('/the/view/')
gc: collecting generation 0...
gc: objects in each generation: 724 5748 147341
gc: done.
gc: collecting generation 0...
gc: objects in each generation: 731 6460 147341
gc: done.
[...more of the same...]
gc: collecting generation 1...
gc: objects in each generation: 718 8577 147341
gc: done.
gc: collecting generation 0...
gc: objects in each generation: 714 0 156614
gc: done.
[...more of the same...]
gc: collecting generation 0...
gc: objects in each generation: 715 5578 156612
gc: done.
基本上,分配了大量的对象,但最初被移动到第1代,当gen 1在同一请求中被清空时,它们被移动到第2代。如果我做了手动gc.collect(2 )之后,他们被删除。并且,正如我所提到的,当下一次自动第2代扫描发生时,也会删除,如果我理解正确的话,在这种情况下,每10个请求就会发生一次(此时应用需要大约150MB)。
好吧,所以最初我认为在处理一个请求时可能会有一些循环引用,这会阻止在处理该请求时收集任何这些对象。但是,我花了好几个小时尝试使用pympler.muppy和objgraph,在请求处理之后和调试之间找到一个,并且似乎没有。相反,似乎在请求期间创建的14.000个对象都在一个请求全局对象的引用链中,即一旦请求消失,它们就可以被释放。
无论如何,这是我试图解释它的原因。但是,如果这是真的并且确实没有循环依赖,那么一旦任何导致它们被保持的请求对象消失,整个对象树就不会被释放,而不涉及垃圾收集器,纯粹是由于引用计数降到零?
使用该设置,以下是我的问题:
以上是否有意义,或者我是否必须在其他地方寻找问题?在这个特定的用例中,重要数据长期存在只是一个不幸的事故吗?
我能做些什么来避免这个问题。我已经看到了优化视图的一些潜力,但这似乎是一个范围有限的解决方案 - 尽管我不确定我的通用性是什么,或者;例如,手动调用gc.collect()或gc.set_threshold()是否明智?
就垃圾收集器本身的工作方式而言:
我是否正确理解,如果扫描查看对象并确定其具有非循环的参考,但实际上可以跟踪,则该对象始终会移动到下一代到根对象。
如果gc执行第1代扫描,并找到第2代中对象引用的对象,会发生什么情况;它是否遵循第2代内部的关系,还是在分析情况之前等待第2代扫描?
使用gc.DEBUG_STATS时,我主要关心的是“每一代中的对象”信息;但是,我不断收到数百个“gc:0.0740s过去了。”,“gc:1258233035.9370s已经过去了。”消息;它们非常不方便 - 打印出来需要相当长的时间,而且它们使得有趣的东西更难找到。有没有办法摆脱它们?
我不认为有一种方法可以生成gc.get_objects(),例如只检索第2代的对象,例如?
答案 0 :(得分:3)
以上是否有意义,或者我是否必须在其他地方寻找问题?在这个特定的用例中,重要的数据长期存在只是一个不幸的事故吗?
是的,它确实有意义。是的,还有其他值得考虑的问题。 Django使用threading.local
作为DatabaseWrapper
的基础(并且一些贡献者使用它来使请求对象可以从未明确传递的地方访问)。这些全局对象在请求中存活,并且可以保持对对象的引用,直到在线程中处理其他视图。
我能做些什么来避免这个问题。我已经看到了优化视图的一些潜力,但这似乎是一个范围有限的解决方案 - 尽管我不确定我的通用性是什么,或者;例如,手动调用gc.collect()或gc.set_threshold()是否明智?
一般建议(可能你知道,但无论如何):避免循环引用和全局(包括threading.local
)。当django设计难以避免时,尝试打破循环并清除全局变量。 gc.get_referrers(obj)
可能会帮助您找到需要关注的地方。另一种方法是禁用垃圾收集器并在每次请求后手动调用它,这是最好的地方(这会阻止对象移动到下一代)。
我不认为有一种方法可以生成gc.get_objects(),例如只检索第2代的对象?
不幸的是,gc
接口无法实现这一点。但有几种方法可以去。您可以仅考虑gc.get_objects()
返回的列表的结尾,因为此列表中的对象按生成进行排序。您可以通过在调用之间存储对它们的弱引用(例如,在WeakKeyDictionary
中)来将列表与从先前调用返回的列表进行比较。您可以在自己的C模块中重写gc.get_objects()
(这很容易,主要是复制粘贴编程!),因为它们在内部生成存储,甚至可以使用ctypes
访问内部结构(需要非常深{{1}理解)。
答案 1 :(得分:2)
我认为您的分析看起来很合理。我不是gc
的专家,所以每当我遇到这样的问题时,我只需在适当的非时间关键位置添加对gc.collect()
的调用,然后忘掉它。
我建议您在视图中调用gc.collect()
,看看它对您的响应时间和内存使用情况有何影响。
另请注意this question,这表明设置DEBUG=True
会占用内存,因为它几乎已经过了销售日期。