1. 程式人生 > >訊息匯流排(MQ)知多少

訊息匯流排(MQ)知多少

1 什麼時候用MQ?

1.1 MQ的基本概念

  訊息匯流排(Message Queue,MQ),是一種跨程序的通訊機制,用於在上下游之間傳遞訊息。MQ是一種常見的上下游“邏輯解耦+物理解耦”的訊息通訊服務,訊息傳送上游只需要依賴MQ,邏輯上和物理上都不用依賴其他服務。

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的優點是:

  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)要想提升整體吞吐量,需要下游優化,例如批量處理等方式(訊息接收方需要做的)