消息隊列是怎樣設計的
之前閱讀過一篇美團的技術文章《消息隊列設計精要》,覺得寫的不錯,可以讓人更加了解消息隊列
消息隊列的設計要點,大致總結整理如下:
- 最簡單的消息隊列可以做成一個消息轉發器,把一次RPC做成兩次RPC。
- 構建一個整體的數據流,例如producer發送給broker,broker發送給consumer,consumer回復消費確認,broker刪除/備份消息等。
- 利用RPC將數據流串起來(可以使用開源RPC框架,比如Dubbo等)
- 負載均衡、
- 服務發現、
- 通信協議:Thrift,Dubbo
- 序列化協議
- 存儲子系統:通過存儲承載消息堆積,然後在合適的時機投遞消息
- 持久化、非持久化;
- 從速度來看,文件系統 > 分散式KV(持久化)> 分散式文件系統 > 資料庫,而可靠性卻截然相反。需要根據具體業務選擇
- 消費關係的保存:
- 單播:點對點
- 多播:一點對多點
- 通用設計:支持組間廣播,不同的組註冊不同的訂閱。組內的不同機器,如果註冊一個相同的ID,則單播;如果註冊不同的ID(如IP地址+埠),則廣播。
- 廣播關係的維護,一般由於消息隊列本身都是集群,所以都維護在公共存儲上,如config server、zookeeper等。
- 消費確認(任務執行完整性):
- 把消息的送達和消息的處理分開,這樣才真正的實現了消息隊列的本質-解耦。
- 對於沒有特殊邏輯的消息,默認Auto Ack也是可以的
- 但一定要允許消費方主動進行消費確認ack,並與broker約定下次投遞時間
- 高級特性:
- 可靠投遞(最終一致性):
- 高可用:超時重發與消息重複、消息隊列冪等設計(broker多機器共享一個DB或者一個分散式文件/kv系統)、定時任務補償
- 不是所有的系統都要求最終一致性或者可靠投遞。任何基礎組件要服務於業務場景。
- 消息可能會重複,並且在異常情況下,要接受消息的延遲。
- 每當要發生不可靠的事情(RPC等)之前,先將消息落地,然後發送。
- 當消息發送失敗或者不知道成功失敗(比如超時)時,消息狀態是待發送。
- 對於各種不確定(超時、down機、消息沒有送達、送達後數據沒落地、數據落地了回復沒收到),其實對於發送方來說,都是一件事情,就是消息沒有送達。
- 定時任務不停輪詢所有待發送消息,最終一定可以送達。
- 重複消息:
- 如何鑒別消息重複,並冪等的處理重複消息:
- 版本號
- 狀態機
- 一個消息隊列server如何盡量減少重複消息的投遞:
- 鑒別消息重複:broker記錄MessageId,直到投遞成功後清除,重複的ID到來不做處理。
- 減少重複投遞:對於server投遞到consumer的消息,由於不確定對端是在處理過程中還是消息發送丟失的情況下,有必要記錄下投遞的IP地址。決定重發之前詢問這個IP,消息處理成功了嗎?如果詢問無果,再重發。
- 順序消息:
- 順序消息要求:允許消息丟失;從發送方到服務方到接受者都是單點單線程。
- pull模式實現比較容易(見下)
- 一個主流消息隊列的設計範式里,應該是不丟消息的前提下,盡量減少重複消息,不保證消息的投遞順序。
- 事務特性:
- 在與本地業務的同一個事務中,本地消息落地(落庫、需要業務方提供資料庫)
- 消息只要投遞到服務端確認後本地才做刪除
- 定時任務掃描本地消息庫表進行補償發送
- 性能優化
- 非同步:
- 對於客戶端來說,同步與非同步主要是拿到一個Result,還是Future(Listenable)的區別。實現方式可以是線程池,NIO或者其他事件機制。
- 服務端非同步需要RPC協議支持。參考servlet 3.0規範,服務端可以吐一個future給客戶端,並且在future done的時候通知客戶端。
- 實踐:
- 客戶端必須等待服務端消息成功落地,才算是消息發送成功。
- 我們不希望消息的發送阻塞客戶端的主流程,所以可以先使用線程池提交一個發送請求,主流程繼續往下走。
- 服務端是純非同步。客戶端的線程池wait在服務端吐回的future上,直到服務端處理完畢,才解除阻塞繼續進行。
- 批量:消費者合適消費消息:通過網路請求小包合併成大包提高性能
- 消息消費是push還是pull:
- push:push模型最大的致命傷是慢消費。
- 如果消費者的速度比發送者的速度慢很多,勢必造成消息在broker的堆積。
- 最致命的是broker給consumer推送一堆consumer無法處理的消息,consumer不是reject就是error,然後來回踢皮球
- 所以對於建立索引等慢消費,消息量有限且到來的速度不均勻的情況,pull模式比較合適。
- pull:pull模式存在消息延遲與忙等問題
- pull模式如果想做到全局順序消息,就相對容易很多:
- producer對應partition,並且單線程。
- consumer對應partition,消費確認(或批量確認),繼續消費即可。
- 所以對於日誌push送這種最好全局有序,但允許出現小誤差的場景,pull模式非常合適。如果你不想看到通篇亂套的日誌~~Anyway,需要順序消息的場景還是比較有限的而且成本太高,請慎重考慮。
本文首發於公眾號:EnjoyMoving,歡迎關注交流~
https://wx4.sinaimg.cn/mw690/73036ef6ly1fwn6kdgxm8j20c00cfmye.jpg(公眾號二維碼)
推薦閱讀: