鍍金池/ 教程/ Python/ 調(diào)試內(nèi)存溢出
Benchmarking
命令行工具(Command line tools)
下載器中間件(Downloader Middleware)
信號(Signals)
Telnet 終端(Telnet Console)
初窺 Scrapy
數(shù)據(jù)收集(Stats Collection)
Scrapyd
通用爬蟲(Broad Crawls)
Item Loaders
試驗階段特性
Scrapy 入門教程
自動限速(AutoThrottle)擴展
Settings
Scrapy 終端(Scrapy shell)
下載項目圖片
DjangoItem
調(diào)試(Debugging)Spiders
選擇器(Selectors)
Feed exports
Spiders Contracts
借助 Firefox 來爬取
Logging
Spiders
Ubuntu 軟件包
實踐經(jīng)驗(Common Practices)
安裝指南
Item Exporters
擴展(Extensions)
Items
Spider 中間件(Middleware)
異常(Exceptions)
例子
發(fā)送 email
架構(gòu)概覽
常見問題(FAQ)
Jobs:暫停,恢復(fù)爬蟲
核心 API
使用 Firebug 進行爬取
Item Pipeline
Link Extractors
Web Service
調(diào)試內(nèi)存溢出

調(diào)試內(nèi)存溢出

在 Scrapy 中,類似 RequestsResponse 及 Items 的對象具有有限的生命周期: 他們被創(chuàng)建,使用,最后被銷毀。

這些對象中,Request 的生命周期應(yīng)該是最長的,其會在調(diào)度隊列(Scheduler queue)中一直等待,直到被處理。更多內(nèi)容請參考架構(gòu)概覽

由于這些 Scrapy 對象擁有很長的生命,因此將這些對象存儲在內(nèi)存而沒有正確釋放的危險總是存在。 而這導(dǎo)致了所謂的”內(nèi)存泄露”。

為了幫助調(diào)試內(nèi)存泄露,Scrapy 提供了跟蹤對象引用的機制,叫做 trackref, 或者您也可以使用第三方提供的更先進內(nèi)存調(diào)試庫 Guppy(更多內(nèi)容請查看下面)。而這都必須在 Telnet 終端中使用。

內(nèi)存泄露的常見原因

內(nèi)存泄露經(jīng)常是由于 Scrapy 開發(fā)者在 Requests 中(有意或無意)傳遞對象的引用(例如,使用 meta 屬性或 request 回調(diào)函數(shù)),使得該對象的生命周期與 Request 的生命周期所綁定。這是目前為止最常見的內(nèi)存泄露的原因,同時對新手來說也是一個比較難調(diào)試的問題。

在大項目中,spider 是由不同的人所編寫的。而這其中有的 spider 可能是有”泄露的”, 當(dāng)所有的爬蟲同時運行時,這些影響了其他(寫好)的爬蟲,最終,影響了整個爬取進程。

與此同時,在不限制框架的功能的同時避免造成這些造成泄露的原因是十分困難的。因此, 我們決定不限制這些功能而是提供調(diào)試這些泄露的實用工具。這些工具回答了一個問題: 哪個 spider 在泄露 。

內(nèi)存泄露可能存在與一個您編寫的中間件,管道(pipeline) 或擴展,在代碼中您沒有正確釋放 (之前分配的)資源。例如,您在 spider_opened 中分配資源但在 spider_closed 中沒有釋放它們。

使用 trackref 調(diào)試內(nèi)存泄露

trackref 是 Scrapy 提供用于調(diào)試大部分內(nèi)存泄露情況的模塊。 簡單來說,其追蹤了所有活動(live)的 Request,Request,Item 及 Selector 對象的引用。

您可以進入 telnet 終端并通過 prefs() 功能來檢查多少(上面所提到的)活躍(alive)對象。 pref()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

正如所見,報告也展現(xiàn)了每個類中最老的對象的時間(age)。 If you’re running multiple spiders per process chances are you can figure out which spider is leaking by looking at the oldest request or response. You can get the oldest object of each class using the get_oldest() function (from the telnet console).

如果您有內(nèi)存泄露,那您能找到哪個 spider 正在泄露的機會是查看最老的 request 或 response。 您可以使用 get_oldest()方法來獲取每個類中最老的對象, 正如此所示(在終端中)(原文檔沒有樣例)。

哪些對象被追蹤了?

trackref 追蹤的對象包括以下類(及其子類)的對象:

  • scrapy.http.Request
  • scrapy.http.Response
  • scrapy.item.Item
  • scrapy.selector.Selector
  • scrapy.spider.Spider

真實例子

讓我們來看一個假設(shè)的具有內(nèi)存泄露的準(zhǔn)確例子。

假如我們有些 spider 的代碼中有一行類似于這樣的代碼:

return Request("http://www.somenastyspider.com/product.php?pid=%d" % product_id,
    callback=self.parse, meta={referer: response}")

代碼中在 request 中傳遞了一個 response 的引用,使得 reponse 的生命周期與 request 所綁定, 進而造成了內(nèi)存泄露。

讓我們來看看如何使用 trackref 工具來發(fā)現(xiàn)哪一個是有問題的 spider(當(dāng)然是在不知道任何的前提的情況下)。

當(dāng) crawler 運行了一小陣子后,我們發(fā)現(xiàn)內(nèi)存占用增長了很多。 這時候我們進入 telnet 終端,查看活躍(live)的引用:

>>> prefs()
Live References

SomenastySpider                     1   oldest: 15s ago
HtmlResponse                     3890   oldest: 265s ago
Selector                            2   oldest: 0s ago
Request                          3878   oldest: 250s ago

上面具有非常多的活躍(且運行時間很長)的 response,而其比 Request 的時間還要長的現(xiàn)象肯定是有問題的。 因此,查看最老的 response:

>>> from scrapy.utils.trackref import get_oldest
>>> r = get_oldest('HtmlResponse')
>>> r.url
'http://www.somenastyspider.com/product.php?pid=123'

就這樣,通過查看最老的 response 的 URL,我們發(fā)現(xiàn)其屬于 somenastyspider.com spider。現(xiàn)在我們可以查看該 spider 的代碼并發(fā)現(xiàn)導(dǎo)致泄露的那行代碼(在 request 中傳遞 response 的引用)。

如果您想要遍歷所有而不是最老的對象,您可以使用 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',
...

很多 spider?

如果您的項目有很多的 spider,prefs() 的輸出會變得很難閱讀。針對于此,該方法具有 ignore 參數(shù),用于忽略特定的類(及其子類)。例如:

>>> from scrapy.spider import Spider
>>> prefs(ignore=Spider)

將不會展現(xiàn)任何 spider 的活躍引用。

scrapy.utils.trackref 模塊

以下是 trackref模塊中可用的方法。

class scrapy.utils.trackref.object_ref

如果您想通過 trackref 模塊追蹤活躍的實例,繼承該類(而不是對象)。

scrapy.utils.trackref.print_live_refs(class_name, ignore=NoneType)

打印活躍引用的報告,以類名分類。

參數(shù): ignore (類或者類的元組) – 如果給定,所有指定類(或者類的元組)的對象將會被忽略。

scrapy.utils.trackref.get_oldest(class_name)

返回給定類名的最老活躍(alive)對象,如果沒有則返回 None。首先使用 print_live_refs()來獲取每個類所跟蹤的所有活躍(live)對象的列表。

scrapy.utils.trackref.iter_all(class_name)

返回一個能給定類名的所有活躍對象的迭代器,如果沒有則返回 None。首先使用 print_live_refs()來獲取每個類所跟蹤的所有活躍(live)對象的列表。

使用 Guppy 調(diào)試內(nèi)存泄露

trackref 提供了追蹤內(nèi)存泄露非常方便的機制,其僅僅追蹤了比較可能導(dǎo)致內(nèi)存泄露的對象 (Requests, Response, Items 及 Selectors)。然而,內(nèi)存泄露也有可能來自其他(更為隱蔽的)對象。 如果是因為這個原因,通過 trackref 則無法找到泄露點,您仍然有其他工具:Guppy library

如果使用 setuptools,您可以通過下列命令安裝 Guppy:

easy_install guppy

telnet 終端也提供了快捷方式(hpy)來訪問 Guppy 堆對象(heap objects)。下面給出了查看堆中所有可用的 Python 對象的例子:

>>> 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.>

您可以看到大部分的空間被字典所使用。接著,如果您想要查看哪些屬性引用了這些字典, 您可以:

>>> 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 模塊十分強大,不過也需要一些關(guān)于 Python 內(nèi)部的知識。關(guān)于 Guppy 的更多內(nèi)容請參考 Guppy documentation。

Leaks without leaks

有時候,您可能會注意到 Scrapy 進程的內(nèi)存占用只在增長,從不下降。不幸的是,有時候這并不是 Scrapy 或者您的項目在泄露內(nèi)存。這是由于一個已知(但不有名)的 Python 問題。Python 在某些情況下可能不會返回已經(jīng)釋放的內(nèi)存到操作系統(tǒng)。關(guān)于這個問題的更多內(nèi)容請看:

改進方案由 Evan Jones 提出,在這篇文章中詳細(xì)介紹,在 Python 2.5 中合并。 不過這僅僅減小了這個問題,并沒有完全修復(fù)。引用這片文章:

不幸的是,這個 patch 僅僅會釋放沒有在其內(nèi)部分配對象的區(qū)域(arena)。這意味著 碎片化是一個大問題。某個應(yīng)用可以擁有很多空閑內(nèi)存,分布在所有的區(qū)域(arena)中,但是沒法釋放任何一個。這個問題存在于所有內(nèi)存分配器中。解決這個問題的唯一辦法是 轉(zhuǎn)化到一個更為緊湊(compact)的垃圾回收器,其能在內(nèi)存中移動對象。這需要對 Python 解析器做一個顯著的修改。

這個問題將會在未來 Scrapy 發(fā)布版本中得到解決。我們打算轉(zhuǎn)化到一個新的進程模型,并在可回收的子進程池中運行 spider。