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

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

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

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

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

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

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

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

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

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

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

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

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

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

正如所見,報(bào)告也展現(xiàn)了每個(gè)類中最老的對(duì)象的時(shí)間(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)存泄露,那您能找到哪個(gè) spider 正在泄露的機(jī)會(huì)是查看最老的 request 或 response。 您可以使用 get_oldest()方法來(lái)獲取每個(gè)類中最老的對(duì)象, 正如此所示(在終端中)(原文檔沒(méi)有樣例)。

哪些對(duì)象被追蹤了?

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

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

真實(shí)例子

讓我們來(lái)看一個(gè)假設(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 中傳遞了一個(gè) response 的引用,使得 reponse 的生命周期與 request 所綁定, 進(jìn)而造成了內(nèi)存泄露。

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

當(dāng) crawler 運(yùn)行了一小陣子后,我們發(fā)現(xiàn)內(nèi)存占用增長(zhǎng)了很多。 這時(shí)候我們進(jìn)入 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

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

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

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

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

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

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

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

scrapy.utils.trackref 模塊

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

class scrapy.utils.trackref.object_ref

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

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

打印活躍引用的報(bào)告,以類名分類。

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

scrapy.utils.trackref.get_oldest(class_name)

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

scrapy.utils.trackref.iter_all(class_name)

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

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

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

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

easy_install guppy

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

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

Leaks without leaks

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

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

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

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