调试内存泄漏

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

从所有这些对象,请求可能是具有最长生命周期的请求,因为它在Scheduler队列中保持等待,直到它处理它。欲了解更多信息,请参阅架构概述。

由于这些Scrapy对象具有(相当长的)使用寿命,因此总是存在将其存储在内存中的风险,而不会释放它们,从而导致所谓的“内存泄漏”。

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

内存泄漏的常见原因

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

在大型项目中,蜘蛛通常是由不同的人写的,其中一些蜘蛛可能会“泄漏”,从而影响其他(写得好)的蜘蛛,当它们同时运行时,这又会影响到整个爬行过程。

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

太多请求?

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

使用trackref调试内存泄漏

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

您可以使用与print_live_refs()函数的别名的prefs()函数来输入telnet控制台并检查上述类别中的几个对象当前是否存在。

telnet localhost 6023

>>> prefs()

现场参考

ExampleSpider 1 oldest:15s ago

HtmlResponse 10 oldest:1s ago

选择器2 oldest:0s ago

FormRequest 878最旧:7s ago

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

跟踪哪些对象?

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

scrapy.http.Request

scrapy.http.Response

scrapy.item.Item

scrapy.selector.Selector

scrapy.spiders.Spider

一个真实的例子

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

返回请求(“http://www.somenastyspider.com/product.php?pid=%d”%product\_id,

callback = self.parse,meta = {referer:response})

该行在请求中传递响应引用,该请求有效地将响应生命周期与请求'一致,这肯定​​会导致内存泄漏。

让我们看看我们如何能够通过使用trackref工具来发现原因(当然不用先验)。

爬网程序运行几分钟后,我们注意到其内存使用量已经增长很多,我们可以进入其telnet控制台并查看实时参考:

>>> prefs()

现场参考

SomenastySpider 1 oldest:15s ago

HtmlResponse 3890 oldest:265s ago

选择器2 oldest:0s ago

请求3878最旧:250s ago

事实上有这么多的现场回应(而且他们太老了)肯定是可疑的,因为响应应该比请求的寿命相对较短。答复的数量类似于请求的数量,所以它看起来像是以某种方式绑定的。我们现在可以检查蜘蛛的代码来发现生成泄漏的令人讨厌的行(请求中传递响应引用)。

有时,有关活动对象的额外信息可能会有所帮助。我们来看一下最古老的回应:

如果要迭代所有对象,而不是获取最旧的对象,可以使用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()的输出。因此,该函数具有忽略参数,该参数可用于忽略特定类(及其所有子字段)。例如,这不会显示对蜘蛛的任何实时引用:

>>>从scrapy.spiders进口蜘蛛

>>> 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或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库。

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

pip安装guppy

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

>>> x = hpy.heap()

>>> x.bytype

一组297033个对象的分区。总大小= 52587824字节。

指数计数%大小%累计%类型

 0 22307 8 16423880 31 16423880 31 dict

 1 122285 41 12441544 24 28865424 55 str

 2 68346 23 5966696 11 34832120 66元组

 3 227 0 5836528 11 40668648 77 unicode

 4 2461 1 2222272 4 42890920 82类型

 5 16870 6 2024400 4 44915320 85功能

 6 13949 5 1673880 3 46589200 89 types.CodeType

 7 13422 5 1653104 3 48242304 92列表

 8 3735 1 1173680 2 49415984 94 \_sre.SRE\_Pattern

 9 1209 0 456936 1 49872920 95 scrapy.http.headers.Headers

<1676更多行。类型例如'_.more'来查看

你可以看到大多数空间都是由dicts使用的。那么,如果你想看看哪些属性被引用,你可以这样做:

>>> x.bytype [0] .byvia

一组22307个对象的分区。总大小= 16423880字节。

指数计数%大小%累计%参考通过:

 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'来查看

您可以看到,Guppy模块非常强大,但也需要一些关于Python内部部分的深入知识。有关Guppy的更多信息,请参阅Guppy文档。

泄漏无泄漏

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

Python内存管理

Python内存管理第2部分

Python内存管理第3部分

本文详细介绍的Evan Jones提出的改进已经在Python 2.5中合并,但这只能减少问题,但并不能完全解决它。引用论文:

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

为了保持内存消耗的合理性,您可以将作业分成几个较小的作业,也可以不间断地启用持久性作业队列并停止/启动蜘蛛。

results matching ""

    No results matching ""