# 调试内存泄漏
> 译者:[OSGeo 中国](https://www.osgeo.cn/)
在Scrapy中,请求、响应和项目等对象的生命周期是有限的:它们被创建、使用一段时间,最后被销毁。
从所有这些对象中,请求可能是生命周期最长的请求,因为它一直在调度程序队列中等待,直到需要处理它为止。有关详细信息,请参阅 [体系结构概述](architecture.html#topics-architecture) .
由于这些零碎的物体有(相当长的)寿命,总有在没有正确释放它们的情况下将它们累积到内存中的风险,从而导致所谓的“内存泄漏”。
为了帮助调试内存泄漏,scrapy提供了一种内置机制,用于跟踪调用的对象引用 [trackref](#topics-leaks-trackrefs) ,您还可以使用第三方库 [Guppy](#topics-leaks-guppy) 有关更高级的内存调试(请参阅下面的详细信息)。两种机制都必须从 [Telnet Console](telnetconsole.html#topics-telnetconsole) .
## 内存泄漏的常见原因
Scrapy开发人员传递请求中引用的对象(例如,使用 [`meta`](request-response.html#scrapy.http.Request.meta "scrapy.http.Request.meta") 属性或请求回调函数),它有效地将这些引用对象的生存期限制为请求的生存期。到目前为止,这是导致零碎项目内存泄漏的最常见原因,对于新手来说,这是一个很难调试的原因。
在大型项目中, Spider 通常是由不同的人编写的,其中一些 Spider 可能会“泄漏”,从而在其他(写得好的) Spider 同时运行时影响其他 Spider ,而这反过来又会影响整个爬行过程。
如果您没有正确地释放(以前分配的)资源,那么泄漏也可能来自您编写的定制中间件、管道或扩展。例如,在上分配资源 [`spider_opened`](signals.html#std:signal-spider_opened) 但不释放它们 [`spider_closed`](signals.html#std:signal-spider_closed) 如果你跑步,可能会引起问题 [multiple spiders per process](practices.html#run-multiple-spiders) .
### 请求太多?
默认情况下,scrapy将请求队列保存在内存中;它包括 [`Request`](request-response.html#scrapy.http.Request "scrapy.http.Request") 对象和请求属性中引用的所有对象(例如 [`meta`](request-response.html#scrapy.http.Request.meta "scrapy.http.Request.meta") )虽然不一定是泄漏,但这可能会占用大量内存。有可能 [persistent job queue](jobs.html#topics-jobs) 有助于控制内存使用。
## 使用调试内存泄漏 `trackref`
`trackref` 是Scrapy提供的一个模块,用于调试最常见的内存泄漏情况。它基本上跟踪对所有活动请求、响应、项和选择器对象的引用。
您可以进入telnet控制台并使用 `prefs()` 函数的别名 [`print_live_refs()`](#scrapy.utils.trackref.print_live_refs "scrapy.utils.trackref.print_live_refs") 功能:
```py
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
```
如您所见,该报告还显示了每个类中最旧对象的“年龄”。如果每个进程运行多个spider,那么通过查看最早的请求或响应,您很可能会发现哪个spider正在泄漏。您可以使用 [`get_oldest()`](#scrapy.utils.trackref.get_oldest "scrapy.utils.trackref.get_oldest") 功能(从telnet控制台)。
### 跟踪哪些对象?
被跟踪的对象 `trackrefs` 都来自这些类(及其所有子类):
* [`scrapy.http.Request`](request-response.html#scrapy.http.Request "scrapy.http.Request")
* [`scrapy.http.Response`](request-response.html#scrapy.http.Response "scrapy.http.Response")
* [`scrapy.item.Item`](items.html#scrapy.item.Item "scrapy.item.Item")
* [`scrapy.selector.Selector`](selectors.html#scrapy.selector.Selector "scrapy.selector.Selector")
* [`scrapy.spiders.Spider`](spiders.html#scrapy.spiders.Spider "scrapy.spiders.Spider")
### 一个真实的例子
让我们来看一个假设的内存泄漏案例的具体示例。假设我们有一只 Spider ,上面有一条和这条类似的线:
```py
return Request("http://www.somenastyspider.com/product.php?pid=%d" % product_id,
callback=self.parse, meta={referer: response})
```
该行正在请求中传递一个响应引用,它有效地将响应生命周期与请求的生命周期联系起来,这肯定会导致内存泄漏。
让我们看看如何通过使用 `trackref` 工具。
当爬虫运行几分钟后,我们注意到它的内存使用量增长了很多,我们可以进入它的telnet控制台并检查实时引用:
```py
>>> prefs()
Live References
SomenastySpider 1 oldest: 15s ago
HtmlResponse 3890 oldest: 265s ago
Selector 2 oldest: 0s ago
Request 3878 oldest: 250s ago
```
事实上,存在如此多的实时响应(而且它们太老了),这是绝对可疑的,因为与请求相比,响应的生存期应该相对较短。响应的数量与请求的数量相似,因此看起来它们是以某种方式捆绑在一起的。我们现在可以检查 Spider 的代码,以发现产生泄漏的讨厌的行(在请求中传递响应引用)。
有时,关于活动对象的额外信息可能会有所帮助。让我们检查最早的响应:
```py
>>> 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()`](#scrapy.utils.trackref.iter_all "scrapy.utils.trackref.iter_all") 功能:
```py
>>> 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',
...
```
### Spider 太多了?
如果项目并行执行的spider太多,则 `prefs()` 很难阅读。因此,该函数具有 `ignore` 可用于忽略特定类(及其所有子类)的参数。例如,这不会显示任何对 Spider 的实时引用:
```py
>>> from scrapy.spiders import Spider
>>> prefs(ignore=Spider)
```
### scrapy.utils.trackRef模块
以下是 [`trackref`](#module-scrapy.utils.trackref "scrapy.utils.trackref: Track references of live objects") 模块。
```py
class scrapy.utils.trackref.object_ref
```
如果要使用跟踪活动实例,请从此类(而不是对象)继承 `trackref` 模块。
```py
scrapy.utils.trackref.print_live_refs(class_name, ignore=NoneType)
```
打印实时引用的报告,按类名分组。
| 参数: | **ignore** (_class_ _or_ _classes tuple_) -- 如果给定,则将忽略指定类(或类的元组)中的所有对象。 |
| --- | --- |
```py
scrapy.utils.trackref.get_oldest(class_name)
```
返回具有给定类名的最旧活动对象,或者 `None` 如果没有找到。使用 [`print_live_refs()`](#scrapy.utils.trackref.print_live_refs "scrapy.utils.trackref.print_live_refs") 首先获取每个类名的所有跟踪活动对象的列表。
```py
scrapy.utils.trackref.iter_all(class_name)
```
返回具有给定类名的所有活动对象的迭代器,或者 `None` 如果没有找到。使用 [`print_live_refs()`](#scrapy.utils.trackref.print_live_refs "scrapy.utils.trackref.print_live_refs") 首先获取每个类名的所有跟踪活动对象的列表。
## 用Guppy调试内存泄漏
`trackref` 为跟踪内存泄漏提供了非常方便的机制,但它只跟踪更可能导致内存泄漏的对象(请求、响应、项和选择器)。但是,还有其他一些情况,内存泄漏可能来自其他(或多或少是模糊的)对象。如果这是你的情况,你不能用 `trackref` ,您还有另一个资源: [Guppy library](https://pypi.python.org/pypi/guppy). If you're using Python3, see [用muppy调试内存泄漏](#topics-leaks-muppy).
如果你使用 `pip` ,可以使用以下命令安装Guppy::
```py
pip install guppy
```
telnet控制台还提供内置的快捷方式( `hpy` )用于访问Guppy堆对象。下面是一个使用guppy查看堆中所有可用python对象的示例:
```py
>>> 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.>
```
你可以看到大多数空间都是听写使用的。然后,如果要查看引用这些dict的属性,可以执行以下操作:
```py
>>> 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](http://guppy-pe.sourceforge.net/) .
## 用muppy调试内存泄漏
如果您使用的是python 3,那么可以使用muppy [Pympler](https://pypi.org/project/Pympler/) .
如果你使用 `pip` ,可以使用以下命令安装muppy::
```py
pip install Pympler
```
下面是一个使用muppy查看堆中所有可用python对象的示例:
```py
>>> 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 documentation](https://pythonhosted.org/Pympler/muppy.html) .
## 无泄漏泄漏
有时,您可能会注意到您的废进程的内存使用只会增加,但不会减少。不幸的是,即使Scrapy和您的项目都没有泄漏内存,也可能发生这种情况。这是由于Python的一个(不太常见)已知问题造成的,在某些情况下,该问题可能不会将释放的内存返回到操作系统。有关此问题的详细信息,请参阅:
* [Python Memory Management](http://www.evanjones.ca/python-memory.html)
* [Python Memory Management Part 2](http://www.evanjones.ca/python-memory-part2.html)
* [Python Memory Management Part 3](http://www.evanjones.ca/python-memory-part3.html)
Evan Jones提出的改进建议,详情见 [this paper](http://www.evanjones.ca/memoryallocator/) 在python 2.5中进行了合并,但这只会减少问题,并不能完全解决问题。引用论文:
> _不幸的是,这个补丁只能在竞技场中不再分配对象的情况下释放竞技场。这意味着碎片化是一个大问题。一个应用程序可以有许多兆字节的空闲内存,分散在所有的区域中,但是它将无法释放其中的任何一个。这是所有内存分配器都遇到的问题。解决这个问题的唯一方法是移动到一个压缩垃圾收集器,它能够移动内存中的对象。这需要对python解释器进行重大更改。_
为了保持内存消耗合理,可以将作业拆分为几个较小的作业或启用 [persistent job queue](jobs.html#topics-jobs) 不时停止/启动Spider。
- 简介
- 第一步
- Scrapy at a glance
- 安装指南
- Scrapy 教程
- 实例
- 基本概念
- 命令行工具
- Spider
- 选择器
- 项目
- 项目加载器
- Scrapy shell
- 项目管道
- Feed 导出
- 请求和响应
- 链接提取器
- 设置
- 例外情况
- 内置服务
- Logging
- 统计数据集合
- 发送电子邮件
- 远程登录控制台
- Web服务
- 解决具体问题
- 常见问题
- 调试spiders
- Spider 合约
- 常用做法
- 通用爬虫
- 使用浏览器的开发人员工具进行抓取
- 调试内存泄漏
- 下载和处理文件和图像
- 部署 Spider
- AutoThrottle 扩展
- Benchmarking
- 作业:暂停和恢复爬行
- 延伸 Scrapy
- 体系结构概述
- 下载器中间件
- Spider 中间件
- 扩展
- 核心API
- 信号
- 条目导出器
- 其余所有
- 发行说明
- 为 Scrapy 贡献
- 版本控制和API稳定性