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