《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分布式抓取和實(shí)時分析


通常,很容易將性能理解錯。對于Scrapy,幾乎一定會把它的性能理解錯,因?yàn)檫@里有許多反直覺的地方。除非你對Scrapy的結(jié)構(gòu)有清楚的了解,你會發(fā)現(xiàn)努力提升Scrapy的性能卻收效甚微。這就是處理高性能、低延遲、高并發(fā)環(huán)境的復(fù)雜之處。對于優(yōu)化瓶頸, Amdahl定律仍然適用,但除非找到真正的瓶頸,吞吐量并不會增加。要想學(xué)習(xí)更多,可以看Dr.Goldratt的《目標(biāo)》這本書,其中用比喻講到了更多關(guān)于瓶延遲、吞吐量的知識。本章就是來幫你確認(rèn)Scrapy配置的瓶頸所在,讓你避免明顯的錯誤。

請記住,本章相對較難,涉及到許多數(shù)學(xué)。但計(jì)算還算比較簡單,并且有圖表示意。如果你不喜歡數(shù)學(xué),可以直接忽略公式,這樣仍然可以搞明白Scrapy的性能是怎么回事。

Scrapy的引擎——一個直觀的方法

并行系統(tǒng)看起來就像管道系統(tǒng)。在計(jì)算機(jī)科學(xué)中,我們使用隊(duì)列符表示隊(duì)列并處理元素(見圖1的左邊)。隊(duì)列系統(tǒng)的基本定律是Little定律,它指明平衡狀態(tài)下,隊(duì)列系統(tǒng)中的總元素個數(shù)(N)等于吞吐量(T)乘以總排隊(duì)/處理時間(S),即N=T*S。另外兩種形式,T=N/S和S=N/T也十分有用。

圖1 Little定律、隊(duì)列系統(tǒng)、管道

管道(圖1的右邊)在幾何學(xué)上也有一個相似的定律。管道的體積(V)等于長度(L)乘以橫截面積(A),即V=L*A。

如果假設(shè)L代表處理時間S(L≈S),體積代表總元素個數(shù)(V≈N),橫截面積啊代表吞吐量(A≈T),Little定律和體積公式就是相同的。

提示:這個類比合理嗎?答案是基本合理。如果我們想象小液滴在管道中以勻速流過,那么L≈S 就完全合理,因?yàn)楣艿涝介L,液滴流過的時間也越長。V≈N 也是合理的,因?yàn)楣艿涝酱?,它能容下的液滴越多。但是,我們可以通過增大壓力的方法,壓入更多的液滴。A≈T是真正的類比。在管道中,吞吐量是每秒流進(jìn)/流出的液滴總數(shù),被稱為體積流速,在正常的情況下,它與A^2成正比。這是因?yàn)楦鼘挼墓艿啦粌H意味更多的液體流出,還具有更快的速度,因?yàn)楣鼙谥g的空間變大了。但對于這一章,我們可以忽略這一點(diǎn),假設(shè)壓力和速度是不變的,吞吐量只與橫截面積成正比。

Little定律與體積公式十分相似,所以管道模型直觀上是正確的。再看看圖1中的右半部。假設(shè)管道代表Scrapy的下載器。第一個十分細(xì)的管道,它的總體積/并發(fā)等級(N)=8個并發(fā)請求。長度/延遲(S)對于一個高速網(wǎng)站,假設(shè)為S=250ms?,F(xiàn)在可以計(jì)算橫街面積/吞吐量T=N/S=8/0.25=32請求/秒。

可以看到,延遲率是手遠(yuǎn)程服務(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)排隊(duì)系統(tǒng)。

串聯(lián)排隊(duì)系統(tǒng)

當(dāng)你將橫截面積/吞吐量不同的管道連接起來時,直觀上,人們會認(rèn)為總系統(tǒng)會受限于最窄的管道(最小的吞吐量T),見圖2。

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

你還可以看到最窄的管道(即瓶頸)放在不同的地方,可以影響其他管道的填充程度。如果將填充程度類比為系統(tǒng)內(nèi)存需求,瓶頸的擺放就十分重要了。最好能將填充程度達(dá)到最高,這樣單位工作的花費(fèi)最小。在Scrapy中,單位工作(抓取一個網(wǎng)頁)大體包括下載器之前的一條URL(幾個字節(jié))和下載器之后的URL和服務(wù)器響應(yīng)。

提示:這就是為什么,Scrapy把瓶頸放在下載器。

確認(rèn)瓶頸

用管道系統(tǒng)的比喻,可以直觀的確認(rèn)瓶頸所在。查看圖2,你可以看到瓶頸之前都是滿的,瓶頸之后就不是滿的。

對于大多數(shù)系統(tǒng),可以用系統(tǒng)的性能指標(biāo)監(jiān)測排隊(duì)系統(tǒng)是否擁擠。通過檢測Scrapy的隊(duì)列,我們可以確定出瓶頸的所在,如果瓶頸不是在下載器的話,我們可以通過調(diào)整設(shè)置使下載器成為瓶頸。瓶頸沒有得到優(yōu)化,吞吐量就不會有優(yōu)化。調(diào)整其它部分只會使系統(tǒng)變得更糟,很可能將瓶頸移到別處。所以在修改代碼和配置之前,你必須找到瓶頸。你會發(fā)現(xiàn)在大多數(shù)情況下,包括本書中的例子,瓶頸的位置都和預(yù)想的不同。

Scrapy的性能模型

讓我們回到Scrapy,詳細(xì)查看它的性能模型,見圖3。

圖3 Scrapy的性能模型

Scrapy包括以下部分:

  • 調(diào)度器:大量的Request在這里排隊(duì),直到下載器處理它們。其中大部分是URL,因此體積不大,也就是說即便有大量請求存在,也可以被下載器及時處理。
  • 阻塞器:這是抓取器由后向前進(jìn)行反饋的一個安全閥,如果進(jìn)程中的響應(yīng)大于5MB,阻塞器就會暫停更多的請求進(jìn)入下載器。這可能會造成性能的波動。
  • 下載器:這是對Scrapy的性能最重要的組件。它用復(fù)雜的機(jī)制限制了并發(fā)數(shù)。它的延遲(管道長度)等于遠(yuǎn)程服務(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會被并行處理。這一點(diǎn)很重要,因?yàn)?,如果你用pipelines連接數(shù)據(jù)庫,你可能無意地向數(shù)據(jù)庫導(dǎo)入數(shù)據(jù),pipelines的默認(rèn)值(100)就會看起來很少。

爬蟲和pipelines的代碼是異步的,會包含必要的延遲,但二者不會是瓶頸。爬蟲和pipelines很少會做繁重的處理工作。如果是的話,服務(wù)器的CPU則是瓶頸。

使用遠(yuǎn)程登錄控制組件

為了理解Requests/Items是如何在管道中流動的,我們現(xiàn)在還不能真正的測量流動。然而,我們可以檢測在Scrapy的每個階段,有多少個Requests/Responses/Items。

通過Scrapy運(yùn)行遠(yuǎn)程登錄,我們就可以得到性能信息。我們可以在6023端口運(yùn)行遠(yuǎn)程登錄命令。然后,會在Scrapy中出現(xiàn)一個Python控制臺。注意,如果在這里進(jìn)行中斷操作,比如time.sleep(),就會暫停爬蟲。通過內(nèi)建的est()函數(shù),可以查看一些有趣的信息。其中一些或是非常專業(yè)的,或是可以從核心數(shù)據(jù)推導(dǎo)出來。本章后面會展示后者。下面運(yùn)行一個例子。當(dāng)我們運(yùn)行一個爬蟲時,我們在開發(fā)機(jī)打開第二臺終端,在端口6023遠(yuǎn)程登錄,然后運(yùn)行est()。

提示:本章代碼位于目錄ch10。這個例子位于ch10/speed。

在第一臺終端,運(yùn)行如下命令:

$ 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ù)的意義,后面會詳解。在第二臺終端,運(yùn)行如下代碼:

$ 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退出遠(yuǎn)程登錄,返回第一臺終端按Ctrl+C停止抓取。

提示:我們現(xiàn)在忽略dqs。如果你通過設(shè)置JOBDIR打開了持久支持,你會得到非零的dqs(len(engine.slot.scheduler.dqs)),你應(yīng)該將它添加到mqs的大小中。

讓我們查看這個例子中的數(shù)據(jù)的意義。mqs指出調(diào)度器中等待的項(xiàng)目很少(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ù)隊(duì)列(mqs),下載器在滿負(fù)荷運(yùn)轉(zhuǎn);下載器之后,工作量較高并有一定波動。

另一個可以查看信息的地方是stats對象,抓取之后打印的內(nèi)容。我們可以以dict的形式訪問它,只需通過via stats.get_stats()遠(yuǎn)程登錄,用p()函數(shù)打?。?/p>

$ 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)頁加載的速度。不用在意細(xì)節(jié),我們接下來會看許多例子?,F(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。默認(rèn)條件下(見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()方法。默認(rèn)下,用crawler.engine.crawl()方法將所有起始URL提供給調(diào)度器。
  • pipeline,DummyPipeline,模擬了一些處理過程。它可以引入四種不同的延遲類型。阻塞/計(jì)算/同步延遲(SPEED_PIPELINE_BLOCKING_DELAY—很差),異步延遲(SPEED_PIPELINE_ASYNC_DELAY—不錯),使用遠(yuǎn)程treq庫進(jìn)行API調(diào)用(SPEED_PIPELINE_API_VIA_TREQ—不錯),和使用Scrapy的crawler.engine.download()進(jìn)行API調(diào)用(SPEED_PIPELINE_API_VIA_DOWNLOADER—不怎么好)。默認(rèn)時,pipeline不添加延遲。
  • settings.py中的一組高性能設(shè)置。關(guān)閉任何可能使系統(tǒng)降速的項(xiàng)。因?yàn)橹辉诒镜胤?wù)器運(yùn)行,我們還關(guān)閉了每個域的請求限制。
  • 一個可以記錄數(shù)據(jù)的擴(kuò)展,和第8章中的類似。它每隔一段時間,就打印出核心數(shù)據(jù)。

在上一個例子,我們已經(jīng)用過了這個系統(tǒng),讓我們重新做一次模擬,并使用Linux的計(jì)時器測量總共的執(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ù)荷下并發(fā)數(shù)是16,與設(shè)置相同。抓取器主要是爬蟲,因?yàn)閜ipeline是空的,它沒有滿負(fù)荷運(yùn)轉(zhuǎn)。它用46秒抓取了5000個Items,并發(fā)數(shù)是16,即每個請求的處理時間是46*16/5000=147ms,而不是預(yù)想的125ms,滿足要求。

標(biāo)準(zhǔn)性能模型

當(dāng)Scrapy正常運(yùn)行且下載器為瓶頸時,就是Scrapy的標(biāo)準(zhǔn)性能模型。此時,調(diào)度器有一定數(shù)量的請求,下載器滿負(fù)荷運(yùn)行。抓取器負(fù)荷不滿,并且加載的響應(yīng)不會持續(xù)增加。

圖5 標(biāo)準(zhǔn)性能模型和一些試驗(yàn)結(jié)果

三項(xiàng)設(shè)置負(fù)責(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ù)量。對于共享站點(diǎn),比如,多個域名指向一個服務(wù)器,這可以幫助你降低服務(wù)器的載荷。

為了更簡明的分析,現(xiàn)在把per-IP的限制關(guān)閉,即使CONCURRENT_REQUESTS_PER_IP為默認(rèn)值(0),并設(shè)置CONCURRENT_REQUESTS_PER_DOMAIN為一個超大值(1000000)。這樣就可以無視其它的設(shè)置,讓下載器的并發(fā)數(shù)完全受CONCURRENT_REQUESTS控制。

我們希望吞吐量取決于下載網(wǎng)頁的平均時間,包括遠(yuǎn)程服務(wù)器和我們系統(tǒng)(Linux、Twisted/Python)的延遲,tdownload=tresponse+toverhead。還可以加上啟動和關(guān)閉的時間。這包括從取得響應(yīng)到Items離開pipeline的時間,和取得第一個響應(yīng)的時間,還有空緩存的內(nèi)部損耗。

總之,如果你要完成N個請求,在爬蟲正常的情況下,需要花費(fèi)的時間是:

所幸的是,我們只需控制一部分參數(shù)就可以了。我們可以用一臺更高效的服務(wù)器控制toverhead,和tstart/stop,但是后者并不值得,因?yàn)槊看芜\(yùn)行只影響一次。除此之外,最值得關(guān)注的就是CONCURRENT_REQUESTS,它取決于我們?nèi)绾问褂梅?wù)器。如果將其設(shè)置成一個很大的值,在某一時刻就會使服務(wù)器或我們電腦的CPU滿負(fù)荷,這樣響應(yīng)就會不及時,tresponse會急劇升高,因?yàn)榫W(wǎng)站會阻塞、屏蔽進(jìn)一步的訪問,或者服務(wù)器會崩潰。

讓我們驗(yàn)證一下這個理論。我們抓取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ù),可以計(jì)算出toverhead=6ms,tstart/stop=3.1s。toverhead可以忽略,但是開始時間相對較長,最好是在數(shù)千條URL時長時間運(yùn)行。因此,可以估算出吞吐量公式是:

處理N個請求,我們可以估算tjob,然后可以直接求出T。

解決性能問題

現(xiàn)在我們已經(jīng)明白如何使Scrapy的性能最大化,讓我們來看看如何解決實(shí)際問題。我們會通過探究癥狀、運(yùn)行錯誤、討論原因、修復(fù)問題,討論幾個實(shí)例。呈現(xiàn)的順序是從系統(tǒng)性的問題到Scrapy的小技術(shù)問題,也就是說,更為常見的問題可能會排在后面。請閱讀全部章節(jié),再開始處理你自己的問題。

實(shí)例1——CPU滿負(fù)荷

癥狀:當(dāng)你提高并發(fā)數(shù)時,性能并沒有提高。當(dāng)你降低并發(fā)數(shù),一切工作正常。下載器沒有問題,但是每個請求花費(fèi)時間太長。用Unix/Linux命令ps或Windows的任務(wù)管理器查看CPU的情況,CPU的占用率非常高。

案例:假設(shè)你運(yùn)行如下命令:

$ 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ù)計(jì)時間是用之前推導(dǎo)的公式求出的,CPU是用命令查看得到的(可以在另一臺終端運(yùn)行查看命令):

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

在我們的試驗(yàn)中,我們沒有進(jìn)行任何處理工作,所以并發(fā)數(shù)可以很高。在實(shí)際中,很快就可以看到性能趨緩的情況發(fā)生。

討論:Scrapy使用的是單線程,當(dāng)并發(fā)數(shù)很高時,CPU可能會成為瓶頸。假設(shè)沒有使用線程池,CPU的使用率建議是80-90%??赡苣氵€會碰到其他系統(tǒng)性問題,比如帶寬、內(nèi)存、硬盤吞吐量,但是發(fā)生這些狀況的可能性比較小,并且不屬于系統(tǒng)管理,所以就不贅述了。

解決:假設(shè)你的代碼已經(jīng)是高效的。你可以通過在一臺服務(wù)器上運(yùn)行多個爬蟲,使累積并發(fā)數(shù)超過CONCURRENT_REQUESTS。這可以充分利用CPU的性能。如果還想提高并發(fā)數(shù),你可以使用多臺服務(wù)器(見11章),這樣就可以使用更多的內(nèi)存、帶寬和硬盤吞吐量。檢查CPU的使用情況是你的首要關(guān)切。

實(shí)例2-阻塞代碼

癥狀:系統(tǒng)的運(yùn)行得十分奇怪。比起預(yù)期的速度,系統(tǒng)運(yùn)行的十分緩慢。改變并發(fā)數(shù),也沒有效果。下載器幾乎是空的(遠(yuǎn)小于并發(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還是爬蟲,你都會看到抓取器滿負(fù)荷,它之前和之后的部分都是空的。看起來這違背了我們之前講的,但是由于我們并沒有一個并行系統(tǒng),pipeline的規(guī)則此處并不適用。這個錯誤很容易犯(例如,使用了阻塞APIs),然后就會出現(xiàn)之前的狀況。相似的討論也適用于計(jì)算復(fù)雜的代碼。應(yīng)該為每個代碼使用多線程,如第9章所示,或在Scrapy的外部批次運(yùn)行,第11章會看到例子。

解決:假設(shè)代碼是繼承而來的,你并不知道阻塞代碼位于何處。沒有pipelines系統(tǒng)也能運(yùn)行的話,使pipeline無效,看系統(tǒng)能否正常運(yùn)行。如果是的話,說明阻塞代碼位于pipelines。如果不是的話,逐一恢復(fù)pipelines,看問題何時發(fā)生。如果必須所有組件都在運(yùn)行,整個系統(tǒng)才能運(yùn)行的話,給每個pipeline階段添加日志消息(或者插入可以打印時間戳的偽pipelines),就可以發(fā)現(xiàn)哪一步花費(fèi)的時間最多。如果你想要一個長期可重復(fù)使用的解決方案,你可以用在每個meta字段添加時間戳的偽pipelines追蹤請求。最后,連接item_scraped信號,打印出時間戳。一旦找到阻塞代碼,將其轉(zhuǎn)化為Twisted/異步,或使用Twisted的線程池。要查看轉(zhuǎn)化的效果,將SPEED_PIPELINE_BLOCKING_DELAY替換為SPEED_PIPELINE_ASYNC_DELAY,然后再次運(yùn)行??梢钥吹叫阅芨倪M(jìn)很大。

實(shí)例3-下載器中有“垃圾”

癥狀:吞吐量比預(yù)期的低。下載器的請求數(shù)貌似比并發(fā)數(shù)多。

案例:模擬下載1000個網(wǎng)頁,每個響應(yīng)時間是0.25秒。當(dāng)并發(fā)數(shù)是16時,根據(jù)公式,整個過程大概需要19秒。我們使用一個pipeline,它使用crawler.engine.download()向一個響應(yīng)時間小于一秒的偽裝API做另一個HTTP請求,。你可以在http://localhost:9312/benchmark/ar:1/api?text=hello嘗試。下面運(yùn)行爬蟲:

$ 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要大。下載器明顯是瓶頸,因?yàn)樗呀?jīng)過載了。讓我們重新運(yùn)行爬蟲,在另一臺終端,遠(yuǎn)程登錄Scrapy。然后就可以查看下載器中運(yùn)行的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(),因?yàn)樗雌饋砗軓?fù)雜,但在Scrapy的robots.txt中間件和媒體pipeline,它被使用了兩次。因此,當(dāng)人們需要處理網(wǎng)絡(luò)APIs時,自然而然要使用它。使用它遠(yuǎn)比使用阻塞APIs要好,例如前面看過的流行的Python的requests包。比起理解Twisted和使用treq,它使用起來也更簡單。這個錯誤很難調(diào)試,所以讓我們轉(zhuǎn)而查看下載器中的請求。如果看到有API或媒體URL不是直接抓取的,就說明pipelines使用了crawler.engine.download()進(jìn)行了HTTP請求。我們的ONCURRENT_REQUESTS限制部隊(duì)這些請求生效,所以下載器中的請求數(shù)總是超過設(shè)置的并發(fā)數(shù)。除非偽請求數(shù)小于CONCURRENT_REQUESTS,下載器不會從調(diào)度器取得新的網(wǎng)頁請求。

圖8 偽API請求決定了性能

因此,當(dā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)的還多。這并不是一個問題,弄清楚它是很有意思的。

圖9 使用長pipelines也符合要求

和預(yù)期一樣,下載器中有16條請求。這意味著系統(tǒng)的吞吐量是T = N/S = 16/0.25 = 64請求/秒。done這一列逐漸升高,可以確認(rèn)這點(diǎn)。每條請求在下載器中耗時0.25秒,但它在pipelines中會耗時1秒,因?yàn)檩^慢的API請求。這意味著在pipeline中,平均的N = T * S = 64 * 1 = 64 Items。這完全合理。這是說pipelines是瓶頸嗎?不是,因?yàn)閜ipelines沒有同時處理響應(yīng)數(shù)量的限制。只要這個數(shù)字不持續(xù)增加,就沒有問題。接下來會進(jìn)一步討論。

實(shí)例4-大量響應(yīng)造成溢出

癥狀:下載器幾乎滿負(fù)荷運(yùn)轉(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

討論:我們可能簡單的認(rèn)為延遲的原因是“需要更多的時間創(chuàng)建、傳輸、處理網(wǎng)頁”,但這并不是真正的原因。對于響應(yīng)的大小有一個強(qiáng)制性的限制,max_active_size = 5000000。每一個響應(yīng)都和響應(yīng)體的大小相同,至少為1kB。

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

這個限制可能是Scrapy最基本的機(jī)制,當(dāng)存在慢爬蟲和pipelines時,以保證性能。如果pipelines的吞吐量小于下載器的吞吐量,這個機(jī)制就會起作用。當(dāng)pipelines的處理時間很長,即便是很小的響應(yīng)也可能觸發(fā)這個機(jī)制。下面是一個極端的例子,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)上很難做什么。當(dāng)你不再需要響應(yīng)體的時候,可以立即清除它。這可能是在爬蟲的后續(xù)清除響應(yīng)體,但是這么做不會重置抓取器的計(jì)數(shù)器。你能做的是減少pipelines的處理時間,減少抓取器中的響應(yīng)數(shù)量。用傳統(tǒng)的優(yōu)化方法就可以做到:檢查交互中的APIs或數(shù)據(jù)庫是否支持抓取器的吞吐量,估算下載器的能力,將pipelines進(jìn)行后批次處理,或使用性能更強(qiáng)的服務(wù)器或分布式抓取。

實(shí)例5-item并發(fā)受限/過量造成溢出

癥狀:爬蟲對每個響應(yīng)產(chǎn)生多個Items。吞吐量比預(yù)期的小,和之前的實(shí)例相似,也呈現(xiàn)出間歇性。

案例:我們有1000個請求,每一個會返回100個items。響應(yīng)時間是0.25秒,pipelines處理時間是3秒。進(jìn)行幾次試驗(yàn),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)這種情況。這個案例的人為性太強(qiáng),因?yàn)橥掏铝窟_(dá)到了每秒1300個Items。吞吐量這么高是因?yàn)榉€(wěn)定的低延遲、沒進(jìn)行處理、響應(yīng)很小。這樣的條件很少見。

我們首先觀察到的是,以前scrape和p/line兩列的數(shù)值是相同的,現(xiàn)在p/line顯示的是shows CONCURRENT_ITEMS * scrape。這是因?yàn)閟crape顯示Reponses,而p/line顯示Items。

第二個是圖11中像一個浴缸的函數(shù)。部分原因是縱坐標(biāo)軸造成的。在左側(cè),有非常高延遲,因?yàn)檫_(dá)到了內(nèi)存極限。右側(cè),并發(fā)數(shù)太大,CPU使用率太高。取得最優(yōu)化并不是那么重要,因?yàn)楹苋菀紫蜃蠡蛳蛴易儎印?/p>

解決:很容易檢測出這個例子中的兩個錯誤。如果CPU使用率太高,就降低并發(fā)數(shù)。如果達(dá)到了5MB的響應(yīng)限制,pipelines就不能很好的銜接下載器的吞吐量,提高并發(fā)數(shù)就可以解決。如果不能解決問題,就查看一下前面的解決方案,并審視是否系統(tǒng)的其它部分可以支撐抓取器的吞吐量。

實(shí)例6-下載器沒有充分運(yùn)行

癥狀:提高了CONCURRENT_REQUESTS,但是下載器中的數(shù)量并沒有提高,并且沒有充分利用。調(diào)度器是空的。

案例:首先運(yùn)行一個沒有問題的例子。將響應(yīng)時間設(shè)為1秒,這樣可以簡化計(jì)算,使下載器吞吐量T = N/S = N/1 = CONCURRENT_REQUESTS。然后運(yùn)行如下代碼:

$ 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)運(yùn)行(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é)果不同。下載器沒有滿負(fù)荷運(yùn)行,吞吐量為T=N/S-tstart/stop=500/(32.2-3.1)=17請求/秒。

討論:d/load列可以確認(rèn)下載器沒有滿負(fù)荷運(yùn)行。這是因?yàn)闆]有足夠的URL進(jìn)入。抓取過程產(chǎn)生URL的速度慢于處理的速度。這時,每個索引頁會產(chǎn)生20個URL+下一個索引頁。吞吐量不可能超過每秒20個請求,因?yàn)楫a(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ù)變化的。初始都是線性變化,直到到達(dá)系統(tǒng)限制。你可以改變爬蟲的規(guī)則進(jìn)行試驗(yàn)。如果使用LIFO(默認(rèn)項(xiàng))規(guī)則,即先發(fā)出索引頁請求最后收回,可以看到性能有小幅提高。你也可以將索引頁的優(yōu)先級設(shè)置為最高。兩種方法都不會有太大的提高,但是你可以通過分別設(shè)置SPEED_INDEX_RULE_LAST=1和SPEED_INDEX_HIGHER_PRIORITY=1,進(jìn)行試驗(yàn)。請記住,這兩種方法都會首先下載索引頁(因?yàn)閮?yōu)先級高),因此會在調(diào)度器中產(chǎn)生大量URL,這會提高對內(nèi)存的要求。在完成索引頁之前,輸出的結(jié)果很少。索引頁不多時推薦這種做法,有大量索引時不推薦這么做。

另一個簡單但高效的方法是分享首頁。這需要你使用至少兩個首頁URL,并且它們之間距離最大。例如,如果首頁有100頁,你可以選擇1和51作為起始。爬蟲這樣就可以將抓取下一頁的速度提高一倍。相似的,對首頁中的商品品牌或其他屬性也可以這么做,將首頁大致分為兩個部分。你可以使用-s SPEED_INDEX_SHARDS設(shè)置進(jìn)行模擬:

$ 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è)計(jì)初衷就是讓下載器作為瓶頸。使CONCURRENT_REQUESTS從小開始,逐漸變大,直到發(fā)生以下的限制:

  • CPU利用率 > 80-90%
  • 源網(wǎng)站延遲急劇升高
  • 抓取器的響應(yīng)達(dá)到內(nèi)存5Mb上限
    同時,進(jìn)行如下操作:
  • 始終保持調(diào)度器(mqs/dqs)中有一定數(shù)量的請求,避免下載器是空的
  • 不使用阻塞代碼或CPU密集型代碼
圖13 解決Scrapy性能問題的路線圖

總結(jié)

在本章中,我們通過案例展示了Scrapy的架構(gòu)是如何影響性能的。細(xì)節(jié)可能會在未來的Scrapy版本中變動,但是本章闡述的原理在相當(dāng)長一段時間內(nèi)可以幫助你理解以Twisted、Netty Node.js等為基礎(chǔ)的異步框架。

談到具體的Scrapy性能,有三個確定的答案:我不知道也不關(guān)心、我不知道但會查出原因,和我知道。本章已多次指出,“更多的服務(wù)器/內(nèi)存/帶寬”不能提高Scrapy的性能。唯一的方法是找到瓶頸并解決它。

在最后一章中,我們會學(xué)習(xí)如何進(jìn)一步提高性能,不是使用一臺服務(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分布式抓取和實(shí)時分析


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容