# 調試內存泄漏
> 譯者:[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穩定性