Debugging memory leaks (调试内存泄漏)
在 Scrapy 中,诸如请求(requests)、响应(responses)和项目(items)之类的对象的生命周期是有限的:它们被创建、使用一段时间,最后被销毁。
在所有这些对象中,Request 可能是生命周期最长的一个,因为它会在调度器队列中等待,直到需要处理它为止。有关更多信息,请参阅架构概览。
由于这些 Scrapy 对象的生命周期(相当长),总是存在将它们累积在内存中而没有正确释放它们的风险,从而导致所谓的“内存泄漏”。
为了帮助调试内存泄漏,Scrapy 提供了一个用于跟踪对象引用的内置机制,称为 trackref,您还可以使用名为 muppy 的第三方库进行更高级的内存调试(有关更多信息,请参见下文)。这两种机制都必须从 Telnet 控制台中使用。
Common causes of memory leaks (内存泄漏的常见原因)
经常发生(有时是偶然,有时是故意)Scrapy 开发者在请求中传递被引用的对象(例如,使用 cb_kwargs 或 meta 属性或请求回调函数),这有效地将这些被引用对象的生命周期绑定到请求的生命周期。这是 Scrapy 项目中迄今为止最常见的内存泄漏原因,对于新手来说也相当难以调试。
在大型项目中,爬虫通常由不同的人编写,其中一些爬虫可能会“泄漏”,从而在并发运行时影响其他(编写良好)的爬虫,进而影响整个爬取过程。
泄漏也可能来自您编写的自定义中间件、管道或扩展,如果您没有正确释放(先前分配的)资源。例如,如果您在每个进程中运行多个爬虫,在 spider_opened 上分配资源但不在 spider_closed 上释放它们可能会导致问题。
Too Many Requests? (请求过多?)
默认情况下,Scrapy 将请求队列保留在内存中;它包括 Request 对象和 Request 属性中引用的所有对象(例如在 cb_kwargs 和 meta 中)。虽然不一定是泄漏,但这会占用大量内存。启用持久作业队列可能有助于控制内存使用。
Debugging memory leaks with trackref (使用 trackref 调试内存泄漏)
trackref 是 Scrapy 提供的一个模块,用于调试最常见的内存泄漏情况。它基本上跟踪所有活动的 Request、Response、Item、Spider 和 Selector 对象的引用。
您可以进入 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 控制台)获取每个类中最旧的对象。
Which objects are tracked? (哪些对象被跟踪?)
trackrefs 跟踪的对象都来自这些类(及其所有子类):
scrapy.Requestscrapy.http.Responsescrapy.Itemscrapy.Selectorscrapy.Spider
A real example (一个真实示例)
让我们看一个假设的内存泄漏案例的具体示例。假设我们有一个爬虫,其中有一行类似这样的代码:
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',
...]
Too many spiders? (爬虫太多?)
如果您的项目中有太多爬虫并行执行,prefs() 的输出可能难以阅读。因此,该函数有一个 ignore 参数,可用于忽略特定类(及其所有子类)。例如,这不会显示任何对爬虫的活动引用:
>>> from scrapy.spiders import Spider
>>> prefs(ignore=Spider)
scrapy.utils.trackref module (scrapy.utils.trackref 模块)
以下是 trackref 模块中可用的函数。
class scrapy.utils.trackref.object_ref[source]
如果要使用 trackref 模块跟踪活动实例,请从这个类继承。
scrapy.utils.trackref.print_live_refs(class_name, ignore=NoneType)[source]
打印按类名分组的活动引用报告。
参数:
- ignore (
type或tuple) – 如果给定,则指定类(或类元组)中的所有对象都将被忽略。
scrapy.utils.trackref.get_oldest(class_name)[source]
返回给定类名下最旧的活动对象,如果没有找到则返回 None。请先使用 print_live_refs() 获取按类名分类的所有被跟踪活动对象的列表。
scrapy.utils.trackref.iter_all(class_name)[source]
返回给定类名下所有活动对象的迭代器,如果没有找到则返回 None。请先使用 print_live_refs() 获取按类名分类的所有被跟踪活动对象的列表。
Debugging memory leaks with muppy (使用 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 文档。
Leaks without leaks (没有泄漏的泄漏)
有时,您可能会注意到 Scrapy 进程的内存使用量只会增加,但从不减少。不幸的是,即使 Scrapy 和您的项目都没有内存泄漏,这种情况也可能发生。这是由于 Python 的一个(不太为人所知)问题,即在某些情况下可能不会将释放的内存返回给操作系统。有关此问题的更多信息,请参阅:
Evan Jones 提出的改进,详见这篇论文,已合并到 Python 2.5 中,但这只是减轻了问题,并没有完全解决。引用论文中的话:
不幸的是,此补丁只能在其中不再分配任何对象时释放一个 arena。这意味着碎片化是一个大问题。应用程序可能有许多兆字节的空闲内存,分散在所有 arena 中,但它将无法释放任何内存。这是所有内存分配器都会遇到的问题。解决它的唯一方法是转向紧凑型垃圾回收器,它能够移动内存中的对象。这将需要对 Python 解释器进行重大更改。
为了保持合理的内存消耗,您可以将作业分成几个较小的作业,或者启用持久作业队列并不时停止/启动爬虫。