在 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)存泄露經(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
是 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)有樣例)。
trackref 追蹤的對(duì)象包括以下類(及其子類)的對(duì)象:
讓我們來(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',
...
如果您的項(xiàng)目有很多的 spider,prefs() 的輸出會(huì)變得很難閱讀。針對(duì)于此,該方法具有 ignore 參數(shù),用于忽略特定的類(及其子類)。例如:
>>> from scrapy.spider import Spider
>>> prefs(ignore=Spider)
將不會(huì)展現(xiàn)任何 spider 的活躍引用。
以下是 trackref
模塊中可用的方法。
如果您想通過(guò) trackref 模塊追蹤活躍的實(shí)例,繼承該類(而不是對(duì)象)。
打印活躍引用的報(bào)告,以類名分類。
參數(shù): ignore (類或者類的元組) – 如果給定,所有指定類(或者類的元組)的對(duì)象將會(huì)被忽略。
返回給定類名的最老活躍(alive)對(duì)象,如果沒(méi)有則返回 None。首先使用 print_live_refs()
來(lái)獲取每個(gè)類所跟蹤的所有活躍(live)對(duì)象的列表。
返回一個(gè)能給定類名的所有活躍對(duì)象的迭代器,如果沒(méi)有則返回 None
。首先使用 print_live_refs()
來(lái)獲取每個(gè)類所跟蹤的所有活躍(live)對(duì)象的列表。
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。
有時(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。