RocketMQ原理、源碼分析及實踐

原理描述

模塊架構

 

 

RocketMQ架構上主要分爲四部分,如上圖所示:

  • Producer:消息發佈的角色,支持分佈式集羣方式部署。Producer通過MQ的負載均衡模塊選擇相應的Broker集羣隊列進行消息投遞,投遞的過程支持快速失敗並且低延遲。

  • Consumer:消息消費的角色,支持分佈式集羣方式部署。支持以push推,pull拉兩種模式對消息進行消費。同時也支持集羣方式和廣播方式的消費,它提供實時消息訂閱機制,可以滿足大多數用戶的需求。

  • NameServer:NameServer是一個非常簡單的Topic路由註冊中心,其角色類似Dubbo中的zookeeper,支持Broker的動態註冊與發現。主要包括兩個功能:Broker管理,NameServer接受Broker集羣的註冊信息並且保存下來作爲路由信息的基本數據。然後提供心跳檢測機制,檢查Broker是否還存活;路由信息管理,每個NameServer將保存關於Broker集羣的整個路由信息和用於客戶端查詢的隊列信息。然後Producer和Conumser通過NameServer就可以知道整個Broker集羣的路由信息,從而進行消息的投遞和消費。NameServer通常也是集羣的方式部署,各實例間相互不進行信息通訊。Broker是向每一臺NameServer註冊自己的路由信息,所以每一個NameServer實例上面都保存一份完整的路由信息。當某個NameServer因某種原因下線了,Broker仍然可以向其它NameServer同步其路由信息,Producer,Consumer仍然可以動態感知Broker的路由的信息。

  • BrokerServer:Broker主要負責消息的存儲、投遞和查詢以及服務高可用保證,爲了實現這些功能,Broker包含了以下幾個重要子模塊。

  1. Remoting Module:整個Broker的實體,負責處理來自clients端的請求。
  2. Client Manager:負責管理客戶端(Producer/Consumer)和維護Consumer的Topic訂閱信息
  3. Store Service:提供方便簡單的API接口處理消息存儲到物理硬盤和查詢功能。
  4. HA Service:高可用服務,提供Master Broker 和 Slave Broker之間的數據同步功能。
  5. Index Service:根據特定的Message key對投遞到Broker的消息進行索引服務,以提供消息的快速查詢。

存儲概念

 

 

  1. CommitLog:消息主體以及元數據的存儲主體,存儲Producer端寫入的消息主體內容,消息內容不是定長的。單個文件大小默認1G ,文件名長度爲20位,左邊補零,剩餘爲起始偏移量,比如00000000000000000000代表了第一個文件,起始偏移量爲0,文件大小爲1G=1073741824;當第一個文件寫滿了,第二個文件爲00000000001073741824,起始偏移量爲1073741824,以此類推。消息主要是順序寫入日誌文件,當文件滿了,寫入下一個文件;
  2. ConsumeQueue:消息消費隊列,引入的目的主要是提高消息消費的性能,由於RocketMQ是基於主題topic的訂閱模式,消息消費是針對主題進行的,如果要遍歷commitlog文件中根據topic檢索消息是非常低效的。Consumer即可根據ConsumeQueue來查找待消費的消息。其中,ConsumeQueue(邏輯消費隊列)作爲消費消息的索引,保存了指定Topic下的隊列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。consumequeue文件可以看成是基於topic的commitlog索引文件,故consumequeue文件夾的組織方式如下:topic/queue/file三層組織結構,具體存儲路徑爲:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同樣consumequeue文件採取定長設計,每一個條目共20個字節,分別爲8字節的commitlog物理偏移量、4字節的消息長度、8字節tag hashcode,單個文件由30W個條目組成,可以像數組一樣隨機訪問每一個條目,每個ConsumeQueue文件大小約5.72M;
  3. IndexFile:IndexFile(索引文件)提供了一種可以通過key或時間區間來查詢消息的方法。Index文件的存儲位置是:$HOME \store\index${fileName},文件名fileName是以創建時的時間戳命名的,固定的單個IndexFile文件大小約爲400M,一個IndexFile可以保存 2000W個索引,IndexFile的底層存儲設計爲在文件系統中實現HashMap結構,故rocketmq的索引文件其底層實現爲hash索引。

調用邏輯

nameserv

nameserv比較簡單,其實就是一個註冊中心的邏輯,broker啓動之後會把自己的地址信息等註冊到namserv上,這裏其實就是基於netty的通信,然後producer實例啓動的時候會先去和nameserv建立鏈接然後獲取broker信息,name不承載負載均衡的功能。

producer

因爲負載均衡都是在客戶端實現的,所以producer會首先啓動一個客戶端,這個客戶端首先會去啓動netty實例,連接nameserv將producer自己註冊到nameserv上。之後在發送消息時,producer會從nameserv上獲取topic對應broker的存儲列表,更新本地的緩存,嘗試找到topic對應的broker。

 

如果沒有找到topic,相當於自動創建topic的邏輯,他就會自己創建一個topic的config,然後放到本地緩存的topic的table裏面,之後會根據這個topic的table選擇一個消費隊列,這個消費隊列後面在broker的地方說,默認消費隊列的數量是4,如果是一個新的topic會獲取一個隨機的隊列進行添加,同時還可能會有一個指數退避容錯的策略運行,具體的容錯策略均在MQFaultStrategy這個類中定義。這裏有一個sendLatencyFaultEnable開關變量,如果開啓,在隨機遞增取模的基礎上,再過濾掉not available的Broker代理。所謂的"latencyFaultTolerance",是指對之前失敗的,按一定的時間做退避。例如,如果上次請求的latency超過550Lms,就退避3000Lms;超過1000L,就退避60000L;如果關閉,採用隨機遞增取模的方式選擇一個隊列(MessageQueue)來發送消息,latencyFaultTolerance機制是實現消息發送高可用的核心關鍵所在。

 

 

 

所以最終producer的發送的東西就是,選擇broker,然後選擇brocker的消費隊列,把這個隊列的id,消息的topic,消息的內容,tag等東西封裝到消息報文裏,通過netty發送到broker端。

broker

broker這個東西是所有模塊裏面最複雜的,首先它啓動的時候創建了一對線程池隊列等,粗略數了一下有十多個,但是這些線程池也是,實現他的消息隊列的核心邏輯。

 

 

上圖中,兩個邏輯是比較最重要的,第一個是messageStore,是message的存儲的服務,處理了commitlog和consumeIndex的存儲,但是這兩個是放在不同的線程裏面做的,producer發送消息過來會直接先寫進commitlog日誌裏,在broker 啓動的時候會創建一條單一線程,專門從commitlog裏面獲取沒有被消費的消息,然後構造ConsumeQueue實例,創建consumeQueue索引文件,用來給消費者消費。

然後後面的remotingservice和fastRemotingServer其實都是啓動netty的客戶端和服務端,用於和namesrv、producer和consumer消息的收發。

接收producer消息

接收消息發送消息這些,其實就是走的標準的netty的Reactor的模型,這個官方文檔上有,下面也會簡單說下,可以根據nettyserver的創建的,找到處理的handler。

 

 

 

一路找下來,在直接看請求處理的的case。請求處理的過程中,首先其實就是調用對應的處理器來處理,這裏用的是AsyncnettyRequestProcessor來處理的,其實實際處理的實例類是SendMessageProcessor中的processRequest方法。

 

 

 

 

 

這裏後續的處理流程就是校驗請求頭,請求類型等,組裝存儲的數據結構,最終會調用我們之前啓動的時候創建的messageStore這個類中去存儲。

 

 

存儲的過程中,其實只存儲了commitlog這個日誌,沒有去構建消費隊列,也沒有構建消費者隊列的的索引文件,寫入commitlog後就會直接返回給producer,表示成功。

 

 

consumeQueue構建

看開發者指南的時候,有的地方一帶而過,導致我看源碼的時候一直沒想明白這個consumeQueue到底是什麼時候創建的,導致思維有個斷層,後來又仔細看了下文檔,才知道他其實是在broker啓動的時候start了一條線程,專門根據commitlog的內容把消息分發到不同的隊列中,因爲在我的想法裏,可能我直接扔到線程池裏異步構建consumeQueue也是可以的,雖然還是需要一條這樣的線程進行掉電之後的補償,可能我太菜了吧。

在broker的start()方法裏面,啓動的messageStore,在messageStore裏面,又啓動了一個單線程處理器。

 

 

這個start方法裏面其實是個死循環,只要服務不停,就不聽的從commitlog裏面讀取消息分發,具體的實現在doReput()

 

通過DefaultMessageStor的doDispatch方法來構建隊列和分發消息。DefaultMessageStore在初始化時會維護一個dispatcherList,默認加載兩個類;CommitLogDispatcherBuildConsumeQueue用於構建ConsumerQueue、CommitLogDispatcherBuildIndex用於構建Index索引。

 

dispatch()會調用putMessagePositionInfo()方法,putMessagePositionInfo方法首先在consumeQueueTable中找到對應TOPIC和queueID的ConsumerQueue然後調用ConsumerQueue的putMessagePositionInfoWrapper方法構建ConsumerQueue。

 

 

 

 

ConsumeQueue的putMessagePositionInfoWrapper完成ConsumerQueue的構建。

 

 

putMessagePositionInfo構建consumerQueue

 

 

到這裏就說明了consumeQueue是怎麼構建起來的了。

consumer

在Consumer啓動後,它就會通過定時任務不斷地向RocketMQ集羣中的所有Broker實例發送心跳包(其中包含了,消息消費分組名稱、訂閱關係集合、消息通信模式和客戶端id的值等信息)。Broker端在收到Consumer的心跳消息後,會將它維護在ConsumerManager的本地緩存變量—consumerTable,同時並將封裝後的客戶端網絡通道信息保存在本地緩存變量—channelInfoTable中,爲之後做Consumer端的負載均衡提供可以依據的元數據信息。

consumer的負載均衡,或者說他的併發就是根據consumeQueue來實現的,先來說下爲什麼需要consumeQueue的這個東西,在rocketmq的消息模型中,其實也使用了“請求-確認”的機制,確定消息不再傳遞的過程中由於網絡或者服務器故障,或者消費失敗導致丟失,沒有推送,所以在消費端,消費者在收到消息運行消費邏輯(比如,將數據保存到數據庫)後,會返回給broker一個確認的ACK,broker纔會認定這條消息消費成功,但是這個給消費端帶來了問題,因爲要保證消息的有序性,在一條消息被消費成功之前是不會繼續下一條消息的消費的(其實如果真的消費不掉,broker會把這條消息放到一個單獨的死信隊列中,進行後續處理),否則會出現消息空洞,不能保證有序性,比如一個訂單產生了三條消息,訂單創建,訂單付款,訂單完成,這個順序是不能亂的,順序消費纔有意義,所以在這個例子下面需要保證消息消費的的順序,rocketmq的做法就是把這三條消息都放到一個隊列裏面,隊頭的消息沒有消費成功,就不消費下一個,但是不相關的兩筆訂單的消息又想同時消費怎麼做,那就是在添加一個隊列,增加消費者的數量這樣就可以實現併發擴容。

 

 

所以Rocketmq的實現方式是這樣的,對於需要有序的消息,會放到同一個消費隊列裏,在rocketmq的設計裏,同一個topic下面會有多個消費組,消費組的概念就是topic下面的消息會投放到所有的消費組中,每個消費組都消費主題中一份完整的消息,不同消費組之間消費進度彼此不受影響,也就是說,一條消息被 Consumer Group1 消費過,也會再給 Consumer Group2 消費。消費組中包含多個消費者,同一個組內的消費者是競爭消費的關係,每個消費者負責消費組內的一部分消息。如果一條消息被消費者 Consumer1 消費了,那同組的其他消費者就不會再收到這條消息。

一上面的訂單爲例,producer發送消息的時候,會經過一個負載均衡的算法,這個是允許客戶自定義的。


// RocketMQ通過MessageQueueSelector中實現的算法來確定消息發送到哪一個隊列上
// RocketMQ默認提供了兩種MessageQueueSelector實現:隨機/Hash
// 當然你可以根據業務實現自己的MessageQueueSelector來決定消息按照何種策略發送到消息隊列中
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        Integer id = (Integer) arg;
        int index = id % mqs.size();
        return mqs.get(index);
    }
}, orderId);

這樣我們可以決定,同一個訂單號可以通過自定義的取模分配到同一個隊列上,每個消費者同時管理整個消費組中的多個隊列,訂單的消息在隊列中是有序的,在整個消費組或者topic下是無序的,這樣既保證了順序執行,又保證了提高併發度不造成消息空洞。

其實我個人認爲對於亂序消費可以容忍消息空洞,因爲我開發過類似的東西,無非就是你消費失敗的時候可以進行補償重新推送,寫個定時器去定時看那些消息沒有發送,再次發送一下就行了,同時還可以使用線程池進行併發發送提高性能。可能rocketmq作爲一個產品化的系統來說,需要考慮多種情況,才使用了這種兼容嚴格順序消費和普通順序消費(其實如果是默認的隨機投遞隊列的情況就是亂序消費)的模式。

上面扯了一大堆其實是解決了爲什麼mq的消費模型是以消費組,消費隊列,消費者構成的,下面說下代碼裏是具體怎麼把隊列分配給消費者的。


作者:Elijah同學
鏈接:https://juejin.im/post/5eeb12226fb9a058897dbf03

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章