Skip to main content

调试内存泄漏

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

从所有这些对象,请求可能是一个具有最长的生命周期,因为它在调度程序队列中等待,直到它的时间来处理它。更多信息请参阅 建筑概述

由于这些Scrapy对象具有(相当长的)生存期,总是存在将它们累积在存储器中而不适当地释放它们的风险,并且因此导致所谓的“内存泄漏”。

为了帮助调试内存泄漏,Scrapy提供了一个用于跟踪称为 trackref 的对象引用的内置机制,您还可以使用称为 Guppy 的第三方库来进行更高级的内存调试(有关详细信息,请参见下文)。这两种机制必须从 Telnet控制台 中使用。

内存泄漏的常见原因

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

在大项目中,蜘蛛通常由不同的人写成,并且其中一些蜘蛛可能是“泄漏”的,因此当它们同时运行时影响其他(精心编写的)蜘蛛,这反过来影响整个爬行过程。

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

请求太多?

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

使用 trackref 调试内存泄漏

trackref 是Scrapy提供的一个模块,用于调试最常见的内存泄漏情况。它基本上跟踪对所有活动请求,响应,项目和选择器对象的引用。

您可以进入telnet控制台,并使用 prefs() 函数(它是 print_live_refs() 函数的别名)检查当前存在多少个对象(上面提到的类):

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控制台)获取每个类的最旧的对象。

跟踪哪些对象?

trackrefs 跟踪的对象都来自这些类(及其所有子类):

一个真正的例子

让我们看一个假设的内存泄漏情况的具体例子。假设我们有一些蜘蛛有类似这一行:

return Request("http://www.somenastyspider.com/product.php?pid=%d" % product_id,
    callback=self.parse, meta={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

事实上,有这么多的实时响应(并且它们这么古老)是肯定可疑的,因为响应应该比请求相对较短的生命周期。响应的数量类似于请求的数量,所以它看起来像是以某种方式绑定。我们现在可以去检查蜘蛛的代码,以发现生成漏洞的讨厌的行(在请求内部传递响应引用)。

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

>>> 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',
...

太多的蜘蛛?

如果您的项目有太多的并行执行的蜘蛛,prefs() 的输出可能很难阅读。因此,该函数具有一个 ignore 参数,可用于忽略特定类(及其所有子类)。例如,这将不会显示任何对蜘蛛的活引用:

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

scrapy.utils.trackref模块

以下是 trackref 模块中提供的功能。

class scrapy.utils.trackref.object_ref

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

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

打印活动引用的报告,按类名分组。

参数:ignore (class or classes tuple) – 如果给定,来自指定类(或类的元组)的所有对象将被忽略。
scrapy.utils.trackref.get_oldest(class_name)

返回最老的对象与给定的类名称,或 None 如果没有找到。首先使用 print_live_refs() 获取每个类名称的所有跟踪活对象的列表。

scrapy.utils.trackref.iter_all(class_name)

返回一个迭代器在所有对象的活动与给定的类名称,或 None 如果没有找到。首先使用 print_live_refs() 获取每个类名称的所有跟踪活对象的列表。

调试内存泄漏与Guppy

trackref 提供了一个非常方便的机制来跟踪内存泄漏,但它只跟踪更可能导致内存泄漏(请求,响应,项目和选择器)的对象。然而,还有其他情况下,内存泄漏可能来自其他(更多或更少的模糊)对象。如果这是你的情况,并且你不能使用 trackref 找到你的泄漏,你还有另一个资源:Guppy library

如果使用 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.>

你可以看到,大多数空间是由词典使用。然后,如果你想看看引用哪些属性,你可以做:

>>> 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 documentation

泄漏无泄漏

有时,您可能会注意到,Scrapy进程的内存使用量只会增加,但不会减少。不幸的是,这可能发生,即使Scrapy或你的项目都没有泄漏记忆。这是由于一个(不太好)已知的Python问题,在某些情况下可能不会返回释放的内存到操作系统。有关此问题的详细信息,请参阅:

Evan Jones提出的改进(在 this paper 中详述)在Python 2.5中合并,但这只是减少了问题,它不能完全解决它。引用论文:

不幸的是,这个补丁只能释放一个竞技场,如果没有更多的对象分配在它了了。这意味着碎片是一个大问题。应用程序可能有许多兆字节的可用内存,分散在所有的领域,但它将无法释放任何它。这是所有内存分配器经历的问题。解决它的唯一方法是移动到压缩的垃圾收集器,它能够在内存中移动对象。这将需要对Python解释器进行重大更改。

为了保持内存使用合理,您可以将作业拆分为几个较小的作业或启用 持久作业队列 和不时停止/启动spider。