1. 程式人生 > >高可靠高效能的訊息佇列怎麼去實現?

高可靠高效能的訊息佇列怎麼去實現?

我們看看RPC呼叫的場景。服務A呼叫如圖所示服務。在正常情況下,一般都不會有問題。但是在以下情況,服務A呼叫會遇到問題。

1.png

問題一:如果有流量高峰,服務B響應超時,會發生什麼情況?

整個RPC呼叫鏈路都會受到影響,甚至發生雪崩。

問題二:服務A邏輯複雜,邏輯耦合嚴重,怎麼做拆分?

把一些呼叫鏈路中可以非同步呼叫的邏輯調整為消費MQ訊息。

問題三:RPC呼叫,jar依賴問題?服務B升級,呼叫B的相關服務是否需要升級?

RPC服務需要依賴生成的介面描述jar,服務介面升級一般很難做到向前相容,所以相關呼叫方也需要升級。

MQ是以訊息為載體的可靠非同步呼叫的框架,能很好的應對上面三個問題。流量削峰,MQ是天然支援的,因為MQ有可靠儲存,可以落地。解耦合,交給MQ也很合適。因為MQ的接入方處理的是訊息,做到向前相容也是比較容易的。

2.png

使用MQ之後,服務A,B通過MQ做到鬆耦合,也能很好的應對流量高峰。

MQ很好很強大,是RPC的有效補充,那問題來了怎麼實現一個可靠MQ?本文結合二手交易平臺的特點,詳細介紹轉轉ZZMQ架構設計實踐。

訊息佇列架構設計

我們考慮的重點是:可靠,高效能,運維友好,接入方便,可以支援大量堆積,有效緩衝業務高峰,針對這些需求,我們做了一些設計上的考慮和取捨,最終形成了如下的架構方案。

3.jpeg

RegisterCenter 作為註冊中心,負責路由服務,無狀態,每個NameNode都是對等的,NameNode 可以任意水平擴充套件。NameNode 與broker和Client都建立了長連線。Broker 內是主從兩臺機器,slave從master拉取訊息Log和消費Offset,做HA。Broker也可以方便地水平擴充套件,加入新機器,更新topic的路由資訊,client會定時更新路由資訊。Consumer 和Producer 都需要到註冊中心註冊,同時拉取Topic的路由資訊。Management Protal 是用來管理叢集,維護Topic的相關聯絡人資訊。

儲存設計

一個高效能ZZMQ的瓶頸和難點就是儲存系統,儲存系統關乎到效能,資料可靠儲存實現是否簡單,資料備份控制,訊息狀態的表示。

目前最常見的是以Kafka為代表的Log-append-file,檔案順序寫,追求吞吐量,訊息消費狀態通過offset來控制,還有以各種儲存引擎實現的,比如leveldb(activemq),rocksdb等。

4.jpeg

從這張圖(http://queue.acm.org/detail.cfm?id=1563874),我們能看到磁碟順序讀寫的效能甚至超過了記憶體隨機訪問的效能,能達到50多M/s。並且,相對來說,Log-append-file簡單一些。最後選擇了類似kafka的log-append-file。

Kafkatopic分成partition,順序讀寫,partition有一個小的問題,單機不能支撐大量的topic,單機能支援幾百分割槽,隨著分割槽數量的增加,效能有所下降。而我們的場景的是:業務大部分topic的qps並不像kafka處理的日誌檔案那麼高,希望能單機支撐更多的topic數量。

針對這一點,我們做了一些調整,topic訊息本身不再分割槽,統一儲存,使用更加輕量的Consumer Queue實現partition的功能。Consumer Queue儲存的只是位置資訊,更加輕量,能做到支援更多的topic。

訊息寫入pageCache,再Dispatch到對應的Consumer Queue中去,Consumer Queue只有訊息Log的offset資訊,以及大小和其他的一些元資料,佔用很小的空間。這樣設計,經過驗證,單機能夠支援幾千佇列。

這樣的設計也帶來了一個副作用,因為訊息本身是統一儲存,topic數量多的時候,訊息的讀取是隨機讀,效能有些許下降,目前,我們通過優化linuxpageCache和IO排程,開啟系統預讀,效能相對順序讀沒有太大下降。

訊息訂閱模型

5.png

Consumergroup A 有兩個消費者例項,B也有兩個消費者例項,同時訂閱了Topic-A,他們之間的消費進度是獨立的。Group處理相同topic,消費邏輯一致的一個整體,包含不同的消費例項。

ConsumerGroup基本上是跟kafka的Group一致,由不同的ConsumerGroup來區分不同的消費組,維護Group對應的消費狀態,能很方便的實現。

網路

基於netty實現了網路層的協議。Netty對nio,和reactor做了很好的封裝,netty4.x 對direct buffer的利用,以及高效的buffer分配和回收演算法,netty也解決了TCP協議的半包問題,使用方不需要自己組TCP包。用netty實現網路層協議,網路層的可靠由netty來做,減少了複雜度。

Push 還是Pull

這個設計的考慮主要是兩個方面:慢消費,以及消費延遲。主動Push能做到低的消費延遲,但是對於慢消費,不能很好的應對,主動Push需要感知消費者的速率,不至於push 太快,把消費者壓垮了。Pull模式,由消費者控制拉取的速度,能很好的應對慢消費的問題,但是,Pull模式對消費延遲不敏感,拉取的頻率不好控制,處理不好有可能造成CPU使用率飆升。參照kafka的pull,實現了longpull。Consumer 與Broker建立長連線,Consumer發起拉取請求,如果有訊息,Broker 返回訊息,如果沒有訊息,broker hold住這個請求一段時間,等有訊息再notify這個請求,返回給客戶端。基於long pull,消費延遲能降到跟push差不多,同時又能由Consumer 控制拉取速度。

重複訊息和順序訊息

訊息重複在一個複雜網路條件下很難避免的,如果由MQ本身來做去重,代價太大,所以我們要求接入MQ的邏輯做好冪等(狀態機或者版本號的一些機制)。順序訊息,跟kafka一樣,Consumer queue跟partition類似,一個Queue內部,消費是有序的。如果要做到完全有序,只能一個Producer,一個Consumer。

高可用

Broker組由主從兩臺機器構成,可以配置的策略有同步雙寫和非同步複製。預設情況,採用的非同步複製,由slave去拉取master上面的訊息Log和offset。Broker 接入了內部的監控系統,每分鐘上報topic的消費情況和broker狀態,能做到分鐘級別發現異常消費。訊息寫入PageCache之後,預設是非同步刷盤。也可以配置為同步刷盤,只有刷盤成功才會返回。

6.png

Broker的預設配置非同步刷盤,Master-Slave非同步複製。Client消費消費訊息的可靠是通過Consume queue的offset來保證的,Client會定時上報已經消費成功的Offset資訊。如果Client被異常kill掉,沒有確認的訊息會被Client 重新拉取,消費。

一條訊息的生命過程

Proudcer通過與NameNode的路由找到topic的broker地址,producer發訊息,netty序列化,通過TCP傳輸到對應的broker,broker寫log的pageCache,返回。Broker Dispatch訊息到對應的Consumer queue,broker喚醒刷盤執行緒,broker喚醒slave同步執行緒,slave拉取訊息。

NotifyConsumer 拉取請求,經過netty序列化,通過tcp到達Consumer,Consumer消費成功,Client上報消費成功offset。

Broker持久化Offset,slave同步offset。三天之後,broker刪除訊息。

PS:關注360linker公眾號,入官方社群取免費視訊教程、知名單位招聘資訊。交流分享IT圈學習經驗。