调试内存泄漏¶
在 Scrapy 中,诸如请求、响应和数据项之类的对象具有有限的生命周期:它们被创建、使用一段时间,最后被销毁。
在所有这些对象中,Request 可能是生命周期最长的一个,因为它会一直等待在 Scheduler 队列中,直到轮到它被处理。有关更多信息,请参阅 架构概述。
由于这些 Scrapy 对象具有(相当长)的生命周期,因此始终存在在内存中累积它们而没有正确释放它们的风险,从而导致所谓的“内存泄漏”。
为了帮助调试内存泄漏,Scrapy 提供了一种用于跟踪对象引用的内置机制,称为 trackref,您还可以使用名为 muppy 的第三方库进行更高级的内存调试(有关更多信息,请参见下文)。这两种机制都必须从 Telnet 控制台 使用。
内存泄漏的常见原因¶
这种情况经常发生(有时是意外,有时是故意),Scrapy 开发人员在请求中传递了对象引用(例如,使用 cb_kwargs
或 meta
属性或请求回调函数),并且这实际上将这些引用对象的生存期绑定到请求的生存期。这是迄今为止 Scrapy 项目中内存泄漏最常见的原因,对于新手来说也是一个非常难以调试的问题。
在大型项目中,爬虫通常由不同的人编写,其中一些爬虫可能会“泄漏”,从而在并发运行时影响其他(编写良好的)爬虫,进而影响整个爬取过程。
如果您没有正确释放(先前分配的)资源,则泄漏也可能来自您编写的自定义中间件、管道或扩展。例如,在 spider_opened
上分配资源,但在 spider_closed
上未释放资源,如果您正在运行 每个进程多个爬虫,则可能会导致问题。
请求过多?¶
默认情况下,Scrapy 将请求队列保存在内存中;它包含 Request
对象以及 Request 属性中引用的所有对象(例如,在 cb_kwargs
和 meta
中)。虽然不一定是泄漏,但这可能会占用大量内存。启用 持久作业队列 可以帮助控制内存使用情况。
使用 trackref
调试内存泄漏¶
trackref
是 Scrapy 提供的一个用于调试最常见内存泄漏案例的模块。它基本上跟踪对所有活动 Request、Response、Item、Spider 和 Selector 对象的引用。
您可以进入 telnet 控制台并使用 prefs()
函数检查当前有多少对象(上述类的对象)处于活动状态,该函数是 print_live_refs()
函数的别名。
telnet localhost 6023
.. code-block:: pycon
>>> 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
跟踪的所有对象都来自这些类(及其所有子类)
scrapy.Request
scrapy.Selector
一个真实的例子¶
让我们来看一个假设的内存泄漏案例的具体示例。假设我们有一些爬虫,其中包含类似于以下代码的行
return Request(f"http://www.somenastyspider.com/product.php?pid={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
存在如此多的活动响应(而且它们非常旧)绝对值得怀疑,因为与请求相比,响应应该具有相对较短的生命周期。响应的数量与请求的数量相似,因此看起来它们以某种方式绑定在一起。我们现在可以检查爬虫的代码以发现导致泄漏的代码行(在请求中传递响应引用)。
有时有关活动对象的额外信息可能会有所帮助。让我们检查最旧的响应
>>> 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
模块中可用的函数。
- scrapy.utils.trackref.get_oldest(class_name)[source]¶
返回具有给定类名的最旧的活动对象,如果未找到则返回
None
。首先使用print_live_refs()
获取每个类名的所有跟踪活动对象的列表。
- scrapy.utils.trackref.iter_all(class_name)[source]¶
返回一个迭代器,用于遍历所有具有给定类名的活动对象,如果未找到则返回
None
。首先使用print_live_refs()
获取每个类名下所有已跟踪活动对象的列表。
使用muppy调试内存泄漏¶
trackref
提供了一种非常方便的跟踪内存泄漏的机制,但它只跟踪那些更有可能导致内存泄漏的对象。但是,在其他情况下,内存泄漏也可能来自其他(或多或少模糊)的对象。如果遇到这种情况,并且无法使用trackref
找到泄漏,则还可以使用另一个资源:muppy库。
您可以从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文档。
无泄漏的泄漏¶
有时,您可能会注意到Scrapy进程的内存使用量只会增加,而不会减少。不幸的是,即使Scrapy和您的项目都没有内存泄漏,这种情况也可能发生。这是由于Python的一个(不太为人所知)的问题,在某些情况下,它可能不会将释放的内存返回给操作系统。有关此问题的更多信息,请参阅
Evan Jones提出的改进(在这篇论文中详细介绍)已合并到Python 2.5中,但这只能减少问题,并不能完全解决它。引用论文原文:
不幸的是,此补丁只能在arena中不再分配任何对象时才能释放arena。这意味着碎片化是一个大问题。应用程序可能拥有数兆字节的空闲内存,分散在所有arena中,但它将无法释放其中任何一个。这是所有内存分配器都遇到的问题。解决它的唯一方法是转向压缩垃圾回收器,它能够在内存中移动对象。这需要对Python解释器进行重大更改。
为了使内存消耗保持在合理的范围内,您可以将作业分成几个较小的作业,或者启用持久作业队列并定期停止/启动spider。