《Learning Scrapy》(中文版)第10章 理解Scrapy的性能


序言
第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 Little定律良价、隊列系統(tǒng)、管道

管道(圖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翘县。

圖2 不同的串聯(lián)排隊系統(tǒng)

你還可以看到最窄的管道(即瓶頸)放在不同的地方,可以影響其他管道的填充程度谴分。如果將填充程度類比為系統(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优妙。

圖3 Scrapy的性能模型

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=1http://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诊赊。
圖4 評分服務(wù)器創(chuàng)建了一個結(jié)構(gòu)可變的假網(wǎng)站
  • 爬蟲,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ù)增加途戒。

圖5 標準性能模型和一些試驗結(jié)果

三項設(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是用命令查看得到的(可以在另一臺終端運行查看命令):

圖6 當并發(fā)數(shù)超出一定值時殿遂,性能變化趨緩诈铛。

在我們的試驗中,我們沒有進行任何處理工作墨礁,所以并發(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ā)生的狀況襟锐。

圖7 阻塞代碼使并發(fā)數(shù)無效化

無論阻塞代碼位于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)頁請求。

圖8 偽API請求決定了性能

因此作岖,當原始請求持續(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)的還多凉夯。這并不是一個問題,弄清楚它是很有意思的采幌。

圖9 使用長pipelines也符合要求

和預(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谷誓。

圖10 下載器中的請求數(shù)不規(guī)律變化,說明存在響應(yīng)大小限制

這個限制可能是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
...
圖11 以CONCURRENT_ITEMS為參數(shù)的抓取時間函數(shù)

討論:只有每個響應(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 以每頁能產(chǎn)生的鏈接數(shù)為參數(shù)的吞吐量函數(shù)

在圖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密集型代碼
圖13 解決Scrapy性能問題的路線圖

總結(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分布式抓取和實時分析


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末茴肥,一起剝皮案震驚了整個濱河市坚踩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瓤狐,老刑警劉巖瞬铸,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異础锐,居然都是意外死亡嗓节,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門皆警,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拦宣,“玉大人,你說我怎么就攤上這事信姓⊥宜恚” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵意推,是天一觀的道長豆瘫。 經(jīng)常有香客問我,道長菊值,這世上最難降的妖魔是什么外驱? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮俊性,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘描扯。我一直安慰自己定页,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布绽诚。 她就那樣靜靜地躺著典徊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪恩够。 梳的紋絲不亂的頭發(fā)上卒落,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音蜂桶,去河邊找鬼儡毕。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的腰湾。 我是一名探鬼主播雷恃,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼费坊!你這毒婦竟也來了倒槐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤附井,失蹤者是張志新(化名)和其女友劉穎讨越,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體永毅,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡把跨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了卷雕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片节猿。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖漫雕,靈堂內(nèi)的尸體忽然破棺而出滨嘱,到底是詐尸還是另有隱情,我是刑警寧澤浸间,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布太雨,位于F島的核電站,受9級特大地震影響魁蒜,放射性物質(zhì)發(fā)生泄漏囊扳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一兜看、第九天 我趴在偏房一處隱蔽的房頂上張望锥咸。 院中可真熱鬧,春花似錦细移、人聲如沸搏予。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雪侥。三九已至,卻和暖如春精绎,著一層夾襖步出監(jiān)牢的瞬間速缨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工代乃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留旬牲,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像引谜,于是被迫代替她去往敵國和親牍陌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容