RocketMQ組件及原理深度剖析詳解

 

 RocketMQ於2017年9月成爲Apache基金會的頂級項目。有着支撐億級消息量的能力,可以爲複雜的業務場景提供系統解耦、削峯填谷、以及低延遲、高吞吐的能力,下面將詳細介紹RockeMQ的核心組件和功能,以及細節。

 

RocketMQ應用場景及作用? 

    應用解耦:用戶調用訂單系統創建訂單後,分別調用庫存系統、支付系統、物流系統,使用解耦之後,支付系統、庫存系統、物流系統分別從消息隊列中去消費。

    流量消峯:平時的qps爲1千左右,在某個秒殺時刻達到了1萬,其實沒必要花大資金升級系統,只需要將消息緩存在消息隊列中即可,起到緩衝左右,避免請求全部一次性到訂單系統。

    消息分發:團隊之間數據的推送。

 

RocketMQ中的角色及作用?

    Producer:發送消息,Comsumer:消費消息,Broker:存儲、傳輸消息,NameServer:協調各個地方

    爲了消除單點故障,增加可靠性和吞吐量,可以在多臺機器上部署多個NameServer和Broker,爲每一個Broker部署一個或多個Slave。

    啓動的順序:先啓動NameServer,再啓動Broker,這時候消息隊列就可以提供服務了。

    Broker:負責接收Producer發過來的消息、處理Consumer的消費消息請求、消息的持久化存儲、消息的HA機制以及服務端過濾功能等。

    NameServer功能:

    1)集羣的各個組件通過它瞭解全局信息,各個角色機器定期向NameServer上報自己的狀態,超時不上報的話,NameServer會認爲某個機器出故障不可用了,其他的組件會把這個機器從可用列表中移除。

    2)NameServer可以部署多個,相互之間獨立,其他角色同時向多個NameServer機器上報狀態信息,從而達到熱備份的目的。NameServer本身是無狀態的,也就是NameServer中的Broker、Topic等狀態信息不會持久存儲,都是各個角色定時上報並存儲到內存中的。

    集羣狀態的存儲結構:主要有下面5個變量,源碼如下

public class RouteInfoManager {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    // key爲Topic名稱,存儲了所有Topic的屬性信息
    // value是QueueData隊列,隊列長度爲這個Topic數據存儲的Master Broker的個數
    // QueueData裏存儲着Broker的名稱、讀寫queue的數量、同步標識等
    private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;

    // 以BrokerName爲索引,相同的名稱的Broker可能存在多臺機器,一個Master和多個Slave
    // BrokerName對應的屬性信息包括Cluster名稱,一個Msater Broker和多個Slave Broker的地址信息
    private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;

    // 存儲集羣中Cluster的信息,就是一個Cluster名稱對應一個BrokerName組成的集合
    private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;

    // 這個結構和brokerAddrTable有關,key是BrokerAddr,也就是對應着一臺機器
    // value是這臺Broker機器的實時狀態,包括上次更新狀態的時間戳,NameServer會定期檢查這個時間戳
    // 超時沒有更新就認爲這個Broker無效了,並將其從Broker列表移除
    // 每10秒檢查一次、時間戳超過2分鐘則認爲Broker已失效
    private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;

    // Filter Server是過濾服務器,是一種服務端過濾方式,一個Broker可以有一個或多個Filter Server
    // key爲Broker的地址,value是和這個Broker關聯的多個Filter Server的地址
    private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

    // 通過構造函數初始化
    public RouteInfoManager() {
        this.topicQueueTable = new HashMap<String, List<QueueData>>(1024);
        this.brokerAddrTable = new HashMap<String, BrokerData>(128);
        this.clusterAddrTable = new HashMap<String, Set<String>>(32);
        this.brokerLiveTable = new HashMap<String, BrokerLiveInfo>(256);
        this.filterServerTable = new HashMap<String, List<String>>(256);
    }
    ...
}

 

創建Topic的流程是怎樣?    

    1)通過創建Topic的命令,指定在哪個Broker上創建Topic的Message Queue

    2)創建Topic命令被髮往對應的Broker,Broker接到創建Topic的請求後,執行具體的創建邏輯

    3)創建的最後一步會向NameServer發送註冊信息,NameServer完成創建Topic的邏輯後,其他客戶端才能發現新增的Topic,邏輯在RouteInfoManager.registerBroker()函數裏。

    registerBroker()主要邏輯:首先更新Broker信息,然後對每個Master角色的Broker,創建一個QueueData對象,如果是新建Topic,就是添加QueueData對象;如果是修改Topic,就是把舊的QueueData刪除,加入新的QueueData。

 

爲什麼不用Zookeeper做集羣的管理?

    因爲Zookerper太重,包括master選舉,而RocketMQ不需要這些複雜的功能,而且也可以避免引入另一箇中間件,增加維護成本。

 

RocketMQ的消費者類型?

    1)DefaultMQPushConsumer:系統收到消息後自動調用處理函數處理消息,自動保存Offset,而且加入新的DefaultMQPushConsumer後自動做負載均衡。默認採用循環的方式逐個讀取一個Topic的所有MessageQueue。

    DefaultMQPushConsumer需要設置三個參數:Consumer的GroupName、NameServer的地址和端口號、Topic的名稱(不需要topic下的所有消息時,可通過指定消息的tag標籤用於過濾)

    GroupName:用於把多個Consumer組織到一起,提高併發處理能力,它需要和消息模式配合使用

    Rocket支持2種消息模式:Clustering和Broadcasting

    Clustering模式下:同一個ConsumerGroup裏的每個Consumer只消費所訂閱消息的一部分內容,同一個ConsumerGroup裏所有的Consumer消費的內容合起來纔是所訂閱Topic內容的整體,從而達到負載均衡的目的。

    Broadcasting模式下:同一個ConsumerGroup裏的每個Consumer都能消費所訂閱Topic的全部消息,也就是一個消息會被多個消費者消費。使用存儲Offset的類是LocalFileOffsetStrore。

    特點:以長輪詢的方式通過Client端和Server端的配合,達到既有pull的優點又有push的實時性。

    通過設置Broker的最長阻塞時間(默認15秒),當服務端收到新消息請求後,如果隊列沒有新消息,則進入循環不斷查看狀態,每次waitForRunning一段時間(默認5秒),然後再check,超時後就返回空結果。在這個期間如果收到了新的消息,會直接調用notifyMessageArriving函數返回請求結果。

    缺點:這種長輪詢的方式會佔用資源,適合用在消息隊列這種客戶端連接數可控的場景。

    流量控制:PushConsumer有個線程池,消息處理邏輯在各個線程裏同時執行。

    那麼客戶端如何得知當前消息堆積數量?如何重複處理某些消息?如何延遲處理某些消息呢?

    通過定義了一個快照類ProcessQueue,在運行的時候每個Message Queue都會有個對應的ProcessQueue對象,保存了這個Message Queue消息處理狀態的快照。

    ProcessQueue對象裏主要是一個TreeMap和一個讀寫鎖。TreeMap中以Message Queue的Offset作爲key,消息內容的引用爲value,保存了所有從Message Queue獲取到,但還未被處理的消息。讀寫鎖控制着每個線程對TreeMap對象的併發訪問。

    PushConsumer在每次pull請求前會做三個判斷來控制流量:未處理的消息數、消息總大小、Offset速度,任何一個超過都會隔一段時間再拉去消息。

    2)DefaultMQPullConsumer:由使用者自主控制讀取操作

    主要處理以下三件事情:

    獲取某個Message Queue並遍歷下面每條消息:一個Topic包括多個Message Queue,則需要遍歷多個Message Queue。

    維護Offsetstore:從一個message Queue里拉取消息的時候,要傳入Offset參數(long類型),隨着不斷的讀取,Offset會不斷增長。這個時候由用戶負責把Offset存儲下來,內存或者磁盤。

    根據不同的消息狀態做不同處理:如FOUNT、NO_NEW_MSG,分別表示獲取到消息和沒有新消息。

 

Consumer的啓動、關閉流程?

    PullConsumer關閉:主動權更高,可以根據實際情況,暫停、停止、啓動消費過程,需要注意的是Offset的保存,要在程序的異常處理部分增加把Offset寫入磁盤方面的處理。

    PushConsumer關閉:要調用shutdown()函數、以便釋放資源、保存Offset等。這個調用要加到Consumer所在應用的退出邏輯中。

    PushConsumer啓動:會在啓動時做各種配置檢查,然後連接NameServer獲取Topic信息,如果遇到無法連接NameServer的異常,依然會正常啓動,但是不會收到消息。 這樣是爲了保證集羣在多個NameServer時,某一個連接異常時不立刻退出,而是不斷重新連接,保證整體服務依然可用。

 

生產者類型以及發送消息過程?

    DefaultMQProducer:RocketMQ默認使用的類,在發送消息時,需要經歷以下五個步驟:

    1)設置Producer的GroupName

    2)設置InstanceName,不設置的話默認爲"Default",當一個JVM啓動多個Producer的時候通過InstanceName來區分。

    3)設置發送失敗重試次數,保證消息不丟

    4)設置NameServer地址

    5)組裝消息併發送

消息的發送方式:

    通過Broker配置文件裏的flushDiskType參數設置:SYNC_FLUSH、ASYNC_FLUSH

    同步刷盤:在返回寫狀態成功時,消息已經被寫入磁盤。具體流程是,消息寫入內存的PAGECACHE後,立刻通知刷盤線程刷盤,然後等待刷盤完成,刷盤線程執行完成後喚醒等待的線程,返回消息寫成功的狀態。

    異步刷盤:在返回寫狀態成功時,消息可能只是被寫入了內存的PAGECACHE,寫操作的返回快,吞吐量大,當內存裏的消息量積累到一定程度時,統一觸發寫磁盤動作,快速寫入。

消息發送的返回狀態、發送的方式:

    FLUSH_DISK_TIMEOUT:沒有在規定時間內完成刷盤(需要Broker的刷盤策略爲SYNC_FLUSH纔會報這個錯誤)

    FLUSH_SLAVE_TIMEOUT:表示在主備方式下,並且Broker被設置爲SYNC_MASTER方式,沒有在設定時間內完成主從同步

    SLAVE_NOT_AVAILABLE:產生的場景和FLUSH_SLAVE_TIMEOUT類似,沒有找到被配置成Slave的Broker

    SEND_OK:發送成功,上面三種失敗都沒發生就是成功

    發送延遲消息:通過調用setDelayTimeLevel(int level)

    自定義消息發送規則:默認會輪流向各個Message Queue發送消息。Consumer消費的時候回根據負載均衡策略,消費被分配到的Message Queue。如果不經過特定設置,消息發往哪個隊列,被誰消費都是未知的。通過MessageQueueSelector對象作爲參數可以指定發到哪個Message Queue。

 

RocketMQ對事務的支持?

    客戶端有三個類來支持用戶實現事務消息:

    1)LocalTransactionExecutor:根據情況返回LocalTransactionState.ROLLBACK_MESSAGE或者LocalTransactionState.COMMIT_MESSAGE狀態

    2)TransactionMQProducer:用法和DefaultMQProducer類似,要通過它啓動一個Producer,但是比DefaultMQProducer多設置本地事務處理函數和回查請求狀態函數

    3)TransactionCheckListener:實現MQ服務器的回查請求,返回LocalTransactionState.ROLLBACK_MESSAGE或LocalTransactionState.COMMIT_MESSAGE

 

如何存儲Offset和調整Offset?

    Offset是指一條消息在某個消息隊列中的位置。

    Offset的類結構:主要分爲本地文件類型(LocalFileOffsetStore)和Broker代存類型(RemoteBrokerOffsetStore)兩種。

    OffsetStore使用Json格式存儲。

RocketMQ底層通信機制?

    相關的代碼在Remoting模塊裏,最上層是RemotingServer接口,定義了三個方法:

void start();
void shutdown();
void registerRPCHook(RPCHook rpcHook);

    RemotingClient接口和RemotingServer接口繼承RemotingService接口,並增加了自己特有的方法。

    NettyRemotingClient和NettyRemotingServer分別實現了RemotingClient和RemotingServer,而且都繼承了NettyRemotingAbstract類,這兩個類是基於netty實現的。

    RocketMQ各個模塊間的通信,通過發送同一格式的自定義消息(RemotingCommand)來完成,大部分邏輯都是通過發送、接受並處理Command來完成的。

 

消息的存儲和發送機制?

    一臺服務器把本地磁盤文件的內容發送到客戶端,一般分爲兩個步驟:

    1)read(file, tmp_buf, len):讀取本地文件內容

    2)write(socket, tmp_buf, len):將讀取的內容通過網絡發送出去

    tmp_buf是預先申請的內存。這兩個操作實際上進行了4次數據複製:從磁盤複製到內核態內存、從內核態內存複製到用戶態內存(完成read)、從用戶態內存複製到網絡驅動的內核態內存、從網絡驅動的內核態內存複製到網卡中進行傳輸(完成write)    

    通過mmap的方式,省去向用戶態的內存複製,提高速度。在Java中是通過MappedByteBuffer實現的,RocketMQ充分利用上述特性,也就是所謂"零拷貝"技術,提高消息存盤和網絡發送的速度。

 

消息的存儲結構?

    RocketMQ消息的存儲是由ConsumeQueue和CommitLog配合完成的。

    CommitLog:消息真正的物理存儲文件,以物理文件的方式存放,每臺Broker上的CommitLog被本機器所有ConsumerQueue共享。在CommitLog中,一個消息的存儲長度是不固定的,RocketMQ採取一些機制,儘量向CommitLog中順序寫,但是隨機讀。ConsumerQueue的內容也會被寫到磁盤裏作持久存儲。

    ConsumeQueue:消息的邏輯隊列,類似數據庫的索引文件,存儲的是指向物理存儲的地址,每個Topic下的每個Message Queue都有一個對應的ConsumeQueue文件。文件地址是:

${${storeRoot}\consumequeue\${topicName}\${queueId}\${fileName}}

    存儲機制這樣設計的好處:

    1)CommitLog順序寫,可以大大提高寫入效率。(順序寫比隨機寫性能高6000倍) 

    2)雖然是隨機讀,但是利用操作系統的pagecache機制,可以批量地從磁盤讀取,作爲cache存到內存中,加速後續的讀取速度。

    3)爲了保證CommitLog和ConsumeQueue的一致性,CommitLog裏存儲了Consume Queues、Message Key、Tag等所有信息,即使ConsumeQueue丟失也可以通過CommitLog恢復。

 

高可用機制?

    通過Master和Slave的配合達到高可用性的。

    Master和Slave區別:

    1)在Broker的配置文件中,參數brokerId的值爲0表示Master,大於0表示Slave。

    2)Master支持讀和寫,Slave僅支持讀,也就是Producer只能和Master角色的Broker連接寫入消息

    在Consumer的配置文件中,不需要設置從Master讀還是Slave讀,當Master不可用或者繁忙時,Consumer會被自動切換到從Slave讀,這樣就達到了消費端的高可用性。

    在創建Topic的時候,把Topic的多個Message Queue創建在多個Broker組上,這樣當一個Broker組的Master不可用後,其他組的Master仍然可用,Producer仍然可以發送消息。RocketMQ目前還不支持Slave自動轉成Master,如果需要,則要手動停止Slave角色的Broker,更改配置文件,用新的配置文件啓動Broker。

 

RocketMQ的順序消息實現?

    RocketMQ在默認情況下不保證順序,比如創建一個Topic,默認八個寫隊列,八個讀隊列。這時候一條消息可能被寫入任意一個隊列裏;在數據的讀取過程中,可能有多個Consumer,每個Consumer也可能啓動多個線程並行處理,所以消息被哪個Consumer消費,被消費的順序和寫入的順序是否一致是不確定的。

    保證全局順序消息:需要先把Topic的讀寫隊列數設置爲一,然後Producer和Consumer的併發設置也要是一。

    部分順序消息:在發送端,把同一業務ID的消息發送到同一個Message Queue;在消費過程中,要做到從同一個Message Queue讀取的消息不被併發處理。

    發送端:通過MessageQueueSelector類來控制把消息發往哪個Message Queue。

    消費端:通過MessageListenerOrderly類來解決但Message Queue的消息被併發處理的問題。

    MessageListenerOrderly實現原理:不僅通過參數限制一次從Broker的一個消息隊列獲取消息的最大數量,而且在具體實現中,爲每個Consumer Queue加個鎖,消費每個消息前,必須先獲取對應Consumer Queue的鎖,保證同一時間,同一個Consumer Queue的消息不被併發消費,但不同的Consumer Queue的消息可以併發處理。

 

 如何動態增減機器?

   動態增減NameServer,優先級從高到底依次如下:

    1)代碼設置setNamesrvAddr()

    2)Java啓動參數設置,rocketmq.namesrv.addr

    3)Linux的環節變量設置

    4)HTTP服務來設置,請求指定的URL地址(唯一支持動態增減NameServer的方式,無需重啓),使用後其他組件會每隔2分鐘請求一次該URL,獲取最新的NameServer地址

    動態增減Broker:

    增減Broker後,一是可以把新建的Topic指定到新的broker機器上,均衡利用資源。另一種是通過updateTopic命令更改現有的Topic配置,在新加的Broker上創建新的隊列。

    當Topic只有一個Master Broker時:停掉Broker後,發送消息會受到影響

    當Topic有多個Master Broker時:如果使用同步方式發送,則會重試,自動向另一個Broker發消息,不會受影響。如果使用異步方式發送,則會丟失切換過程中的消息,因爲異步方式下,發送失敗不會重試。

 

PS:很久沒來發博客了,由於工作問題,沒時間整理,所以都整理在自己的雲筆記中,現在準備重新撿來,把之前的總結,學習,分享出來,然後上週建立了自己的微信公衆號,每週更新技術分享,感興趣的小夥伴可以關注一下,另外可以加我討論學習,進入我的微信技術羣討論,分享或者內推。。。

微信公衆號:

個人微信:T_Stone11

 

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