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