序言
第1章 Scrapy介紹
第2章 理解HTML和XPath
第3章 爬蟲基礎(chǔ)
第4章 從Scrapy到移動應(yīng)用
第5章 快速構(gòu)建爬蟲
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy編程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和實時分析
通常,很容易將性能理解錯玻驻。對于Scrapy姆打,幾乎一定會把它的性能理解錯统扳,因為這里有許多反直覺的地方挚赊。除非你對Scrapy的結(jié)構(gòu)有清楚的了解突雪,你會發(fā)現(xiàn)努力提升Scrapy的性能卻收效甚微交播。這就是處理高性能摔敛、低延遲、高并發(fā)環(huán)境的復(fù)雜之處掷漱。對于優(yōu)化瓶頸粘室, Amdahl定律仍然適用,但除非找到真正的瓶頸卜范,吞吐量并不會增加衔统。要想學(xué)習(xí)更多,可以看Dr.Goldratt的《目標》這本書海雪,其中用比喻講到了更多關(guān)于瓶延遲锦爵、吞吐量的知識。本章就是來幫你確認Scrapy配置的瓶頸所在奥裸,讓你避免明顯的錯誤险掀。
請記住,本章相對較難湾宙,涉及到許多數(shù)學(xué)樟氢。但計算還算比較簡單,并且有圖表示意侠鳄。如果你不喜歡數(shù)學(xué)埠啃,可以直接忽略公式,這樣仍然可以搞明白Scrapy的性能是怎么回事伟恶。
Scrapy的引擎——一個直觀的方法
并行系統(tǒng)看起來就像管道系統(tǒng)碴开。在計算機科學(xué)中,我們使用隊列符表示隊列并處理元素(見圖1的左邊)博秫。隊列系統(tǒng)的基本定律是Little定律叹螟,它指明平衡狀態(tài)下鹃骂,隊列系統(tǒng)中的總元素個數(shù)(N)等于吞吐量(T)乘以總排隊/處理時間(S)台盯,即N=T*S罢绽。另外兩種形式,T=N/S和S=N/T也十分有用静盅。
管道(圖1的右邊)在幾何學(xué)上也有一個相似的定律蒿叠。管道的體積(V)等于長度(L)乘以橫截面積(A)明垢,即V=L*A。
如果假設(shè)L代表處理時間S(L≈S)市咽,體積代表總元素個數(shù)(V≈N)痊银,橫截面積啊代表吞吐量(A≈T),Little定律和體積公式就是相同的施绎。
提示:這個類比合理嗎溯革?答案是基本合理。如果我們想象小液滴在管道中以勻速流過谷醉,那么L≈S 就完全合理致稀,因為管道越長,液滴流過的時間也越長俱尼。V≈N 也是合理的抖单,因為管道越大,它能容下的液滴越多遇八。但是矛绘,我們可以通過增大壓力的方法,壓入更多的液滴刃永。A≈T是真正的類比货矮。在管道中,吞吐量是每秒流進/流出的液滴總數(shù)揽碘,被稱為體積流速次屠,在正常的情況下,它與A^2成正比雳刺。這是因為更寬的管道不僅意味更多的液體流出劫灶,還具有更快的速度,因為管壁之間的空間變大了掖桦。但對于這一章本昏,我們可以忽略這一點,假設(shè)壓力和速度是不變的枪汪,吞吐量只與橫截面積成正比涌穆。
Little定律與體積公式十分相似怔昨,所以管道模型直觀上是正確的。再看看圖1中的右半部宿稀。假設(shè)管道代表Scrapy的下載器趁舀。第一個十分細的管道,它的總體積/并發(fā)等級(N)=8個并發(fā)請求祝沸。長度/延遲(S)對于一個高速網(wǎng)站矮烹,假設(shè)為S=250ms。現(xiàn)在可以計算橫街面積/吞吐量T=N/S=8/0.25=32請求/秒罩锐。
可以看到奉狈,延遲率是手遠程服務(wù)器和網(wǎng)絡(luò)延遲的影響,不受我們控制涩惑。我們可以控制的是下載器的并發(fā)等級(N)仁期,將8提升到16或32,如圖1所示竭恬。對于固定的管道長度(這也不受我們控制)跛蛋,我們只能增加橫截面積來增加體積,即增加吞吐量萍聊。用Little定律來講问芬,并發(fā)如果是16個請求,就有T=N/S=16/0.25=64請求/秒寿桨,并發(fā)32個請求此衅,就有T=N/S=32/0.25=128請求/秒。貌似如果并發(fā)數(shù)無限大亭螟,吞吐量也就無限大挡鞍。在得出這個結(jié)論之前,我們還得考慮一下串聯(lián)排隊系統(tǒng)预烙。
串聯(lián)排隊系統(tǒng)
當你將橫截面積/吞吐量不同的管道連接起來時墨微,直觀上,人們會認為總系統(tǒng)會受限于最窄的管道(最小的吞吐量T)扁掸,見圖2翘县。
你還可以看到最窄的管道(即瓶頸)放在不同的地方,可以影響其他管道的填充程度谴分。如果將填充程度類比為系統(tǒng)內(nèi)存需求锈麸,瓶頸的擺放就十分重要了。最好能將填充程度達到最高牺蹄,這樣單位工作的花費最小忘伞。在Scrapy中,單位工作(抓取一個網(wǎng)頁)大體包括下載器之前的一條URL(幾個字節(jié))和下載器之后的URL和服務(wù)器響應(yīng)。
提示:這就是為什么氓奈,Scrapy把瓶頸放在下載器翘魄。
確認瓶頸
用管道系統(tǒng)的比喻,可以直觀的確認瓶頸所在舀奶。查看圖2暑竟,你可以看到瓶頸之前都是滿的,瓶頸之后就不是滿的伪节。
對于大多數(shù)系統(tǒng)光羞,可以用系統(tǒng)的性能指標監(jiān)測排隊系統(tǒng)是否擁擠。通過檢測Scrapy的隊列怀大,我們可以確定出瓶頸的所在,如果瓶頸不是在下載器的話呀闻,我們可以通過調(diào)整設(shè)置使下載器成為瓶頸化借。瓶頸沒有得到優(yōu)化,吞吐量就不會有優(yōu)化盟步。調(diào)整其它部分只會使系統(tǒng)變得更糟莉炉,很可能將瓶頸移到別處肿孵。所以在修改代碼和配置之前,你必須找到瓶頸蒜焊。你會發(fā)現(xiàn)在大多數(shù)情況下,包括本書中的例子科贬,瓶頸的位置都和預(yù)想的不同泳梆。
Scrapy的性能模型
讓我們回到Scrapy,詳細查看它的性能模型榜掌,見圖3优妙。
Scrapy包括以下部分:
- 調(diào)度器:大量的Request在這里排隊,直到下載器處理它們憎账。其中大部分是URL套硼,因此體積不大,也就是說即便有大量請求存在胞皱,也可以被下載器及時處理邪意。
- 阻塞器:這是抓取器由后向前進行反饋的一個安全閥,如果進程中的響應(yīng)大于5MB反砌,阻塞器就會暫停更多的請求進入下載器雾鬼。這可能會造成性能的波動。
- 下載器:這是對Scrapy的性能最重要的組件于颖。它用復(fù)雜的機制限制了并發(fā)數(shù)呆贿。它的延遲(管道長度)等于遠程服務(wù)器的響應(yīng)時間,加上網(wǎng)絡(luò)/操作系統(tǒng)、Python/Twisted的延遲做入。我們可以調(diào)節(jié)并發(fā)請求數(shù)冒晰,但是對其它延遲無能為力。下載器的能力受限于CONCURRENT_REQUESTS*設(shè)置竟块。
- 爬蟲:這是抓取器將Response變?yōu)镮tem和其它Request的組件壶运。只要我們遵循規(guī)則來寫爬蟲,通常它不是瓶頸浪秘。
- Item Pipelines:這是抓取器的第二部分蒋情。我們的爬蟲對每個Request可能產(chǎn)生幾百個Items,只有CONCURRENT_ITEMS會被并行處理耸携。這一點很重要棵癣,因為,如果你用pipelines連接數(shù)據(jù)庫夺衍,你可能無意地向數(shù)據(jù)庫導(dǎo)入數(shù)據(jù)狈谊,pipelines的默認值(100)就會看起來很少。
爬蟲和pipelines的代碼是異步的沟沙,會包含必要的延遲河劝,但二者不會是瓶頸。爬蟲和pipelines很少會做繁重的處理工作矛紫。如果是的話赎瞎,服務(wù)器的CPU則是瓶頸。
使用遠程登錄控制組件
為了理解Requests/Items是如何在管道中流動的颊咬,我們現(xiàn)在還不能真正的測量流動务甥。然而,我們可以檢測在Scrapy的每個階段贪染,有多少個Requests/Responses/Items缓呛。
通過Scrapy運行遠程登錄,我們就可以得到性能信息杭隙。我們可以在6023端口運行遠程登錄命令哟绊。然后,會在Scrapy中出現(xiàn)一個Python控制臺痰憎。注意票髓,如果在這里進行中斷操作,比如time.sleep()铣耘,就會暫停爬蟲洽沟。通過內(nèi)建的est()函數(shù),可以查看一些有趣的信息蜗细。其中一些或是非常專業(yè)的裆操,或是可以從核心數(shù)據(jù)推導(dǎo)出來怒详。本章后面會展示后者。下面運行一個例子踪区。當我們運行一個爬蟲時昆烁,我們在開發(fā)機打開第二臺終端,在端口6023遠程登錄缎岗,然后運行est()静尼。
提示:本章代碼位于目錄ch10。這個例子位于ch10/speed传泊。
在第一臺終端鼠渺,運行如下命令:
$ pwd
/root/book/ch10/speed
$ ls
scrapy.cfg speed
$ scrapy crawl speed -s SPEED_PIPELINE_ASYNC_DELAY=1
INFO: Scrapy 1.0.3 started (bot: speed)
...
現(xiàn)在先不關(guān)注scrapy crawl speed和它的參數(shù)的意義,后面會詳解眷细。在第二臺終端拦盹,運行如下代碼:
$ telnet localhost 6023
>>> est()
...
len(engine.downloader.active) : 16
...
len(engine.slot.scheduler.mqs) : 4475
...
len(engine.scraper.slot.active) : 115
engine.scraper.slot.active_size : 117760
engine.scraper.slot.itemproc_size : 105
然后在第二臺終端按Ctrl+D退出遠程登錄,返回第一臺終端按Ctrl+C停止抓取薪鹦。
提示:我們現(xiàn)在忽略dqs掌敬。如果你通過設(shè)置JOBDIR打開了持久支持,你會得到非零的dqs(len(engine.slot.scheduler.dqs))池磁,你應(yīng)該將它添加到mqs的大小中。
讓我們查看這個例子中的數(shù)據(jù)的意義楷兽。mqs指出調(diào)度器中等待的項目很少(4475個請求)地熄。len(engine.downloader.active)指出下載器現(xiàn)在正在下載16個請求端考。這與我們在CONCURRENT_REQUESTS的設(shè)置相同却特。len(engine.scraper.slot.active)說明現(xiàn)在正有115個響應(yīng)在抓取器中處理。 (engine.scraper.slot.active_size)告訴我們這些響應(yīng)的大小是115kb。除了響應(yīng)仙蛉,105個Items正在pipelines(engine.scraper.slot.itemproc_size)中處理,這說明還有10個在爬蟲中趁餐。經(jīng)過總結(jié)澎怒,我們看到瓶頸是下載器喷面,在下載器之前有很長的任務(wù)隊列(mqs),下載器在滿負荷運轉(zhuǎn)盒齿;下載器之后,工作量較高并有一定波動符匾。
另一個可以查看信息的地方是stats對象垛贤,抓取之后打印的內(nèi)容某饰。我們可以以dict的形式訪問它露乏,只需通過via stats.get_stats()遠程登錄,用p()函數(shù)打永徒稀:
$ p(stats.get_stats())
{'downloader/request_bytes': 558330,
...
'item_scraped_count': 2485,
...}
這里對我們最重要的是item_scraped_count观蜗,它可以通過stats.get_value ('item_scraped_count')之間訪問。它告訴我們現(xiàn)在已經(jīng)抓取了多少個items墓捻,以及增長的速率,即吞吐量砖第。
評分系統(tǒng)
我為本章寫了一個簡單的評分系統(tǒng),它可以讓我們評估在不同場景下的性能梧兼。它的代碼有些復(fù)雜放吩,你可以在speed/spiders/speed.py找到羽杰,但我們不會深入講解它。
這個評分系統(tǒng)包括:
- 服務(wù)器上http://localhost:9312/benchmark/...的句柄(handlers)集灌。我們可以控制這個假網(wǎng)站的結(jié)構(gòu)(見圖4)腌零,通過調(diào)節(jié)URL參數(shù)/Scrapy設(shè)置梯找,控制網(wǎng)頁加載的速度。不用在意細節(jié)益涧,我們接下來會看許多例子⌒獯福現(xiàn)在,先看一下http://localhost:9312/benchmark/index?p=1和http://localhost:9312/benchmark/id:3/rr:5/index?p=1的不同闲询。第一個網(wǎng)頁在半秒內(nèi)加載完畢久免,每頁只含有一個item,第二個網(wǎng)頁加載用了五秒扭弧,每頁有三個items阎姥。我們還可以在網(wǎng)頁上添加垃圾信息,降低加載速度鸽捻。例如呼巴,查看http://localhost:9312/benchmark/ds:100/detail?id0=0泽腮。默認條件下(見speed/settings.py),頁面渲染用時SPEED_T_RESPONSE = 0.125秒衣赶,假網(wǎng)站有SPEED_TOTAL_ITEMS = 5000個Items诊赊。
- 爬蟲,SpeedSpider府瞄,模擬用幾種方式取回被SPEED_START_REQUESTS_STYLE控制的start_requests()碧磅,并給出一個parse_item()方法。默認下遵馆,用crawler.engine.crawl()方法將所有起始URL提供給調(diào)度器鲸郊。
- pipeline,DummyPipeline团搞,模擬了一些處理過程严望。它可以引入四種不同的延遲類型。阻塞/計算/同步延遲(SPEED_PIPELINE_BLOCKING_DELAY—很差)逻恐,異步延遲(SPEED_PIPELINE_ASYNC_DELAY—不錯)像吻,使用遠程treq庫進行API調(diào)用(SPEED_PIPELINE_API_VIA_TREQ—不錯),和使用Scrapy的crawler.engine.download()進行API調(diào)用(SPEED_PIPELINE_API_VIA_DOWNLOADER—不怎么好)复隆。默認時拨匆,pipeline不添加延遲。
- settings.py中的一組高性能設(shè)置挽拂。關(guān)閉任何可能使系統(tǒng)降速的項惭每。因為只在本地服務(wù)器運行,我們還關(guān)閉了每個域的請求限制亏栈。
- 一個可以記錄數(shù)據(jù)的擴展台腥,和第8章中的類似。它每隔一段時間绒北,就打印出核心數(shù)據(jù)黎侈。
在上一個例子,我們已經(jīng)用過了這個系統(tǒng)闷游,讓我們重新做一次模擬峻汉,并使用Linux的計時器測量總共的執(zhí)行時間。核心數(shù)據(jù)打印如下:
$ time scrapy crawl speed
...
INFO: s/edule d/load scrape p/line done mem
INFO: 0 0 0 0 0 0
INFO: 4938 14 16 0 32 16384
INFO: 4831 16 6 0 147 6144
...
INFO: 119 16 16 0 4849 16384
INFO: 2 16 12 0 4970 12288
...
real 0m46.561s
Column Metric
s/edule len(engine.slot.scheduler.mqs)
d/load len(engine.downloader.active)
scrape len(engine.scraper.slot.active)
p/line engine.scraper.slot.itemproc_size
done stats.get_value('item_scraped_count')
mem engine.scraper.slot.active_size
結(jié)果這樣顯示出來效果很好脐往。調(diào)度器中初始有5000條URL休吠,結(jié)束時done的列也有5000條。下載器全負荷下并發(fā)數(shù)是16业簿,與設(shè)置相同瘤礁。抓取器主要是爬蟲,因為pipeline是空的辖源,它沒有滿負荷運轉(zhuǎn)蔚携。它用46秒抓取了5000個Items希太,并發(fā)數(shù)是16,即每個請求的處理時間是46*16/5000=147ms酝蜒,而不是預(yù)想的125ms誊辉,滿足要求。
標準性能模型
當Scrapy正常運行且下載器為瓶頸時亡脑,就是Scrapy的標準性能模型堕澄。此時,調(diào)度器有一定數(shù)量的請求霉咨,下載器滿負荷運行蛙紫。抓取器負荷不滿,并且加載的響應(yīng)不會持續(xù)增加途戒。
三項設(shè)置負責(zé)控制下載器的性能: CONCURRENT_REQUESTS坑傅,CONCURRENT_REQUESTS_PER_DOMAIN和CONCURRENT_REQUESTS_PER_IP。第一個是宏觀上的控制喷斋,無論任何時候唁毒,并發(fā)數(shù)都不能超過CONCURRENT_REQUESTS。另外星爪,如果是單域或幾個域浆西,CONCURRENT_REQUESTS_PER_DOMAIN 也可以限制活躍請求數(shù)。如果你設(shè)置了CONCURRENT_REQUESTS_PER_IP顽腾,CONCURRENT_REQUESTS_PER_DOMAIN就會被忽略近零,活躍請求數(shù)就是每個IP的請求數(shù)量。對于共享站點抄肖,比如久信,多個域名指向一個服務(wù)器,這可以幫助你降低服務(wù)器的載荷漓摩。
為了更簡明的分析入篮,現(xiàn)在把per-IP的限制關(guān)閉,即使CONCURRENT_REQUESTS_PER_IP為默認值(0)幌甘,并設(shè)置CONCURRENT_REQUESTS_PER_DOMAIN為一個超大值(1000000)。這樣就可以無視其它的設(shè)置痊项,讓下載器的并發(fā)數(shù)完全受CONCURRENT_REQUESTS控制锅风。
我們希望吞吐量取決于下載網(wǎng)頁的平均時間,包括遠程服務(wù)器和我們系統(tǒng)(Linux鞍泉、Twisted/Python)的延遲皱埠,tdownload=tresponse+toverhead。還可以加上啟動和關(guān)閉的時間咖驮。這包括從取得響應(yīng)到Items離開pipeline的時間边器,和取得第一個響應(yīng)的時間训枢,還有空緩存的內(nèi)部損耗。
總之忘巧,如果你要完成N個請求恒界,在爬蟲正常的情況下,需要花費的時間是:
所幸的是砚嘴,我們只需控制一部分參數(shù)就可以了十酣。我們可以用一臺更高效的服務(wù)器控制toverhead,和tstart/stop际长,但是后者并不值得耸采,因為每次運行只影響一次。除此之外工育,最值得關(guān)注的就是CONCURRENT_REQUESTS虾宇,它取決于我們?nèi)绾问褂梅?wù)器。如果將其設(shè)置成一個很大的值如绸,在某一時刻就會使服務(wù)器或我們電腦的CPU滿負荷嘱朽,這樣響應(yīng)就會不及時,tresponse會急劇升高竭沫,因為網(wǎng)站會阻塞燥翅、屏蔽進一步的訪問,或者服務(wù)器會崩潰蜕提。
讓我們驗證一下這個理論森书。我們抓取2000個items,tresponse∈{0.125s谎势,0.25s凛膏,0.5s},CONCURRENT_REQUESTS∈{8脏榆,16猖毫,32,64}:
$ for delay in 0.125 0.25 0.50; do for concurrent in 8 16 32 64; do
time scrapy crawl speed -s SPEED_TOTAL_ITEMS=2000 \
-s CONCURRENT_REQUESTS=$concurrent -s SPEED_T_RESPONSE=$delay
done; done
在我的電腦上须喂,我完成2000個請求的時間如下:
接下來復(fù)雜的數(shù)學(xué)推導(dǎo)吁断,可以跳過。在圖5中坞生,可以看到一些結(jié)果仔役。將上一個公式變形為y=toverhead·x+ tstart/stop,其中x=N/CONCURRENT_REQUESTS是己, y=tjob·x+tresponse又兵。使用最小二乘法(LINEST Excel函數(shù))和前面的數(shù)據(jù),可以計算出toverhead=6ms,tstart/stop=3.1s沛厨。toverhead可以忽略宙地,但是開始時間相對較長,最好是在數(shù)千條URL時長時間運行逆皮。因此宅粥,可以估算出吞吐量公式是:
處理N個請求,我們可以估算tjob页屠,然后可以直接求出T粹胯。
解決性能問題
現(xiàn)在我們已經(jīng)明白如何使Scrapy的性能最大化,讓我們來看看如何解決實際問題辰企。我們會通過探究癥狀风纠、運行錯誤、討論原因牢贸、修復(fù)問題竹观,討論幾個實例。呈現(xiàn)的順序是從系統(tǒng)性的問題到Scrapy的小技術(shù)問題潜索,也就是說臭增,更為常見的問題可能會排在后面。請閱讀全部章節(jié)竹习,再開始處理你自己的問題誊抛。
實例1——CPU滿負荷
癥狀:當你提高并發(fā)數(shù)時,性能并沒有提高整陌。當你降低并發(fā)數(shù)拗窃,一切工作正常。下載器沒有問題泌辫,但是每個請求花費時間太長随夸。用Unix/Linux命令ps或Windows的任務(wù)管理器查看CPU的情況,CPU的占用率非常高震放。
案例:假設(shè)你運行如下命令:
$ for concurrent in 25 50 100 150 200; do
time scrapy crawl speed -s SPEED_TOTAL_ITEMS=5000 \
-s CONCURRENT_REQUESTS=$concurrent
done
求得抓取5000條URL的時間宾毒。預(yù)計時間是用之前推導(dǎo)的公式求出的,CPU是用命令查看得到的(可以在另一臺終端運行查看命令):
在我們的試驗中,我們沒有進行任何處理工作墨礁,所以并發(fā)數(shù)可以很高癌瘾。在實際中,很快就可以看到性能趨緩的情況發(fā)生饵溅。
討論:Scrapy使用的是單線程,當并發(fā)數(shù)很高時妇萄,CPU可能會成為瓶頸蜕企。假設(shè)沒有使用線程池咬荷,CPU的使用率建議是80-90%∏嵫冢可能你還會碰到其他系統(tǒng)性問題幸乒,比如帶寬、內(nèi)存唇牧、硬盤吞吐量罕扎,但是發(fā)生這些狀況的可能性比較小,并且不屬于系統(tǒng)管理丐重,所以就不贅述了腔召。
解決:假設(shè)你的代碼已經(jīng)是高效的。你可以通過在一臺服務(wù)器上運行多個爬蟲扮惦,使累積并發(fā)數(shù)超過CONCURRENT_REQUESTS臀蛛。這可以充分利用CPU的性能。如果還想提高并發(fā)數(shù)崖蜜,你可以使用多臺服務(wù)器(見11章)浊仆,這樣就可以使用更多的內(nèi)存、帶寬和硬盤吞吐量豫领。檢查CPU的使用情況是你的首要關(guān)切抡柿。
實例2-阻塞代碼
癥狀:系統(tǒng)的運行得十分奇怪。比起預(yù)期的速度等恐,系統(tǒng)運行的十分緩慢洲劣。改變并發(fā)數(shù),也沒有效果鼠锈。下載器幾乎是空的(遠小于并發(fā)數(shù))闪檬,抓取器的響應(yīng)數(shù)很少。
案例:使用兩個評分設(shè)置购笆,SPEED_SPIDER_BLOCKING_DELAY和SPEED_PIPELINE_BLOCKING_DELAY(二者效果相同)粗悯,使每個響應(yīng)有100ms的阻塞延遲。在給定的并發(fā)數(shù)下同欠,100條URL大概要2到3秒样傍,但結(jié)果總是13秒左右,并且不受并發(fā)數(shù)影響:
for concurrent in 16 32 64; do
time scrapy crawl speed -s SPEED_TOTAL_ITEMS=100 \
-s CONCURRENT_REQUESTS=$concurrent -s SPEED_SPIDER_BLOCKING_DELAY=0.1
done
討論:任何阻塞代碼都會是并發(fā)數(shù)無效铺遂,并使得CONCURRENT_REQUESTS=1衫哥。公式:100URL*100ms(阻塞延遲)=10秒+tstart/stop,完美解釋了發(fā)生的狀況襟锐。
無論阻塞代碼位于pipelines還是爬蟲撤逢,你都會看到抓取器滿負荷,它之前和之后的部分都是空的∥萌伲看起來這違背了我們之前講的初狰,但是由于我們并沒有一個并行系統(tǒng),pipeline的規(guī)則此處并不適用互例。這個錯誤很容易犯(例如奢入,使用了阻塞APIs),然后就會出現(xiàn)之前的狀況媳叨。相似的討論也適用于計算復(fù)雜的代碼腥光。應(yīng)該為每個代碼使用多線程,如第9章所示糊秆,或在Scrapy的外部批次運行武福,第11章會看到例子。
解決:假設(shè)代碼是繼承而來的扩然,你并不知道阻塞代碼位于何處艘儒。沒有pipelines系統(tǒng)也能運行的話,使pipeline無效夫偶,看系統(tǒng)能否正常運行界睁。如果是的話,說明阻塞代碼位于pipelines兵拢。如果不是的話翻斟,逐一恢復(fù)pipelines,看問題何時發(fā)生说铃。如果必須所有組件都在運行访惜,整個系統(tǒng)才能運行的話,給每個pipeline階段添加日志消息(或者插入可以打印時間戳的偽pipelines)腻扇,就可以發(fā)現(xiàn)哪一步花費的時間最多债热。如果你想要一個長期可重復(fù)使用的解決方案噪馏,你可以用在每個meta字段添加時間戳的偽pipelines追蹤請求聊训。最后,連接item_scraped信號注益,打印出時間戳舶沿。一旦找到阻塞代碼墙杯,將其轉(zhuǎn)化為Twisted/異步,或使用Twisted的線程池括荡。要查看轉(zhuǎn)化的效果高镐,將SPEED_PIPELINE_BLOCKING_DELAY替換為SPEED_PIPELINE_ASYNC_DELAY,然后再次運行畸冲〖邓瑁可以看到性能改進很大观腊。
實例3-下載器中有“垃圾”
癥狀:吞吐量比預(yù)期的低。下載器的請求數(shù)貌似比并發(fā)數(shù)多算行。
案例:模擬下載1000個網(wǎng)頁恕沫,每個響應(yīng)時間是0.25秒。當并發(fā)數(shù)是16時纱意,根據(jù)公式,整個過程大概需要19秒鲸阔。我們使用一個pipeline偷霉,它使用crawler.engine.download()向一個響應(yīng)時間小于一秒的偽裝API做另一個HTTP請求,褐筛。你可以在http://localhost:9312/benchmark/ar:1/api?text=hello嘗試类少。下面運行爬蟲:
$ time scrapy crawl speed -s SPEED_TOTAL_ITEMS=1000 -s SPEED_T_
RESPONSE=0.25 -s SPEED_API_T_RESPONSE=1 -s SPEED_PIPELINE_API_VIA_
DOWNLOADER=1
...
s/edule d/load scrape p/line done mem
968 32 32 32 0 32768
952 16 0 0 32 0
936 32 32 32 32 32768
...
real 0m55.151s
很奇怪,不僅時間多花了三倍渔扎,并發(fā)數(shù)也比設(shè)置的數(shù)值16要大硫狞。下載器明顯是瓶頸,因為它已經(jīng)過載了晃痴。讓我們重新運行爬蟲残吩,在另一臺終端,遠程登錄Scrapy倘核。然后就可以查看下載器中運行的Requests是哪個:
$ telnet localhost 6023
>>> engine.downloader.active
set([<POST http://web:9312/ar:1/ti:1000/rr:0.25/benchmark/api>, ... ])
貌似下載器主要是在做APIs請求泣侮,而不是下載網(wǎng)頁。
討論:你可能希望沒人使用crawler.engine.download()紧唱,因為它看起來很復(fù)雜活尊,但在Scrapy的robots.txt中間件和媒體pipeline,它被使用了兩次漏益。因此蛹锰,當人們需要處理網(wǎng)絡(luò)APIs時,自然而然要使用它绰疤。使用它遠比使用阻塞APIs要好铜犬,例如前面看過的流行的Python的requests包。比起理解Twisted和使用treq峦睡,它使用起來也更簡單翎苫。這個錯誤很難調(diào)試,所以讓我們轉(zhuǎn)而查看下載器中的請求榨了。如果看到有API或媒體URL不是直接抓取的煎谍,就說明pipelines使用了crawler.engine.download()進行了HTTP請求。我們的ONCURRENT_REQUESTS限制部隊這些請求生效龙屉,所以下載器中的請求數(shù)總是超過設(shè)置的并發(fā)數(shù)呐粘。除非偽請求數(shù)小于CONCURRENT_REQUESTS满俗,下載器不會從調(diào)度器取得新的網(wǎng)頁請求。
因此作岖,當原始請求持續(xù)1秒(API延遲)而不是0.25秒時(頁面下載延遲)唆垃,吞吐量自然會發(fā)生變化。這里容易讓人迷惑的地方是痘儡,要是API的調(diào)用比網(wǎng)頁請求還快辕万,我們根本不會觀察到性能的下降。
解決:我們可以使用treq而不是crawler.engine.download()解決這個問題沉删,你可以看到抓取器的性能大幅提高渐尿,這對API可能不是個好消息。我先將CONCURRENT_REQUESTS設(shè)置的很低矾瑰,然后逐步提高砖茸,以確保不讓API服務(wù)器過載。
下面是使用treq的例子:
$ time scrapy crawl speed -s SPEED_TOTAL_ITEMS=1000 -s SPEED_T_
RESPONSE=0.25 -s SPEED_API_T_RESPONSE=1 -s SPEED_PIPELINE_API_VIA_TREQ=1
...
s/edule d/load scrape p/line done mem
936 16 48 32 0 49152
887 16 65 64 32 66560
823 16 65 52 96 66560
...
real 0m19.922s
可以看到一個有趣的現(xiàn)象殴穴。pipeline (p/line)的items似乎比下載器(d/load)的還多凉夯。這并不是一個問題,弄清楚它是很有意思的采幌。
和預(yù)期一樣劲够,下載器中有16條請求。這意味著系統(tǒng)的吞吐量是T = N/S = 16/0.25 = 64請求/秒植榕。done這一列逐漸升高再沧,可以確認這點。每條請求在下載器中耗時0.25秒尊残,但它在pipelines中會耗時1秒炒瘸,因為較慢的API請求。這意味著在pipeline中寝衫,平均的N = T * S = 64 * 1 = 64 Items顷扩。這完全合理。這是說pipelines是瓶頸嗎慰毅?不是隘截,因為pipelines沒有同時處理響應(yīng)數(shù)量的限制。只要這個數(shù)字不持續(xù)增加汹胃,就沒有問題婶芭。接下來會進一步討論。
實例4-大量響應(yīng)造成溢出
癥狀:下載器幾乎滿負荷運轉(zhuǎn)着饥,一段時間后關(guān)閉犀农。這種情況循環(huán)發(fā)生。抓取器的內(nèi)存使用很高宰掉。
案例:設(shè)置和以前相同(使用treq)呵哨,但響應(yīng)很高赁濒,有大約120kB的HTML∶虾Γ可以看到拒炎,這次耗時31秒而不是20秒:
$ time scrapy crawl speed -s SPEED_TOTAL_ITEMS=1000 -s SPEED_T_
RESPONSE=0.25 -s SPEED_API_T_RESPONSE=1 -s SPEED_PIPELINE_API_VIA_TREQ=1
-s SPEED_DETAIL_EXTRA_SIZE=120000
s/edule d/load scrape p/line done mem
952 16 32 32 0 3842818
917 16 35 35 32 4203080
876 16 41 41 67 4923608
840 4 48 43 108 5764224
805 3 46 27 149 5524048
...
real 0m30.611s
討論:我們可能簡單的認為延遲的原因是“需要更多的時間創(chuàng)建、傳輸挨务、處理網(wǎng)頁”击你,但這并不是真正的原因。對于響應(yīng)的大小有一個強制性的限制谎柄,max_active_size = 5000000果漾。每一個響應(yīng)都和響應(yīng)體的大小相同,至少為1kB谷誓。
這個限制可能是Scrapy最基本的機制吨凑,當存在慢爬蟲和pipelines時捍歪,以保證性能。如果pipelines的吞吐量小于下載器的吞吐量鸵钝,這個機制就會起作用糙臼。當pipelines的處理時間很長,即便是很小的響應(yīng)也可能觸發(fā)這個機制恩商。下面是一個極端的例子变逃,pipelines非常長,80秒后出現(xiàn)問題:
$ time scrapy crawl speed -s SPEED_TOTAL_ITEMS=10000 -s SPEED_T_
RESPONSE=0.25 -s SPEED_PIPELINE_ASYNC_DELAY=85
解決:對于這個問題怠堪,在底層結(jié)構(gòu)上很難做什么揽乱。當你不再需要響應(yīng)體的時候,可以立即清除它粟矿。這可能是在爬蟲的后續(xù)清除響應(yīng)體凰棉,但是這么做不會重置抓取器的計數(shù)器。你能做的是減少pipelines的處理時間陌粹,減少抓取器中的響應(yīng)數(shù)量撒犀。用傳統(tǒng)的優(yōu)化方法就可以做到:檢查交互中的APIs或數(shù)據(jù)庫是否支持抓取器的吞吐量,估算下載器的能力掏秩,將pipelines進行后批次處理或舞,或使用性能更強的服務(wù)器或分布式抓取。
實例5-item并發(fā)受限/過量造成溢出
癥狀:爬蟲對每個響應(yīng)產(chǎn)生多個Items蒙幻。吞吐量比預(yù)期的小映凳,和之前的實例相似,也呈現(xiàn)出間歇性杆煞。
案例:我們有1000個請求魏宽,每一個會返回100個items腐泻。響應(yīng)時間是0.25秒,pipelines處理時間是3秒队询。進行幾次試驗派桩,CONCURRENT_ITEMS的范圍是10到150:
for concurrent_items in 10 20 50 100 150; do
time scrapy crawl speed -s SPEED_TOTAL_ITEMS=100000 -s \
SPEED_T_RESPONSE=0.25 -s SPEED_ITEMS_PER_DETAIL=100 -s \
SPEED_PIPELINE_ASYNC_DELAY=3 -s \
CONCURRENT_ITEMS=$concurrent_items
done
...
s/edule d/load scrape p/line done mem
952 16 32 180 0 243714
920 16 64 640 0 487426
888 16 96 960 0 731138
...
討論:只有每個響應(yīng)產(chǎn)生多個Items時才出現(xiàn)這種情況。這個案例的人為性太強蚌斩,因為吞吐量達到了每秒1300個Items铆惑。吞吐量這么高是因為穩(wěn)定的低延遲、沒進行處理送膳、響應(yīng)很小员魏。這樣的條件很少見。
我們首先觀察到的是叠聋,以前scrape和p/line兩列的數(shù)值是相同的撕阎,現(xiàn)在p/line顯示的是shows CONCURRENT_ITEMS * scrape。這是因為scrape顯示Reponses碌补,而p/line顯示Items虏束。
第二個是圖11中像一個浴缸的函數(shù)。部分原因是縱坐標軸造成的厦章。在左側(cè)镇匀,有非常高延遲,因為達到了內(nèi)存極限袜啃。右側(cè)汗侵,并發(fā)數(shù)太大,CPU使用率太高群发。取得最優(yōu)化并不是那么重要晰韵,因為很容易向左或向右變動。
解決:很容易檢測出這個例子中的兩個錯誤熟妓。如果CPU使用率太高宫屠,就降低并發(fā)數(shù)。如果達到了5MB的響應(yīng)限制滑蚯,pipelines就不能很好的銜接下載器的吞吐量浪蹂,提高并發(fā)數(shù)就可以解決。如果不能解決問題告材,就查看一下前面的解決方案坤次,并審視是否系統(tǒng)的其它部分可以支撐抓取器的吞吐量。
實例6-下載器沒有充分運行
癥狀:提高了CONCURRENT_REQUESTS斥赋,但是下載器中的數(shù)量并沒有提高缰猴,并且沒有充分利用。調(diào)度器是空的疤剑。
案例:首先運行一個沒有問題的例子滑绒。將響應(yīng)時間設(shè)為1秒闷堡,這樣可以簡化計算,使下載器吞吐量T = N/S = N/1 = CONCURRENT_REQUESTS疑故。然后運行如下代碼:
$ time scrapy crawl speed -s SPEED_TOTAL_ITEMS=500 \
-s SPEED_T_RESPONSE=1 -s CONCURRENT_REQUESTS=64
s/edule d/load scrape p/line done mem
436 64 0 0 0 0
...
real 0m10.99s
下載器滿狀態(tài)運行(64個請求)杠览,總時長為11秒,和500條URL纵势、每秒64請求的模型相符踱阿,S=N/T+tstart/stop=500/64+3.1=10.91秒。
現(xiàn)在钦铁,再做相同的抓取软舌,不再像之前從列表中提取URL,這次使用SPEED_START_REQUESTS_STYLE=UseIndex從索引頁提取URL牛曹。這與其它章的方法是一樣的佛点。每個索引頁有20條URL:
$ time scrapy crawl speed -s SPEED_TOTAL_ITEMS=500 \
-s SPEED_T_RESPONSE=1 -s CONCURRENT_REQUESTS=64 \
-s SPEED_START_REQUESTS_STYLE=UseIndex
s/edule d/load scrape p/line done mem
0 1 0 0 0 0
0 21 0 0 0 0
0 21 0 0 20 0
...
real 0m32.24s
很明顯,與之前的結(jié)果不同黎比。下載器沒有滿負荷運行恋脚,吞吐量為T=N/S-tstart/stop=500/(32.2-3.1)=17請求/秒。
討論:d/load列可以確認下載器沒有滿負荷運行焰手。這是因為沒有足夠的URL進入。抓取過程產(chǎn)生URL的速度慢于處理的速度怀喉。這時书妻,每個索引頁會產(chǎn)生20個URL+下一個索引頁。吞吐量不可能超過每秒20個請求躬拢,因為產(chǎn)生URL的速度沒有這么快躲履。
解決:如果每個索引頁有至少兩個下一個索引頁的鏈接,呢么我們就可以加快產(chǎn)生URL的速度聊闯。如果可以找到能產(chǎn)生更多URL(例如50)的索引頁面則會更好工猜。通過模擬觀察變化:
$ for details in 10 20 30 40; do for nxtlinks in 1 2 3 4; do
time scrapy crawl speed -s SPEED_TOTAL_ITEMS=500 -s SPEED_T_RESPONSE=1 \
-s CONCURRENT_REQUESTS=64 -s SPEED_START_REQUESTS_STYLE=UseIndex \
-s SPEED_DETAILS_PER_INDEX_PAGE=$details \
-s SPEED_INDEX_POINTAHEAD=$nxtlinks
done; done
在圖12中,我們可以看到吞吐量是如何隨每頁URL數(shù)和索引頁鏈接數(shù)變化的菱蔬。初始都是線性變化篷帅,直到到達系統(tǒng)限制。你可以改變爬蟲的規(guī)則進行試驗拴泌。如果使用LIFO(默認項)規(guī)則魏身,即先發(fā)出索引頁請求最后收回,可以看到性能有小幅提高蚪腐。你也可以將索引頁的優(yōu)先級設(shè)置為最高箭昵。兩種方法都不會有太大的提高,但是你可以通過分別設(shè)置SPEED_INDEX_RULE_LAST=1和SPEED_INDEX_HIGHER_PRIORITY=1回季,進行試驗家制。請記住正林,這兩種方法都會首先下載索引頁(因為優(yōu)先級高),因此會在調(diào)度器中產(chǎn)生大量URL颤殴,這會提高對內(nèi)存的要求觅廓。在完成索引頁之前,輸出的結(jié)果很少诅病。索引頁不多時推薦這種做法哪亿,有大量索引時不推薦這么做。
另一個簡單但高效的方法是分享首頁贤笆。這需要你使用至少兩個首頁URL蝇棉,并且它們之間距離最大。例如芥永,如果首頁有100頁篡殷,你可以選擇1和51作為起始。爬蟲這樣就可以將抓取下一頁的速度提高一倍埋涧。相似的板辽,對首頁中的商品品牌或其他屬性也可以這么做,將首頁大致分為兩個部分棘催。你可以使用-s SPEED_INDEX_SHARDS設(shè)置進行模擬:
$ for details in 10 20 30 40; do for shards in 1 2 3 4; do
time scrapy crawl speed -s SPEED_TOTAL_ITEMS=500 -s SPEED_T_RESPONSE=1 \
-s CONCURRENT_REQUESTS=64 -s SPEED_START_REQUESTS_STYLE=UseIndex \
-s SPEED_DETAILS_PER_INDEX_PAGE=$details -s SPEED_INDEX_SHARDS=$shards
done; done
這次的結(jié)果比之前的方法要好劲弦,并且更加簡潔 。
解決問題的流程
總結(jié)一下醇坝,Scrapy的設(shè)計初衷就是讓下載器作為瓶頸邑跪。使CONCURRENT_REQUESTS從小開始,逐漸變大呼猪,直到發(fā)生以下的限制:
- CPU利用率 > 80-90%
- 源網(wǎng)站延遲急劇升高
- 抓取器的響應(yīng)達到內(nèi)存5Mb上限
同時画畅,進行如下操作: - 始終保持調(diào)度器(mqs/dqs)中有一定數(shù)量的請求,避免下載器是空的
- 不使用阻塞代碼或CPU密集型代碼
總結(jié)
在本章中宋距,我們通過案例展示了Scrapy的架構(gòu)是如何影響性能的轴踱。細節(jié)可能會在未來的Scrapy版本中變動,但是本章闡述的原理在相當長一段時間內(nèi)可以幫助你理解以Twisted谚赎、Netty Node.js等為基礎(chǔ)的異步框架淫僻。
談到具體的Scrapy性能,有三個確定的答案:我不知道也不關(guān)心壶唤、我不知道但會查出原因嘁傀,和我知道。本章已多次指出视粮,“更多的服務(wù)器/內(nèi)存/帶寬”不能提高Scrapy的性能细办。唯一的方法是找到瓶頸并解決它。
在最后一章中,我們會學(xué)習(xí)如何進一步提高性能笑撞,不是使用一臺服務(wù)器岛啸,而是在多臺服務(wù)器上分布多個爬蟲。
序言
第1章 Scrapy介紹
第2章 理解HTML和XPath
第3章 爬蟲基礎(chǔ)
第4章 從Scrapy到移動應(yīng)用
第5章 快速構(gòu)建爬蟲
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy編程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和實時分析