近期在後臺任務應用上遇到多機消費同一個任務隊列的場景,須要引入必定的任務分配機制解決,由於以前也遇到過相似的問題,在此整理一下幾種可能的想法,也但願和你們交流討論更合理、更高效的方案。數據庫
假設咱們有一個集羣,用於處理一系列不一樣的任務,這時候咱們須要對任務進行的必定的分配,使得集羣中的每臺機器都負責一部分任務。編程
通常來講會有以下幾個要求:緩存
在這種場景下,該如何設計任務的分配方案?bash
爲了方便後續的展開,先約束一些表達:markdown
Source
: 用於表示任務的來源Cluster
: 用於表示整個集羣Task
: 用於表示抽象的任務Worker
: 用於表示實際執行任務的具體單元(如物理機)四者之間的關係能夠用下圖表示:多線程
最簡單,但也是很是有效的方案,在進行任務分配前須要提早肯定機器數量N,爲每一個任務進行編號(或直接使用其id),同時爲每一個執行任務的機器實例進行編號(0,1,2...)。併發
即便用下面的公式:負載均衡
Worker = TaskId % Cluster.size()
複製代碼
若是任務沒有id標識,那麼能夠經過隨機數的方式來分配任務,在任務數量足夠多的狀況下,能夠保證分配的均衡性,即:dom
Worker = random.nextInt() % Cluster.size()
複製代碼
簡單取模分配的優勢是足夠簡單,雖然負載均衡的效果比較粗糙,但能夠很快達到想要的效果,在作緊急任務分機分流的時候比較有用。但從長期上看,須要維護機器數量N的實時更新和推送,而且在機器數量發生變更的時候,可能會出現集羣內部的短暫不一致,若是業務對這個比較敏感,還須要進一步優化。分佈式
爲了達到「每一個任務只被一臺機器執行」的目標,能夠考慮使用分佈式鎖機制,當有多個Worker去消費Task時,只有第一個爭搶到鎖的Worker纔可以執行該Task。
理論上講,每次搶到鎖的Worker都是隨機的,那麼也就近似的實現了負載均衡;在有成熟中間件依賴的前提下,實現一個分佈式鎖也並不難(能夠藉助緩存系統的併發控制實現),而且不用考慮機器數量變化的問題。
但這個方案也有着不少的缺陷,首先爭搶鎖的過程自己就會消耗Worker的資源,另外因爲沒法預測究竟哪一個Worker可以爭搶到Task的鎖,因此基本不能保證整個集羣的負載均衡。
我我的認爲這種方案只適合於內容很是簡單、數量比較多,同時執行頻率很是高的任務分發(類比多線程讀寫緩存的場景)。
若是要作到比較精細的負載均衡,那麼最好的方式就是根據集羣的狀態、以及任務自己的特性去量身定製一套任務分配的規則,而後經過一箇中心的路由層來實現任務的調度,即:
一個簡單可行的分配規則是在調度前,計算Worker的CPU、內存等負載,計算一個權重,選擇壓力最小的機器去運行任務;再進一步能夠根據任務自己的複雜度作更精細的拆分。
該方案最大的問題在於,自主去實現一個路由層的成本比較高,另外有出現單點問題的風險(若是路由層掛了,整個任務調度就所有癱瘓了)。
這個是類比以前看到的,基於消息隊列的分佈式數據庫解決方案(原文),藉助一個可靠的Broker,咱們能夠很容易構建出一個生產者-消費者模型。
Source產出的Task將所有投入消息隊列中,下游的Worker接收Task,並執行(消費)。這樣的好處是減小了阻塞,同時能夠根據Worker的執行結果,配置重試策略(若是執行失敗,再次放回到隊列中)。但單單依賴Broker作任務分發的話,並不能解決咱們開頭的兩個問題,所以還須要:
防止消息被重複消費的機制
由於絕大多數的消息隊列Broker的傳輸邏輯都是「保證消息至少被送達一次」,因此頗有可能出現某個Task被多個Worker獲取到的現象,若是要確保「每一個任務都只被執行一次」,那麼這時候可能須要引入一下上面提到的鎖機制來防止重複消費。
不過若是你選擇NSQ做爲Broker的話,就不用考慮這個問題。NSQ的特性保證了某個消息在同一個channel下,必定只能被一個消費者消費。
任務分發
構建了生產者-消費者模型後,依然很差回答「哪一個Task要在哪一個Worker上運行」,也就是任務分發的機制,本質上仍是依賴於消費者消費動做的隨機性,若是要作更精細的調控,大體想一下有兩種方案。
一是在放入隊列前就根據所需規則計算好映射關係,而後對Task作一下標記,最後Worker能夠設置成只對含有特定標記的Task生效,或者根據Task的標記作不一樣Topic來分發。
而是在取出隊列的時候再進行計算,這樣的話可能下游又須要維護一個路由層來作轉發,感受有些得不償失。
就大多數實際狀況而言,依賴Broker自己的消息分發機制便可。
參考響應式編程中的背壓概念。把Source端推送(Push)任務的過程改成Worker端拉取(Pull)任務,「反客爲主」,來實現流速控制和負載均衡。
簡單的說,咱們須要Worker(也多是Cluster)可以根據自身的狀況來預估本身接下來可以承接的任務量,並將其反饋給Source,而後Source生產Task並傳送給Worker(或者Cluster)。
設想一個可行的方案,將Source視爲Server,Worker視爲Client,那麼便造成了一種反向的C/S模式。
其中Worker端的行爲是不斷重複「請求獲取Task -> 運行Task -> 請求獲取Task」這個循環。每當Worker評估自身處於「空閒」狀態時,就向Source端發送請求,來獲取Task並運行。
Source端則相對比較簡單,只須要實現一個接口,每當有請求過來時,返回一個Task,並標記該Task被消費便可。
這種思路雖然能夠較好的保證每臺Worker機器負載處於可控範圍,但也存在幾個問題。
首先是流速問題,由於整個任務隊列的消費速度在此模式下徹底由Worker自己調控,而任務隊列的狀態(還有多少任務須要處理、哪些任務比較緊急..)對Worker是不可見的,因此有可能致使任務在Source端的堆積。
其次是任務調度的延時問題,由於Source端徹底沒法預知下一個Worker的請求會在何時到來,因此對於任何一個被提交的Task,都沒法保證其在什麼時間被執行。對於後臺任務而言這個問題倒不是很大,但對於前臺任務就很是致命了。
要解決上面兩個問題,須要在Source端引入一個合理的任務分配機制,在極端狀況下可能還須要Source端可以強制進行Task的分發。