爬蟲平臺Crawlab核心原理--分佈式架構

背景

Crawlab自第一版發佈已經幾個月了,其中經歷了好幾次迭代:版本從v0.1到了v0.3.0;後端語言從Python到了Golang;從最初使用Celery作爲任務調度引擎,到自己開發分佈式任務調度引擎;從只能運行自定義爬蟲到可以運行可配置爬蟲(雖然還沒遷移到最新版本);從手動部署爬蟲到自動部署爬蟲;從自己搭建環境到Docker部署;從手動執行任務到定時任務;等等(詳情見CHANGELOG)。在使用者們的反饋下,Crawlab爬蟲平臺也逐漸變得穩定和實用,能夠真正幫助到有爬蟲管理需求的用戶。如今在Github上有近1k的star,相關社區(微信羣、微信公衆號)也建立起來,四分之一的用戶表示已經將Crawlab應用於企業爬蟲管理。可以看出,Crawlab是受開發者們關注和喜歡的。

Github: https://github.com/tikazyq/cr...

爲什麼需要爬蟲管理平臺

對於一般的爬蟲愛好者來說,寫一個單機爬蟲腳本已經足夠,而且Scrapy這樣的優秀爬蟲框架能夠讓開發者輕鬆調試、編寫一個簡單的爬蟲,需要定時任務就直接上Crontab,分分鐘搞定。然而,一般的企業對爬蟲的要求相對較高,其中主要涉及一個問題,也就是規模(Scale)。當然,這裏的規模是指大型規模。爬蟲的規模分爲兩種:一種是爬蟲需要抓取大量的數據(Volume),例如全網抓取淘寶的商品;另一種是爬蟲需要涵蓋大量的網站(Coverage),例如搜索引擎。

不同的規模需要有不同的架構策略(如下圖):

  1. 當網站數量只有一個,抓取結果不多時,只需要單機即可,並不需要分佈式爬蟲;
  2. 但當需要抓取結果量級提升時,例如全網抓取淘寶,就需要用分佈式爬蟲了,這是因爲單個機器的帶寬和計算資源不足以做到全網抓取,而且爲了應對反爬蟲技術,還需要大量的代理IP;
  3. 同理,當需要抓取網站的數量增多時,例如你需要創建一個新聞搜索引擎,你同樣需要多臺機器來獲取足夠的帶寬和計算資源;
  4. 對於同時要求Volume和Coverage的應用,不是一般的小企業或個人能做的,對不管是人力和機器的資源都非常高。

而爬蟲管理平臺就是針對情況(2)、(3)、(4)而存在的分佈式管理平臺,能夠讓用戶輕鬆管理多個爬蟲或多機運行的爬蟲。

Crawlab從誕生之初就解決了分佈式爬蟲問題,最早採用了Celery作爲分佈式任務調度引擎,以Redis作爲消息隊列,HTTP請求作爲節點通信媒介,簡單地實現了分佈式管理。但隨着用戶不斷使用Crawlab,發現這樣的方式並不是很方便,用戶需要指定節點的IP地址和API端口,而且還不能指定節點執行任務。因爲各種問題,在最新版本v0.3.0用Golang重構後端的時候,就將Celery棄用了,轉而自己開發分佈式節點的監控和通信應用,這樣更加靈活和高效。本文是核心原理介紹,下面將着重介紹Crawlab的分佈式架構原理(Golang版本)。

整體架構

Crawlab的整體架構如下圖,由五大部分組成:

  1. 主節點(Master Node):負責任務派發、API、部署爬蟲等;
  2. 工作節點(Worker Node):負責執行爬蟲任務;
  3. MongoDB數據庫:存儲節點、爬蟲、任務等日常運行數據;
  4. Redis數據庫:儲存任務消息隊列、節點心跳等信息。
  5. 前端客戶端:Vue應用,負責前端交互和向後端請求數據。

以執行爬蟲任務爲例,它是Crawlab中常見的使用場景,我們來看看它具體是如何工作的:

  1. 前端向主節點發起請求,要求指定在某一工作節點執行任務;
  2. 主節點收到該請求,並將任務數據推送到Redis任務隊列中;
  3. 工作節點持續監聽Redis任務隊列,並利用LPOP獲取任務;
  4. 工作節點執行任務,並將結果寫回到儲存數據庫;

以上就是執行爬蟲任務的大致流程。當然,這還不是全部,我們還需要考慮日誌處理、併發執行、取消任務等細節問題。具體的處理信息,請查看相關文檔源代碼

總的來說,可以將主節點看作是Crawlab整體架構的中控系統,理解爲Crawlab的大腦;工作節點是實際幹活的部分,是Crawlab的運動軀體;MongoDB和Redis是負責通信交流的,可以看作Crawlab的血液和神經網絡。這些模塊一起構成了一個完整、自洽、相互協作的系統。

節點註冊和監控

節點監控主要是通過Redis來完成的(如下圖)。

工作節點會不斷更新心跳信息在Redis上,利用HSET nodes <node_id> <msg>,心跳信息<msg>包含節點MAC地址,IP地址,當前時間戳。

主節點會週期性獲取Redis上的工作節點心跳信息。如果有工作節點的時間戳在60秒之前,則考慮該節點爲離線狀態,會在Redis中刪除該節點的信息,並在MongoDB中設置爲"離線";如果時間戳在過去60秒之內,則保留該節點信息,在MongoDB中設置爲"在線"。

該架構的優點

這樣,就做到了一個監控節點是否在線的節點註冊系統。這樣架構的好處在於,節點之間根本不用像HTTP、RPC那樣IP或端口,只需要知道Redis的地址就可以完成節點註冊和監控。因此,也就減少了用戶配置節點的操作,簡化了使用流程。同時,由於隱藏了IP地址和端口,也更爲安全。另外,相較於Celery版本的監控,我們去除了Flower服務,不用在服務中單獨起一個Flower服務的進程,減少了開銷。

下圖是Crawlab UI界面上的節點之間的關係圖(拓撲圖)。

該架構的缺點

相較於一些常見的分佈式架構,例如Zookeeper,Crawlab還存在一些不完善的地方。

高可用性(High Availability)是Crawlab暫時還沒有做得很好的。例如,當主節點宕機的時候,整個系統就會癱瘓,因爲主節點是Crawlab的大腦中樞,負責很多功能。如果主節點宕機,前端就無法獲取API數據,任務無法調度,當然也無法監控節點了。雖然Zookeeper沒有將可用性(Availability)做得非常完善,但其投票選舉機制保證了其一定程度的高可用。如果Crawlab要改善這一點的話,會在主節點宕機後,用一定的方式選舉出另一個主節點,保證高可用。

節點通信

如果仔細看上面的整體架構圖的話,你可能會注意到Crawlab中通信有兩種。一種是同步信息(Sync via Msg),另一種是派發任務(Assign Tasks)。這兩種通信分別叫即時通信延遲通信。下面分別介紹。

即時通信

即時通信是指某節點A通過某種媒介向另一節點B發送信息,取決於是否爲雙向通信,節點B收到信息後可能會通過同一種媒介將信息回覆給節點A。

Crawlab的即時通信是通過Redis的PubSub來實現的(如下圖)。

所謂PubSub,簡單來說是一個發佈訂閱模式。訂閱者(Subscriber)會在Redis上訂閱(Subscribe)一個通道,其他任何一個節點都可以作爲發佈者(Publisher)在該通道上發佈(Publish)消息。

在Crawlab中,主節點會訂閱nodes:master通道,其他節點如果需要向主節點發送消息,只需要向nodes:master發佈消息就可以了。同理,各工作節點會各自訂閱一個屬於自己的通道nodes:<node_id>(node_id是MongoDB裏的節點ID,是MongoDB ObjectId),如果需要給工作節點發送消息,只需要發佈消息到該通道就可以了。

一個網絡請求的簡單過程如下:

  1. 客戶端(前端應用)發送請求給主節點(API);
  2. 主節點通過Redis PubSub的<nodes:<node_id>通道發佈消息給相應的工作節點;
  3. 工作節點收到消息之後,執行一些操作,並將相應的消息通過<nodes:master>通道發佈給主節點;
  4. 主節點收到消息之後,將消息返回給客戶端。

Crawlab的獲取日誌、獲取系統信息、取消任務、告知節點獲取爬蟲文件都是通過即時通信完成的。

而實現代碼相對來說有些複雜。下面是主節點的PubSub回調函數。

func MasterNodeCallback(channel string, msgStr string) {
    // 反序列化
    var msg NodeMessage
    if err := json.Unmarshal([]byte(msgStr), &msg); err != nil {
        log.Errorf(err.Error())
        debug.PrintStack()
        return
    }

    if msg.Type == constants.MsgTypeGetLog {
        // 獲取日誌
        fmt.Println(msg)
        time.Sleep(10 * time.Millisecond)
        ch := TaskLogChanMap.ChanBlocked(msg.TaskId)
        ch <- msg.Log
    } else if msg.Type == constants.MsgTypeGetSystemInfo {
        // 獲取系統信息
        fmt.Println(msg)
        time.Sleep(10 * time.Millisecond)
        ch := SystemInfoChanMap.ChanBlocked(msg.NodeId)
        sysInfoBytes, _ := json.Marshal(&msg.SysInfo)
        ch <- string(sysInfoBytes)
    }
}

這裏其實是用msg.Type來區分消息類別,如果要擴展的話需要寫不少if/else。工作節點的回調函數也需要寫類似的邏輯。

這個可能跟HTTP請求和RPC通信相較來說麻煩一些。不過,這其實和WebSocket非常像(對WebSocket不瞭解的同學可以看看韋世東最近的文章《開發者必知必會的 WebSocket 協議》),都需要在客戶端和服務端定義回調函數。一個改進方法是不用if/else來區分信息類別,轉而用PubSub頻道名稱,監聽多個頻道。總之,具體實踐中怎麼選擇,還需要考慮實際情況。

延遲通信

延遲通信對即時性要求不高,不需要節點或客戶端對請求即時回覆。通常來說,延遲通信的實現方式有隊列、輪詢等方式。這樣的方式不要求即時性。延遲通信主要是用作需要長時間的操作,例如發送郵件、數據處理、構建應用等等。

Crawlab中的延遲通信主要包含任務隊列以及輪詢,都是通過Redis來實現的。任務隊列是用作爬蟲任務執行:主節點接收抓取請求後,將執行任務的消息推到任務隊列中,工作節點不斷輪詢任務隊列,獲取任務並執行(如下圖)。Crawlab爬蟲任務執行的詳情請參見相關文檔源代碼

爬蟲任務執行

Crawlab的延遲通信主要包括爬蟲任務執行和爬蟲部署。爬蟲任務執行這裏不再贅述。下面簡單介紹一下爬蟲部署(流程如下圖)。

爬蟲部署

整個爬蟲部署的生命週期:

  1. 主節點每5秒,會從爬蟲的目錄獲取爬蟲信息,然後更新到數據庫(這個過程不涉及文件上傳);
  2. 主節點每60秒,從數據庫獲取所有的爬蟲信息,然後將爬蟲打包成zip文件,並上傳到MongoDB GridFS,並且在MongoDB的spiders表裏寫入file_id文件ID;
  3. 主節點通過Redis PubSub發佈消息(file.upload事件,包含文件ID)給工作節點,通知工作節點獲取爬蟲文件;
  4. 工作節點接收到獲取爬蟲文件的消息,從MongoDB GridFS獲取zip文件,並解壓儲存在本地。

這樣,所有爬蟲將被週期性的部署在工作節點上。

在後續的開發中,Crawlab將會加入郵件通知、短信通知、微信推送等功能。而這些都是屬於延遲通信的範疇,主要實現方法無外乎隊列和輪詢。

分佈式實踐 - 抓取上百個新聞網站

下面將介紹一個多機爬蟲的實際應用場景,幫助大家深入理解Crawlab的分佈式原理。

首先,你可能需要足夠的網絡帶寬,因爲需要抓取的網站上百了,不是簡簡單單的單機爬蟲,你需要多臺機器。這裏只是簡單介紹下拓撲架構,並不會詳細介紹大規模爬蟲的去重、反爬、容錯等邏輯。如下圖,每一個工作節點可以限制抓取一部分網站,總共M個網站平均分配給N個工作節點。在Crawlab中就是用指定節點的方式了,這個不難。另外,你也可以通過隨機分配的方式來派發任務,每一個工作節點統計上也會均勻分配到任務,這其實也就是一個負載均衡(Load Balancing)的過程。

當然,你可能好奇上百個新聞網站是不是需要寫上百個爬蟲。對於這個問題,沒有確切的準確回答。答案應該是“看情況”。對於自動提取字段的算法不夠自信的開發者來說,可以選擇Crawlab的可配置爬蟲(看這篇文章《我是如何在3分鐘內開發完一個爬蟲的》),這樣的開發成本相對來說比較小。但是對於已經有技術實力的可以寫出很好的通用提取規則的選手來說,只寫一個通用爬蟲就足夠了(簡單的列表頁提取規則參考《爬蟲平臺Crawlab核心原理--自動提取字段算法》),抓取多個網站等於抓取一個網站,不過還是需要部署在多個機器上,以求最大的帶寬和計算資源。當然,不管是哪一種,都繞不開去重、反爬、錯誤監控,不過這些不在本文討論範圍,網上有很多教程可以多學習一下。

社區

如果您覺得Crawlab對您的日常開發或公司有幫助,請加作者微信 tikazyq1 並註明"Crawlab",作者會將你拉入羣。歡迎在Github上進行star,以及,如果遇到任何問題,請隨時在Github上提issue。另外,歡迎您對Crawlab做開發貢獻。

<p align="center">

<img src="https://user-gold-cdn.xitu.io/2019/7/31/16c48234c8f5b366?w=674&h=896&f=jpeg&s=132795" height="360">

</p>

往期文章

本篇文章由ArtiPub自動發佈, ArtiPub讓您的文章隨處可閱
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章