一直在用rocketmq,對他的功能和大概流程略知一些,但是比較浮,經不起稍微的推敲。是時候進一步瞭解下這個NB的中間件了。
這裏不再贅述它的那些特性,網上一大堆,這裏主要按照自己想了解的一些方面作整理,貼出部分核心代碼,意圖通過表現各個角色間的交互,勾畫大致架構,方便以後對每個要點各個深入。如果想要引導來閱讀源代碼,推薦rmq源碼系列,寫的很有誠意,本筆記中也部分參考引用其文章。
部署結構(邏輯結構/物理結構)
消息中間件的整體看起來像我們相互郵寄明信片。李雷(Producer)通過郵局公告(Namesrv)找到郵局的地址,然後去郵局(Broker)把明信片(Message)發送給韓梅梅(Consumer)。
物理結構
-
Producer 消息生產者,負責產生消息,一般由業務系統負責產生消息。
-
Consumer 消息消費者,負責消費消息,一般是後臺系統負責異步消費。
-
Broker 消息中轉角色,負責存儲消息,轉發消息,一般也稱爲 Server。
-
Namesrv 註冊中心,管理協調角色,維護其他角色信息。
邏輯結構
邏輯結構裏面把所有角色都集羣化了,集羣化後就要牽扯到集羣消費。於是,基於以上的角色,又衍生出其他的概念。
-
Producer Group 一類 Producer 的集合名稱,這類 Producer 通常發送一類消息,且發送邏輯一致。
-
Consumer Group 一類 Consumer 的集合名稱,這類 Consumer 通常消費一類消息,且消費邏輯一致。
角色實現及其交互
按照邏輯/物理部署結構,各個角色間怎麼通信?怎麼保活?數據怎麼備份?
rmq自己對Netty進行了包裝(Facade),方便各個角色使用,在工程的remoting子工程中。
namesrv(Name Server)與broker
namesrv與broker是實現後續一切的基礎,producer和consumer的信息傳遞需要broker來中轉,那麼找到或者說確定一個broker便是第一步了。
namesrv(Name Server)
namesrv是很輕量的服務,簡單、可集羣橫向擴展、無狀態。他是整個mq的管家(像是Dubbo的服務發現),管理其他角色,如果沒有它,整個mq將難以開展工作。
簡單看下namesrv啓動時的行爲。它的核心代碼在工程的namesrv包下,代碼量很少。
namesrv的故事開始於NamesrvStartup
的main方法,它主要來解析配置參數,根據配置裝填並初始化和啓動NamesrvController
。NamesrvController
貫連整個Namesrv的業務功能,通過Netty向外暴露服務,主要負責:
-
管理Topic和Broker的註冊信息
-
管理KV的配置(持久化)
// NamesrvController
public boolean initialize() {
// 管理KV的配置(並持久化)
this.kvConfigManager.load();
// 暴露服務
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
// netty執行線程池
this.remotingExecutor =
Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
// 註冊服務處理器
this.registerProcessor();
// kv信息打印及broker存活檢測任務
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.kvConfigManager.printAllPeriodically();
}
}, 1, 10, TimeUnit.MINUTES);
// 省略TSL配置部分
}
RouteInfoManager
相當於是管家的賬本,裏面保存着關於topic、broker的具體信息,這些信息全部保存在內存中。其維護的信息如下。
//RouteInfoManager
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
/**
* BrokerData {
* private String cluster;
* private String brokerName;
* private HashMap<Long/* brokerId */, String/* broker address */> brokerAddrs;
* }
**/
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
/**
* BrokerLiveInfo {
* private long lastUpdateTimestamp;
* private DataVersion dataVersion;
* private Channel channel;
* private String haServerAddr;
}
**/
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
那麼,這些信息從哪裏來呢?且慢,我們先看下broker。
broker
broker就是例子中郵局,郵局要具備什麼能力呢?
- 接收信件
- 保存信件,保證除不可抗因素外,信件不丟失
- 投遞信件
- 高效工作,信件快速接受及送達
- 信件投遞失敗的處理
- 信件投遞可回溯追蹤(當然,郵局不能看內容了)
- 向管家(namesrv)彙報自己的運營狀態
以上這些其實也就是broker要做的事情,只不過它不存在隱私一說罷了。當然他還需要有其他的特性。
- 支持消息的不同投遞模型:Publish/Subscribe,集羣分組消費等
- 高可用,無單點
- 消息有序
- 消息過濾
- 分佈式事務
broker的主要源碼放在broker目錄下,broker(其他模塊)的啓動模型與namesrv一致,BrokerStartup
解析配置,並根據配置裝備BrokerController
,然後初始化BrokerController
並啓動(start)。進行初始化時的主要啓動一堆定時任務、存儲(MessageStore)和Netty配置(註冊Processor)。
// BrokerController
public boolean initialize() throws CloneNotSupportedException {
boolean result = this.topicConfigManager.load();
// 消費者消費信息
result = result && this.consumerOffsetManager.load();
// 訂閱組
result = result && this.subscriptionGroupManager.load();
// 過濾器
result = result && this.consumerFilterManager.load();
// 省略messageStore相關處理
result = result && this.messageStore.load();
if (result) {
// 暴露服務,啓動兩個Server,小端口和VIPChannel有關
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.clientHousekeepingService);
NettyServerConfig fastConfig = (NettyServerConfig) this.nettyServerConfig.clone();
fastConfig.setListenPort(nettyServerConfig.getListenPort() - 2);
this.fastRemotingServer = new NettyRemotingServer(fastConfig, this.clientHousekeepingService);
// 省略創建各Processor的Executor
// 註冊Processor
this.registerProcessor();
// 持久化消費信息
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
BrokerController.this.consumerOffsetManager.persist();
}, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS);
// 持久化過濾器信息
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
BrokerController.this.consumerFilterManager.persist();
}, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS);
// 設置namesrv地址,如果沒有設置,則從一個web服務fetch
if (this.brokerConfig.getNamesrvAddr() != null) {
this.brokerOuterAPI.updateNameServerAddressList(this.brokerConfig.getNamesrvAddr());
} else if (this.brokerConfig.isFetchNamesrvAddrByAddressServer()) {
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
BrokerController.this.brokerOuterAPI.fetchNameServerAddr();
}, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
}
if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
// 同步topic、consumerOffset、訂閱組等信息到Salve
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
BrokerController.this.slaveSynchronize.syncAll();
}, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS);
} else {
// 如果是Master則定時打印與Salve的差異
}
// 省略TSL設置及事物初始化
}
}
public void start() throws Exception {
// 省略 啓動初始化的那些服務
// 向所有namesrv註冊自己
this.registerBrokerAll(true, false, true);
// 定時向所有namesrv註冊自己
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
}, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
}
public synchronized void registerBrokerAll(final boolean checkOrderConfig, boolean oneway, boolean forceRegister) {
// 封裝broker上的topic信息
TopicConfigSerializeWrapper topicConfigWrapper = this.getTopicConfigManager().buildTopicConfigSerializeWrapper();
// 調用API向namesrv註冊
doRegisterBrokerAll(checkOrderConfig, oneway, topicConfigWrapper);
}
此時,我們可以看到的交互,每個broker啓動時會先對namesrv尋址,然後把自己的信息註冊到所有的namesrv上,並定時維護(心跳),namesrv維護的關於broker的信息來源於此,爲了應對不同的場景需要,namesrv把數據封裝成不同的結構,方面維護。namesrv關於請求的處理在DefaultRequestProcessor
中,透過其processRequest方法我們可以明確其要承擔的全部責任,比如:broker註冊/註銷,增刪改查topic路由,KV信息維護等。broker註冊的信息包括但是不限於以下內容。
{
"topicConfigSerializeWrapper": {
"topicConfigTable":{
"topic_?":{
"defaultReadQueueNums":"16",
"defaultWriteQueueNums":"16",
"topicName":"",
"readQueueNums":"",
"writeQueueNums":"",
"perm":"",
"topicFilterType":"",
"topicSysFlag":"",
"order":""
},
},
"dataVersion":{
"timestamp":"xxxx",
"counter":"xxxx"
}
},
"filterServerList":[
"",//filterServerAddr
],
"clusterName":"",
"brokerAddr":"",
"brokerName":"",
"brokerId":""
}
namesrv與producer
公告(namesrv)維護好了,李雷(producer)就可以通過它來獲取郵局(broker)的信息了。
先來看下producer的啓動時主要做了些什麼事情,producer的主要代碼放在工程的client子工程下的producer包中。拋開事務不談,我們的入口在DefaultMQProducer
的start方法上。
下面列出啓動時我們關心的業務,啓動流程時序圖,網上也不少了。
// DefaultMQProducerImpl
public void start(final boolean startFactory) throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
// groupName合法性校驗
this.checkConfig();
// MixAll.CLIENT_INNER_PRODUCER_GROUP 用來發送消費者消費失敗的消息到重試隊列
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
// 如果不是內部構建的
this.defaultMQProducer.changeInstanceNameToPID();
}
// MQClientManager 以 clientId(ip@pid) 維度保證 MQClientInstance 的單例
this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);
// 向 MQClientInstance 註冊自己
boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
if (!registerOK) {
this.serviceState = ServiceState.CREATE_JUST;
throw new MQClientException("...");
}
// Just for testing or demo program
this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());
if (startFactory) {
// 啓動MQClientInstance
mQClientFactory.start();
}
this.serviceState = ServiceState.RUNNING;
break;
case RUNNING:
case START_FAILED:
case SHUTDOWN_ALREADY:
throw new MQClientException("...");
default:
break;
}
// 向broker發送心跳&&上傳filter信息
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
}
這裏我們需要說下rmq中client(Producer、Consumer和Admin)的基本結構,不同角色的client通過Composite一個MQClientInstance
的方式封裝各自不同的邏輯,所以他們的啓動流程大概一致,Consumer的複雜一些,但主流程無差別:
- 通過clientId獲取
MQClientInstance
實例 - 自己角色特有的邏輯
- 向
MQClientInstance
註冊自己 - 啓動
MQClientInstance
,如果MQClientInstance
已啓動則返回 - 通過
MQClientInstance
方法同步更新自己角色需要的信息
按照以上總結我們着重看下MQClientInstance
的start做了些什麼。
// MQClientInstance
public void start() throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
// If not specified,looking address from name server
if (null == this.clientConfig.getNamesrvAddr()) {
this.mQClientAPIImpl.fetchNameServerAddr();
}
// Start request-response channel
// 啓動remotingClient,開啓與外部交互的通道
this.mQClientAPIImpl.start();
// Start various schedule tasks
this.startScheduledTask();
// Start pull service
// push模式下拉消息服務
this.pullMessageService.start();
// Start rebalance service
// 開啓負載均衡消費隊列服務
this.rebalanceService.start();
// Start push service
// 上個代碼片段提到的 CLIENT_INNER_PRODUCER_GROUP 對應的Producer
this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
this.serviceState = ServiceState.RUNNING;
break;
case START_FAILED:
throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
case RUNNING:
case SHUTDOWN_ALREADY:
default:
break;
}
}
private void startScheduledTask() {
// 如果沒有配置namesrv的地址,默認從web服務定時抓取
if (null == this.clientConfig.getNamesrvAddr()) {
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
}, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
}
// 從namesrv(NamesrvController)獲取topic的路由信息
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
MQClientInstance.this.updateTopicRouteInfoFromNameServer();
}, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);
// 和broker的交互
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
// 清理已離線的Broker
MQClientInstance.this.cleanOfflineBroker();
// 向所有Broker發送心跳,心跳中帶有consumer相關信息:clientId,Subscription等
MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
}, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);
// 持久化消費偏移量(持久化到遠程或者本地)
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
MQClientInstance.this.persistAllConsumerOffset();
}, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);
// 調整push方式下的拉取線程數
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
MQClientInstance.this.adjustThreadPool();
}, 1, 1, TimeUnit.MINUTES);
}
MQClientInstance
啓動時會啓動一個從namesrv定時蒐集本地所有訂閱(consumer)的和發送(producer)的topic,然後依次通過mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3)
來進行更新topic的路由信息。
問題:負載均衡怎麼做的?
namesrv與consumer
正如上文說的那樣,Producer和Consumer都組合了MQClientInstance
,而MQClientInstance
來完成其和namesrv的交互,所以它的過程和producer是一致的,他們需要維護的信息也是一致的。
producer與broker
producer與broker的交互有以下幾個關注點:producer支持發送消息的方式?發送失敗producer怎麼處理?broker怎麼處理producer的消息?
消息發送
producer發送方式有SYNC、ASYNC、ONEWAY,這些方式定義在CommunicationMode
中,但是這塊從DefaultMQProducer
定義的send方法命名中沒有明確區分同步和異步,在我當前的版本中ASYNC的實現存在問題(It will be removed at 4.4.0 cause for exception handling and the wrong Semantics of timeout.)。
我們以最簡單的入口(DefaultMQProducer#send(Message)
)大致看下producer的發送流程。
// DefaultMQProducerImpl
private SendResult sendDefaultImpl(
Message msg,
final CommunicationMode communicationMode,
final SendCallback sendCallback,
final long timeout
) {
// 省略合法性檢查
final long invokeID = random.nextLong();
long beginTimestampFirst = System.currentTimeMillis();
long beginTimestampPrev = beginTimestampFirst;
// 選擇發送topic信息,此步驟數據由·MQClientInstance·中啓動的定時任務維護
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
boolean callTimeout = false;
MessageQueue mq = null;
Exception exception = null;
SendResult sendResult = null;
// 同步模式下,總髮送次數確定,默認失敗重試2次,也就是共嘗試發送3次
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
for (; times < timesTotal; times++) {
String lastBrokerName = null == mq ? null : mq.getBrokerName();
// 選擇本次發送的隊列
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
if (mqSelected != null) {
mq = mqSelected;
try {
beginTimestampPrev = System.currentTimeMillis();
long costTime = beginTimestampPrev - beginTimestampFirst;
if (timeout < costTime) {
callTimeout = true;
// 超時中斷
break;
}
// 封裝信息,並調用ClientAPI發送出去
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
endTimestamp = System.currentTimeMillis();
// 更新本次borker可用性,如果sendLatencyFaultEnable開啓
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
// 如果SYNC模式下發送失敗進行重試,ASYNC和ONEWAY模式下直接返回null
} catch (Exception e) {
// 根據不同的異常類型確認處理方式:重試、中斷、返回
// 更新本次borker可用性,如果sendLatencyFaultEnable開啓
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
}
} else {
// 沒有可用隊列不進行發送
break;
}
}
// 省略異常處理
}
// 省略異常處理
}
那麼發送消息的怎麼確認發送隊列呢?sendLatencyFaultEnable是什麼?
先通過topic找到對應的topicPublishInfo,顧名思義topicPublishInfo裏面維護了該topic相關的broker和隊列信息,在沒有開啓sendLatencyFaultEnable情況下(默認),按照遞增取模的方式選擇即將要發送的隊列,如果開啓了sendLatencyFaultEnable,再取模後還需要判斷當前隊列的broker是否可用。上面代碼中我們可以看到發送消息後,不管成功還是失敗都會更新一個叫做Item的狀態,這個Item指的就是broker,rmq根據此次消耗的時間來更新該broker不可用的時間,以此達到對不穩定broker的規避。具體代碼可看·MQFaultStrategy·。
消息接收
上面說到BrokerController
時,我們未貼出關於broker的NettyServer關於Processor的註冊部分。在這部分中,註冊了不同的Processor:
- SendMessageProcessor 處理client發送的消息
- PullMessageProcessor 處理client來拉取消息
- QueryMessageProcessor 處理對消息的查詢
- EndTransactionProcessor 處理事務消息
- …
這些Processor分別處理不同的RequestCode
。我們這次關心的是SendMessageProcessor
。
處理的流程比較簡單,這裏只列出基本流程,可從SendMessageProcessor#processRequest
看起。
- 檢查broker是否可以對外提供服務(是否到了配置的對外服務時間)
- 消息合法性校驗、broker是否可寫、消息寫入隊列是否存在等檢查
- 如果是消費失敗重試消息則處理其是否要進入死信隊列
- 轉換請求信息封裝爲
MessageExtBrokerInner
- 將
MessageExtBrokerInner
發給MessageStore
進行持久化 - 處理
MessageStore
返回的結果,並根據結果用BrokerStatsManager
做統計更新 - 封裝相應信息並返回。
consumer與broker
韓梅梅終於要收到信了。和現實一樣,要麼郵遞員送給你,要麼你自己上郵局去取信。這兩種方式對應了rmq中的兩種去消息方式:push(DefaultMQPushConsumer
)和pull(DefaultMQPullConsumer
)。這兩種方式在定義上是有區別的,但實現上實際都是pull,只是對pull的處理不同。說到這裏就不得不提長輪詢了,pull方式我們客戶端需要自己起一個線程定時去broker拉取信息,當broker沒有消息可消費時立刻返回;push方式與pull流程一致,但是broker對其請求處理不同,當broker此刻無消息可消費時,broker會hold住當前請求,直到有消息返回或者到了超時時間返回。戳這裏(–>長輪詢–<和–>長輪詢進階–<)更深入瞭解長輪詢,感謝偉大的互聯網讓信息容易共享。
此處以push模式下的MessageListenerConcurrently
消費策略來梳理下獲取消息的流程。
DefaultMQPushConsumer
相比DefaultMQProducerImpl
多啓動了幾個服務:負載均衡服務(consumer平均分配隊列)、維護offset服務、消息消費服務(consumeMessageService)。
// DefaultMQPushConsumer
public synchronized void start() throws MQClientException {
// 省略配置檢查及服務的啓動
// 更新訂閱topic信息
this.updateTopicSubscribeInfoWhenSubscriptionChanged();
// 每個topic隨機選取一個broker檢查subscription語法是否正確,請求code:RequestCode.CHECK_CLIENT_CONFIG
this.mQClientFactory.checkClientInBroker();
// 向broker發送心跳,心跳攜帶client相關信息
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
// 進行負載均衡,默認策略爲`AllocateMessageQueueAveragely`,調整完後發起第一個pullRequest
this.mQClientFactory.rebalanceImmediately();
}
consumer定時拉取服務是在MQClientInstance
中啓動的一個叫做pullMessageService(PullMessageService
)的定時服務,這個服務監聽在一個pullRequestQueue隊列上,當有拉取請求時,根據PullRequest
選取對應分組的DefaultMQPushConsumerImpl
進行拉取。
public class PullRequest {
private String consumerGroup;
private MessageQueue messageQueue;
private ProcessQueue processQueue;
private long nextOffset;
private boolean lockedFirst = false;
// ...
}
PullRequest
中有兩個Queue,messageQueue是指要從broker哪個隊列拉數據,ProcessQueue是拉數據成功後存放數據的隊列。有了processQueue便可以做一些簡單的流控,目前可以根據processQueue的消息數量或者消息的大小來決定是否停止本次拉取,並設置下次拉取的延遲而不是立即開始下次拉取。如果流控通過,consumer開始通過pullAPIWrapper向broker拉去信息,拉取成功後提交一個消費任務(默認非CONSUME_MESSAGE_DIRECTLY,如果此時線程池等待隊列已滿,任務延遲提交),消費任務回掉我們註冊的listener,消息至此投遞完成。
投遞完成後,消費的結果怎麼處理?我們剛提到ProcessQueue可以進行流控,那麼合適出隊以消費的消息?消費失敗又需要做什麼處理?這裏不在展開,可見ConsumeMessageConcurrentlyService
類中。
除了怎麼去消費消息外,對consumer而言還有另外一個需要解決的問題:怎麼給採用CLUSTERING方式的消費羣組成員平均分配隊列?答案在AllocateMessageQueueAveragely
中,該策略保證消息隊列被平均分配給同消費組內的消費者(圖片來源)。
消息存儲
數據的落盤、存儲、數據結構、目錄結構這些請戳–>broker的數據持久化<–
源代碼入口
CommitLog#putMessage(final MessageExtBrokerInner msg)
重要的一些基礎 MappedFile-Java、Java NIO、Btree
此處貼上一張關於broker數據結構及流轉的圖,更直觀來看他的數據結構流向(圖片來源)。
broker與broker(HA)
broker有這幾種角色ASYNC_MASTER、SYNC_MASTER、SLAVE,消息肯定都是寫到MASTER上的,然後同步給SLAVE。
同步的方式分爲SYNC和ASYNC兩種:
- ASYNC,消息寫完後就直接返回,後臺線程去做同步的操作;
- SYNC,等到同步完成後返回。
這兩種方式也稱作異步複製和同步雙寫。HA的整個過程只在store模塊做的,是基於JDK的nio來寫的,沒有依賴remoting模塊。
除此之外,broker啓動時,如果當前broker是SLAVE,則會啓動一個定時任務來同步topic等信息。
// SlaveSynchronize
public void syncAll() {
this.syncTopicConfig();
this.syncConsumerOffset();
this.syncDelayOffset();
this.syncSubscriptionGroupConfig();
}
結語
以上只是對整體架構和主流成的簡單認知,關於其中的細節問題,就需要我們帶着疑問去看源碼了。
問題:
怎麼處理定時消息?
消費失敗的消息怎麼處理?
鏈接是否像dubbo那樣複用?
broker的數據結構是怎樣的?
broker怎麼保證存儲高吞吐?
broker怎麼過濾消息?
“古人學問無遺力,少壯工夫老始成。紙上得來終覺淺,絕知此事要躬行。” – 陸游《冬夜讀書示子聿》
以上貼出源代碼基於incubator-rocketmq release-4.3.2版本,爲了方便均有刪減改,但不影響實際流程。