调试内存泄漏
在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解释器进行重大更改。
为了保持内存消耗的合理性,您可以将作业分成几个较小的作业,也可以不间断地启用持久性作业队列并停止/启动蜘蛛。