Debugging memory leaks

在Scrapy中,诸如请求,响应和项目之类的对象具有有限的生存期:它们被创建,使用了一段时间并最终被销毁.

在所有这些对象中,请求可能是寿命最长的请求,因为它一直在调度程序队列中等待,直到需要处理它为止. 有关更多信息,请参见体系结构概述 .

由于这些Scrapy对象具有(相当长的)生存期,因此始终存在在没有正确释放它们的情况下将它们存储在内存中的风险,从而导致所谓的"内存泄漏".

To help debugging memory leaks, Scrapy provides a built-in mechanism for tracking objects references called trackref, and you can also use a third-party library called Guppy for more advanced memory debugging (see below for more info). Both mechanisms must be used from the Telnet Console.

Common causes of memory leaks

Scrapy开发人员传递请求中引用的对象(例如,使用cb_kwargsmeta属性或请求回调函数)的情况很常见(有时是偶然的,有时是有意的),并且有效地将这些引用对象的生存期限制在请求的生存期. 到目前为止,这是Scrapy项目中最常见的内存泄漏原因,对于新手来说,这是一个非常难调试的原因.

在大型项目中,蜘蛛通常是由不同的人编写的,其中一些蜘蛛可能会"泄漏",从而影响其他(写得好的)蜘蛛同时运行时的其余部分,进而影响到其他蜘蛛.整个爬行过程.

如果未正确释放(先前分配的)资源,则泄漏也可能来自您编写的自定义中间件,管道或扩展. 例如,如果您在每个进程中运行多个蜘蛛,则在spider_closed上分配资源但不在spider_opened释放资源可能会导致问题.

Too Many Requests?

默认情况下,Scrapy将请求队列保留在内存中. 它包括Request对象和Request属性中引用的所有对象(例如cb_kwargsmeta ). 虽然不一定要泄漏,但这可能会占用大量内存. 启用持久作业队列可以帮助控制内存使用.

Debugging memory leaks with trackref

trackreftrackref提供的模块,用于调试最常见的内存泄漏情况. 它基本上跟踪对所有实时请求,响应,项目和选择器对象的引用.

您可以进入telnet控制台,并使用prefs()函数(这是print_live_refs()函数的别名prefs()检查当前(上述类中)有多少个对象处于活动状态:

telnet localhost 6023

>>> prefs()
Live References

ExampleSpider                       1   oldest: 15s ago
HtmlResponse                       10   oldest: 1s ago
Selector                            2   oldest: 0s ago
FormRequest                       878   oldest: 7s ago

如您所见,该报告还显示了每个类中最旧的对象的"年龄". 如果每个进程运行多个蜘蛛,则可以通过查看最早的请求或响应来找出哪个蜘蛛正在泄漏. 您可以使用get_oldest()函数(从telnet控制台)获取每个类的最早的对象.

Which objects are tracked?

trackrefs跟踪的对象全部来自以下类(及其所有子类):

A real example

让我们来看一个假设的内存泄漏情况的具体示例. 假设我们有一些蜘蛛,其线条类似于此:

return Request("http://www.somenastyspider.com/product.php?pid=%d" % product_id,
               callback=self.parse, cb_kwargs={'referer': response})

该行在请求内部传递了一个响应引用,该响应引用有效地将响应生存期与请求的响应寿命相关联,这肯定会导致内存泄漏.

让我们看看如何使用trackref工具发现原因(当然不知道是先验的).

搜寻器运行了几分钟后,我们注意到它的内存使用量已经大大增加,我们可以进入其telnet控制台并检查实时引用:

>>> prefs()
Live References

SomenastySpider                     1   oldest: 15s ago
HtmlResponse                     3890   oldest: 265s ago
Selector                            2   oldest: 0s ago
Request                          3878   oldest: 250s ago

实时响应如此之多(而且它们太旧了)的事实无疑令人怀疑,因为与Requests相比,响应的生命周期较短. 响应的数量类似于请求的数量,因此看起来它们以某种方式捆绑在一起. 现在,我们可以检查蜘蛛程序的代码以发现正在生成泄漏的讨厌的行(在请求内部传递响应引用).

有时,有关活动对象的其他信息可能会有所帮助. 让我们检查最早的响应:

>>> from scrapy.utils.trackref import get_oldest
>>> r = get_oldest('HtmlResponse')
>>> r.url
'http://www.somenastyspider.com/product.php?pid=123'

如果要遍历所有对象,而不是获取最旧的对象,可以使用scrapy.utils.trackref.iter_all()函数:

>>> from scrapy.utils.trackref import iter_all
>>> [r.url for r in iter_all('HtmlResponse')]
['http://www.somenastyspider.com/product.php?pid=123',
 'http://www.somenastyspider.com/product.php?pid=584',
...

Too many spiders?

如果您的项目有太多并行执行的蜘蛛,则prefs()的输出可能很难读取. 由于这个原因,该函数有一个ignore参数,可用于忽略特定的类(及其所有子类). 例如,这不会显示任何有关蜘蛛的实时引用:

>>> from scrapy.spiders import Spider
>>> prefs(ignore=Spider)

scrapy.utils.trackref module

这是trackref模块中可用的功能.

class scrapy.utils.trackref.object_ref

如果要使用trackref模块跟踪活动实例,则从此类(而不是对象)继承.

scrapy.utils.trackref.print_live_refs(class_name, ignore=NoneType)

打印实时引用的报告,按类名称分组.

Parameters:忽略或多个 元组 ) -如果给定的,从指定的类(或类的元组)的所有对象都会被忽略.
scrapy.utils.trackref.get_oldest(class_name)

使用给定的类名返回最旧的对象,如果没有,则返回None . 首先使用print_live_refs()获取每个类名称的所有跟踪的活动对象的列表.

scrapy.utils.trackref.iter_all(class_name)

返回一个使用给定类名运行的所有对象的迭代器;如果None找到,则返回None . 首先使用print_live_refs()获取每个类名称的所有跟踪的活动对象的列表.

Debugging memory leaks with Guppy

trackref提供了一种非常方便的机制来跟踪内存泄漏,但是它只跟踪更可能导致内存泄漏的对象(请求,响应,项目和选择器). 但是,在其他情况下,内存泄漏可能来自其他(或多或少模糊的)对象. 如果是这种情况,并且无法使用trackref找到泄漏,那么您仍然拥有另一个资源: Guppy库 . 如果您使用的是Python3,请参见使用muppy 调试内存泄漏 .

如果使用pip ,则可以使用以下命令安装Guppy:

pip install guppy

telnet控制台还带有用于访问Guppy堆对象的内置快捷方式( hpy ). 这是一个使用Guppy查看堆中所有可用Python对象的示例:

>>> x = hpy.heap()
>>> x.bytype
Partition of a set of 297033 objects. Total size = 52587824 bytes.
 Index  Count   %     Size   % Cumulative  % Type
     0  22307   8 16423880  31  16423880  31 dict
     1 122285  41 12441544  24  28865424  55 str
     2  68346  23  5966696  11  34832120  66 tuple
     3    227   0  5836528  11  40668648  77 unicode
     4   2461   1  2222272   4  42890920  82 type
     5  16870   6  2024400   4  44915320  85 function
     6  13949   5  1673880   3  46589200  89 types.CodeType
     7  13422   5  1653104   3  48242304  92 list
     8   3735   1  1173680   2  49415984  94 _sre.SRE_Pattern
     9   1209   0   456936   1  49872920  95 scrapy.http.headers.Headers
<1676 more rows. Type e.g. '_.more' to view.>

您可以看到dicts占用了大部分空间. 然后,如果要查看从哪些属性引用了这些字典,则可以执行以下操作:

>>> x.bytype[0].byvia
Partition of a set of 22307 objects. Total size = 16423880 bytes.
 Index  Count   %     Size   % Cumulative  % Referred Via:
     0  10982  49  9416336  57   9416336  57 '.__dict__'
     1   1820   8  2681504  16  12097840  74 '.__dict__', '.func_globals'
     2   3097  14  1122904   7  13220744  80
     3    990   4   277200   2  13497944  82 "['cookies']"
     4    987   4   276360   2  13774304  84 "['cache']"
     5    985   4   275800   2  14050104  86 "['meta']"
     6    897   4   251160   2  14301264  87 '[2]'
     7      1   0   196888   1  14498152  88 "['moduleDict']", "['modules']"
     8    672   3   188160   1  14686312  89 "['cb_kwargs']"
     9     27   0   155016   1  14841328  90 '[1]'
<333 more rows. Type e.g. '_.more' to view.>

如您所见,Guppy模块非常强大,但是还需要一些有关Python内部知识的深入知识. 有关Guppy的更多信息,请参阅Guppy文档 .

Debugging memory leaks with muppy

如果您使用的是Python 3,则可以使用Pympler中的muppy .

如果使用pip ,则可以使用以下命令安装muppy:

pip install Pympler

这是一个使用muppy查看堆中所有可用Python对象的示例:

>>> from pympler import muppy
>>> all_objects = muppy.get_objects()
>>> len(all_objects)
28667
>>> from pympler import summary
>>> suml = summary.summarize(all_objects)
>>> summary.print_(suml)
                               types |   # objects |   total size
==================================== | =========== | ============
                         <class 'str |        9822 |      1.10 MB
                        <class 'dict |        1658 |    856.62 KB
                        <class 'type |         436 |    443.60 KB
                        <class 'code |        2974 |    419.56 KB
          <class '_io.BufferedWriter |           2 |    256.34 KB
                         <class 'set |         420 |    159.88 KB
          <class '_io.BufferedReader |           1 |    128.17 KB
          <class 'wrapper_descriptor |        1130 |     88.28 KB
                       <class 'tuple |        1304 |     86.57 KB
                     <class 'weakref |        1013 |     79.14 KB
  <class 'builtin_function_or_method |         958 |     67.36 KB
           <class 'method_descriptor |         865 |     60.82 KB
                 <class 'abc.ABCMeta |          62 |     59.96 KB
                        <class 'list |         446 |     58.52 KB
                         <class 'int |        1425 |     43.20 KB

有关muppy的更多信息,请参阅muppy文档 .

Leaks without leaks

有时,您可能会注意到Scrapy进程的内存使用量只会增加,而不会减少. 不幸的是,即使Scrapy和您的项目都没有泄漏内存,也可能发生这种情况. 这是由于(一个不太为人所知)的Python问题所致,在某些情况下,该问题可能不会将释放的内存返回给操作系统. 有关此问题的更多信息,请参见:

Evan Jones提出的改进(已在本文中进行了详细介绍 )已合并到Python 2.5中,但这只能减少问题,不能完全解决问题. 引用本文:

不幸的是,此修补程序只能在不再分配任何对象的情况下释放竞技场. 这意味着碎片是一个大问题. 一个应用程序可能有许多兆的可用内存,散布在所有领域,但是它将无法释放其中的任何一个. 这是所有内存分配器都遇到的问题. 解决该问题的唯一方法是移至压缩垃圾收集器,该垃圾收集器能够移动内存中的对象. 这将需要对Python解释器进行重大更改.

为了使内存消耗合理,您可以将作业拆分为几个较小的作业,或者启用持久性作业队列并不时停止/启动蜘蛛程序.