消息總線初探

1 何時用MQ?

1.1 MQ的基本概念

  消息總線(Message Queue,MQ),是一種跨進程的通訊機制,用於在上下游之間傳遞消息。MQ是一種常見的上下游「邏輯解耦+物理解耦」的消息通訊服務,消息發送上游只須要依賴MQ,邏輯上和物理上都不用依賴其餘服務。web

1.2 MQ的使用場景

場景一:數據驅動的任務依賴

  有些任務之間有必定的依賴關係,好比:task3須要使用task2的輸出做爲輸入,task2須要使用task1的輸出做爲輸入。這樣的話,tast1, task2, task3之間就有任務依賴關係,必須task1先執行,再task2執行,再task3執行。對於這類需求,常見的實現方式是,使用cron人工排執行時間表:數組

  1. task1,0:00執行,經驗執行時間爲50分鐘;
  2. task2,1:00執行(爲task1預留10分鐘buffer),經驗執行時間也是50分鐘;
  3. task3,2:00執行(爲task2預留10分鐘buffer)
    這裏寫圖片描述

這種方法的壞處是:微信

  1. 若是有一個任務執行時間超過了預留buffer的時間,將會獲得錯誤的結果;
  2. 總任務的執行時間很長,老是要預留不少buffer,若是前置任務提早完成,後置任務不會提早開始;
  3. 若是一個任務被多個任務依賴,這個任務將會稱爲關鍵路徑,排班表很難體現依賴關係,容易出錯;
  4. 若是有一個任務的執行時間要調整,將會有多個任務的執行時間要調整。

優化方案是,採用MQ解耦:數據結構

  1. task1準時開始,結束後發一個「task1 done」的消息;
  2. task2訂閱「task1 done」的消息,收到消息後第一時間啓動執行,結束後發一個「task2 done」的消息;
  3. task3同理
    這裏寫圖片描述

採用MQ的優勢是:架構

  1. 不須要預留buffer,上游任務執行完,下游任務總會在第一時間被執行;
  2. 依賴多個任務,被多個任務依賴都很好處理,只須要訂閱相關消息便可;
  3. 有任務執行時間變化,下游任務都不須要調整執行時間

  須要特別說明的是,MQ只用來傳遞上游任務執行完成的消息,並不用於傳遞真正的輸入輸出數據。框架

場景二:上游沒必要關心執行結果

  上游須要關注執行結果時要用「調用」;上游不關注執行結果時,就可使用MQ了。58同城的不少下游須要關注「用戶發佈帖子」這個事件,好比用戶發佈帖子後,修改用戶統計數據。
  對於這類需求,常見的實現方式是使用調用關係:帖子發佈服務執行完成以後,調用下游業務來完成消息的通知。但事實上,這個通知是否正常正確的執行,帖子發佈服務根本不關注。異步

這種方法的壞處是:微信支付

  1. 帖子發佈流程的執行時間增長了;
  2. 下游服務宕機,可能致使帖子發佈服務受影響,上下游邏輯+物理依賴嚴重;
  3. 每當增長一個須要知道「帖子發佈成功」信息的下游,修改代碼的是帖子發佈服務,屬於架構設計中典型的依賴倒轉。
    這裏寫圖片描述

優化方案是,採用MQ解耦:優化

  1. 帖子發佈成功後,向MQ發一個消息;
  2. 哪一個下游關注「帖子發佈成功」的消息,主動去MQ訂閱
    這裏寫圖片描述

採用MQ的優勢是:spa

  1. 上游執行時間短;
  2. 上下游邏輯+物理解耦,除了與MQ有物理鏈接,模塊之間都不相互依賴;
  3. 新增一個下游消息關注方,上游不須要修改任何代碼

場景三:上游關注執行結果,但執行時間很長

  有時候上游須要關注執行結果,但執行結果時間很長。微信支付,跨公網調用微信的接口,執行時間會比較長,但調用方又很是關注執行結果,此時通常怎麼玩呢?

通常採用「回調網關+MQ」方案來解耦:

  1. 調用方直接跨公網調用微信接口;
  2. 微信返回調用成功,此時並不表明返回成功;
  3. 微信執行完成後,回調統一網關;
  4. 網關將返回結果通知MQ;
  5. 請求方收到結果通知
    這裏寫圖片描述

  這裏須要注意的是,不該該由回調網關來調用上游來通知結果,若是是這樣的話,每次新增調用方,回調網關都須要修改代碼,仍然會反向依賴,使用回調網關+MQ的方案,新增任何對微信支付的調用,都不須要修改代碼啦。

1.3 何時不使用MQ

  雖然MQ是分層架構中的解耦利器,但調用與被調用的關係,是沒法被MQ取代的。

MQ的不足是:

  1. 系統更復雜,多了一個MQ組件;
  2. 消息傳遞路徑更長,延時會增長;
  3. 消息可靠性和重複性互爲矛盾,消息不丟不重難以同時保證;
  4. 上游沒法知道下游的執行結果,這一點是很致命的

例如:用戶登陸場景,登陸頁面調用passport服務,passport服務的執行結果直接影響登陸結果,此處的」登陸頁面」與」passport服務」就必須使用調用關係,而不能使用MQ通訊。

1.4 總結

  1. MQ是一個互聯網架構中常見的解耦利器。
  2. 何時不使用MQ?上游實時關注執行結果。
  3. 何時使用MQ?1)數據驅動的任務依賴; 2)上游不關心多下游執行結果; 3)異步返回執行時間長。

2 MQ是如何作到消息必達?

  MQ要想盡可能消息必達,架構上有兩個核心設計點:(1)消息落地(2)消息超時、重傳、確認。

2.1 MQ核心架構

這裏寫圖片描述

  MQ是一個系統間解耦的利器,它可以很好的解除發佈者、訂閱者之間的耦合,將上下游的消息投遞解耦成兩個部分。MQ的核心架構圖,基本能夠分爲三大塊:

  1. 發送方 -> 左側粉色部分,由兩部分構成:業務調用方與MQ-client-sender,其中後者向前者提供了兩個核心API:SendMsg(bytes[] msg)、SendCallback();
  2. MQ核心集羣 -> 中間藍色部分,分爲四個部分:MQ-server,zk,db,管理後臺web;
  3. 接收方 -> 右側黃色部分,由兩部分構成:業務接收方與MQ-client-receiver,其中後者向前者提供了兩個核心API:RecvCallback(bytes[] msg)、SendAck()

2.2 MQ消息可靠投遞核心流程

  MQ既然將消息投遞拆成了上下半場,爲了保證消息的可靠投遞,上下半場都必須儘可能保證消息必達。
這裏寫圖片描述

MQ消息投遞上半場,MQ-client-sender到MQ-server流程見上圖:

  1. MQ-client將消息發送給MQ-server(此時業務方調用的是API:SendMsg);
  2. MQ-server將消息落地,落地後即爲發送成功;
  3. MQ-server將應答發送給MQ-client(此時回調業務方是API:SendCallback)

  MQ消息投遞下半場,MQ-server到MQ-client-receiver流程見上圖:

  1. MQ-server將消息發送給MQ-client(此時回調業務方是API:RecvCallback);
  2. MQ-client回覆應答給MQ-server(此時業務方主動調用API:SendAck);
  3. MQ-server收到ack,將以前已經落地的消息刪除,完成消息的可靠投遞

2.3 若是消息丟了怎麼辦?

  MQ消息投遞的上下半場,均可以出現消息丟失,爲了下降消息丟失的機率,MQ須要進行超時和重傳。

2.3.1 上半場的超時與重傳

  MQ上半場的1或者2或者3若是丟失或者超時,MQ-client-sender內的timer會重發消息,直到指望收到3,若是重傳N次後還未收到,則SendCallback回調發送失敗,須要注意的是,這個過程當中MQ-server可能會收到同一條消息的屢次重發。

2.3.2 下半場的超時與重傳

  MQ下半場的4或者5或者6若是丟失或者超時,MQ-server內的timer會重發消息,直到收到5而且成功執行6,這個過程可能會重發不少次消息,通常採用指數退避的策略,先隔x秒重發,2x秒重發,4x秒重發,以此類推,須要注意的是,這個過程當中MQ-client-receiver也可能會收到同一條消息的屢次重發。

3.MQ如何作到消息冪等

3.1 消息必達的前提

MQ消息必達,架構上有兩個核心設計點:消息落地,消息超時、重傳、確認
這裏寫圖片描述
  它由發送端、服務端、固化存儲、接收端四大部分組成。爲保證消息的可達性,超時、重傳、確認機制可能致使消息總線、或者業務方收到重複的消息,從而對業務產生影響。因此,MQ冪等性設計相當重要。

3.2 上半場的冪等性設計

MQ消息發送上半場,即上圖中的步驟1-3

1,發送端MQ-client將消息發給服務端MQ-server;
2,服務端MQ-server將消息落地;
3,服務端MQ-server回ACK給發送端MQ-client

  若是3丟失,發送端MQ-client超時後會重發消息,可能致使服務端MQ-server收到重複消息。此時重發是MQ-client發起的,消息的處理是MQ-server。
  爲了不步驟2落地重複的消息,對每條消息,MQ系統內部必須生成一個inner-msg-id,做爲去重和冪等的依據,這個內部消息ID的特性是:

1)全局惟一;
2)MQ生成,具有業務無關性,對消息發送方和消息接收方屏蔽

  有了這個inner-msg-id,就能保證上半場重發,也只有1條消息落到MQ-server的DB中,實現上半場冪等。

3.3 下半場的冪等性設計

MQ消息發送下半場,即上圖中的步驟4-6

4,服務端MQ-server將消息發給接收端MQ-client;
5,接收端MQ-client回ACK給服務端;
6,服務端MQ-server將落地消息刪除

  須要強調的是,接收端MQ-client回ACK給服務端MQ-server,是消息消費業務方的主動調用行爲,不能由MQ-client自動發起,由於MQ系統不知道消費方何時真正消費成功。
  若是5丟失,服務端MQ-server超時後會重發消息,可能致使MQ-client收到重複的消息。此時重發是MQ-server發起的,消息的處理是消息消費業務方,消息重發勢必致使業務方重複消費。爲了保證業務冪等性,業務消息體中,必須有一個biz-id,做爲去重和冪等的依據,這個業務ID的特性是:

(1)對於同一個業務場景,全局惟一
(2)由業務消息發送方生成,業務相關,對MQ透明
(3)由業務消息消費方負責判重,以保證冪等

  有了這個業務ID,纔可以保證下半場消息消費業務方即便收到重複消息,也只有1條消息被消費,保證了冪等。

3.4 總結

  MQ爲了保證消息必達,消息上下半場都可能發送重複消息,如何保證消息的冪等性呢?

上半場

MQ-client生成inner-msg-id,保證上半場冪等。
這個ID全局惟一,業務無關,由MQ保證。

下半場

業務發送方帶入biz-id,業務接收方去重保證冪等。
這個ID對單業務惟一,業務相關,對MQ透明。

結論:冪等性,不只對MQ有要求,對業務上下游也有要求。

4. MQ如何實現消息延遲

4.1 緣起

  不少時候,業務有「在一段時間以後,完成一個工做任務」的需求。例如:滴滴打車訂單完成後,若是用戶一直不評價,48小時後會將自動評價爲5星。通常來講怎麼實現這類「48小時後自動評價爲5星」需求呢?常見方案:啓動一個cron定時任務,每小時跑一次,將完成時間超過48小時的訂單取出,置爲5星,並把評價狀態置爲已評價。
  假設訂單表的結構爲:t_order(oid, finish_time, stars, status, …),更具體的,定時任務每隔一個小時會這麼作一次:
select oid from t_order where finish_time > 48hours and status=0;
update t_order set stars=5 and status=1 where oid in[…];

  若是數據量很大,須要分頁查詢,分頁update,這將會是一個for循環。方案的不足:

(1)輪詢效率比較低
(2)每次掃庫,已經被執行過記錄,仍然會被掃描(只是不會出如今結果集中),有重複計算的嫌疑
(3)時效性不夠好,若是每小時輪詢一次,最差的狀況下,時間偏差會達到1小時
(4)若是經過增長cron輪詢頻率來減小(3)中的時間偏差,(1)中輪詢低效和(2)中重複計算的問題會進一步凸顯

4.2 高效延時消息設計與實現

  高效延時消息,包含兩個重要的數據結構:

(1)環形隊列,例如能夠建立一個包含3600個slot的環形隊列(本質是個數組)
(2)任務集合,環上每個slot是一個Set

  同時,啓動一個timer,這個timer每隔1s,在上述環形隊列中移動一格,有一個Current Index指針來標識正在檢測的slot。

Task結構中有兩個很重要的屬性:

(1)Cycle-Num:當Current Index第幾圈掃描到這個Slot時,執行任務
(2)Task-Function:須要執行的任務指針

這裏寫圖片描述

  假設當前Current Index指向第一格,當有延時消息到達以後,例如但願3610秒以後,觸發一個延時消息任務,只需:

(1)計算這個Task應該放在哪個slot,如今指向1,3610秒以後,應該是第11格,因此這個Task應該放在第11個slot的Set中
(2)計算這個Task的Cycle-Num,因爲環形隊列是3600格(每秒移動一格,正好1小時),這個任務是3610秒後執行,因此應該繞3610/3600=1圈以後再執行,因而Cycle-Num=1

  Current Index不停的移動,每秒移動到一個新slot,這個slot中對應的Set,每一個Task看Cycle-Num是否是0:

(1)若是不是0,說明還須要多移動幾圈,將Cycle-Num減1
(2)若是是0,說明立刻要執行這個Task了,取出Task-Funciton執行(能夠用單獨的線程來執行Task),並把這個Task從Set中刪除

  使用了「延時消息」方案以後,「訂單48小時後關閉評價」的需求,只需將在訂單關閉時,觸發一個48小時以後的延時消息便可:

(1)無需再輪詢所有訂單,效率高
(2)一個訂單,任務只執行一次
(3)時效性好,精確到秒(控制timer移動頻率能夠控制精度)

4.3 總結

  環形隊列是一個實現「延時消息」的好方法,開源的MQ好像都不支持延遲消息,不妨本身實現一個簡易的「延時消息隊列」,能解決不少業務問題,並減小不少低效掃庫的cron任務。

5.MQ如何實現削峯填谷

5.1 站點與服務、服務與服務上下游之間,通常如何通信?

  一種是「直接調用」,經過RPC框架,上游直接調用下游;另外一種是採用「MQ推送」,上游將消息發給MQ,MQ將消息推送給下游。

5.2 爲何會有流量衝擊?

  無論採用「直接調用」仍是「MQ推送」,都有一個缺點,下游消息接收方沒法控制到達本身的流量,若是調用方不限速,頗有可能把下游壓垮。假如,上游下單業務簡單,每秒發起了10000個請求,下游秒殺業務複雜,每秒只能處理2000個請求,頗有可能致使下游系統被壓垮,引起雪崩。

  爲了不雪崩,常見的優化方案有兩種:1)業務上游隊列緩衝,限速發送;2)業務下游隊列緩衝,限速執行。

5.3 MQ怎麼改能緩衝流量?

  由MQ-server推模式,升級爲MQ-client拉模式。MQ-client根據本身的處理能力,每隔必定時間,或者每次拉取若干條消息,實施流控,達到保護自身的效果。而且這是MQ提供的通用功能,無需上下游修改代碼。

5.4 若是上游發送流量過大,會不會致使消息在MQ中堆積?

  下游MQ-client拉取消息,消息接收方可以批量獲取消息,須要下游消息接收方進行優化,方可以提高總體吞吐量,例如:批量寫。

5.4 結論

1)MQ-client提供拉模式,定時或者批量拉取,能夠起到削平流量,下游自我保護的做用(MQ須要作的) 2)要想提高總體吞吐量,須要下游優化,例如批量處理等方式(消息接收方須要作的)

相關文章
相關標籤/搜索