注:本系列源碼分析基於RocketMq 4.8.0,gitee倉庫鏈接:https://gitee.com/funcy/rocketmq.git.
從本文開始,我們來分析rocketMq
消息接收、分發以及投遞流程。
RocketMq
消息處理整個流程如下:
- 消息接收:消息接收是指接收
producer
的消息,處理類是SendMessageProcessor
,將消息寫入到commigLog
文件後,接收流程處理完畢; - 消息分發:
broker
處理消息分發的類是ReputMessageService
,它會啓動一個線程,不斷地將commitLong
分到到對應的consumerQueue
,這一步操作會寫兩個文件:consumerQueue
與indexFile
,寫入後,消息分發流程處理 完畢; - 消息投遞:消息投遞是指將消息發往
consumer
的流程,consumer
會發起獲取消息的請求,broker
收到請求後,調用PullMessageProcessor
類處理,從consumerQueue
文件獲取消息,返回給consumer
後,投遞流程處理完畢。
以上就是rocketMq
處理消息的流程了,接下來我們就從源碼來看相關流程的實現。
1. remotingServer
的啓動流程
在正式分析接收與投遞流程前,我們來了解下remotingServer
的啓動。
remotingServer
是一個netty服務,他開啓了一個端口用來處理producer
與consumer
的網絡請求。
remotingServer
是在BrokerController#start
中啓動的,代碼如下:
public void start() throws Exception {
// 啓動各組件
...
if (this.remotingServer != null) {
this.remotingServer.start();
}
...
}
繼續查看remotingServer
的啓動流程,進入NettyRemotingServer#start
方法:
public void start() {
...
ServerBootstrap childHandler =
this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
...
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(defaultEventExecutorGroup,
HANDSHAKE_HANDLER_NAME, handshakeHandler)
.addLast(defaultEventExecutorGroup,
encoder,
new NettyDecoder(),
new IdleStateHandler(0, 0,
nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
connectionManageHandler,
// 處理業務請求的handler
serverHandler
);
}
});
...
}
這就是一個標準的netty
服務啓動流程了,套路與nameServer
的啓動是一樣的。關於netty
的相關內容,這裏我們僅關注pipeline
上的channelHandler
,在netty
中,處理讀寫請求的操作爲一個個ChannelHandler
,remotingServer
中處理讀寫請求的ChanelHandler
爲NettyServerHandler
,代碼如下:
@ChannelHandler.Sharable
class NettyServerHandler extends SimpleChannelInboundHandler<RemotingCommand> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
processMessageReceived(ctx, msg);
}
}
這塊的操作與nameServer
對外提供的服務極相似(就是同一個類),最終調用的是NettyRemotingAbstract#processRequestCommand
方法:
public void processRequestCommand(final ChannelHandlerContext ctx, final RemotingCommand cmd) {
// 根據 code 從 processorTable 獲取 Pair
final Pair<NettyRequestProcessor, ExecutorService> matched
= this.processorTable.get(cmd.getCode());
// 找不到默認值
final Pair<NettyRequestProcessor, ExecutorService> pair =
null == matched ? this.defaultRequestProcessor : matched;
...
// 從 pair 中拿到 Processor 進行處理
NettyRequestProcessor processor = pair.getObject1();
// 處理請求
RemotingCommand response = processor.processRequest(ctx, cmd);
....
}
如果進入源碼去看,會發現這個方法非常長,這裏省略了異步處理、異常處理及返回值構造等,僅列出了關鍵步驟:
- 根據
code
從processorTable
拿到對應的Pair
- 從
Pair
裏獲取Processor
最終處理請求的就是Processor
了。
2. Processor
的註冊
從上面的分析中可知, Processor
是處理消息的關鍵,它是從processorTable
中獲取的,這個processorTable
是啥呢?
processorTable
是NettyRemotingAbstract
成員變量,裏面的內容是BrokerController
在初始化時(執行BrokerController#initialize
方法)註冊的。之前在分析BrokerController
的初始化流程時,就提到過Processor
的提供操作,這裏再回顧下:
BrokerController
的初始化方法initialize
會調用 BrokerController#registerProcessor
,Processor
的註冊操作就在這個方法裏:
public class BrokerController {
private final PullMessageProcessor pullMessageProcessor;
/**
* 構造方法
*/
public BrokerController(...) {
// 處理 consumer 拉消息請求的
this.pullMessageProcessor = new PullMessageProcessor(this);
}
/**
* 註冊操作
*/
public void registerProcessor() {
// SendMessageProcessor
SendMessageProcessor sendProcessor = new SendMessageProcessor(this);
sendProcessor.registerSendMessageHook(sendMessageHookList);
sendProcessor.registerConsumeMessageHook(consumeMessageHookList);
// 處理 Processor
this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE,
sendProcessor, this.sendMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2,
sendProcessor, this.sendMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE,
sendProcessor, this.sendMessageExecutor);
// PullMessageProcessor
this.remotingServer.registerProcessor(RequestCode.PULL_MESSAGE,
this.pullMessageProcessor, this.pullMessageExecutor);
// 省略其他許許多多的Processor註冊
...
}
...
需要指明的是,sendProcessor
用來處理producer
請求過來的消息,pullMessageProcessor
用來處理consumer
拉取消息的請求。
3. 接收producer
消息
瞭解完remotingServer
的啓動與Processor
的註冊內容後,接下來我們就可以分析接收producer
消息的流程了。
producer
發送消息到broker
時,發送的請求code
爲SEND_MESSAGE
(這塊內容在後面分析producer
時再分析,暫時先當成一個結論吧),根據上面的分析,當消息過來時,會使用NettyServerHandler
這個ChannelHandler
來處理,之後會調用到NettyRemotingAbstract#processRequestCommand
方法。
在NettyRemotingAbstract#processRequestCommand
方法中,會根據消息的code
獲取對應的Processor
來處理,從Processor
的註冊流程來看,處理該SEND_MESSAGE
的Processor
爲SendMessageProcessor
,我們進入SendMessageProcessor#processRequest
看看它的流程:
public RemotingCommand processRequest(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
RemotingCommand response = null;
try {
// broker處理接收消息
response = asyncProcessRequest(ctx, request).get();
} catch (InterruptedException | ExecutionException e) {
log.error("process SendMessage error, request : " + request.toString(), e);
}
return response;
}
沒幹啥事,一路跟下去,直接看普通消息的流程,進入SendMessageProcessor#asyncSendMessage
方法:
private CompletableFuture<RemotingCommand> asyncSendMessage(ChannelHandlerContext ctx,
RemotingCommand request, SendMessageContext mqtraceContext,
SendMessageRequestHeader requestHeader) {
final RemotingCommand response = preSend(ctx, request, requestHeader);
final SendMessageResponseHeader responseHeader
= (SendMessageResponseHeader)response.readCustomHeader();
if (response.getCode() != -1) {
return CompletableFuture.completedFuture(response);
}
final byte[] body = request.getBody();
int queueIdInt = requestHeader.getQueueId();
TopicConfig topicConfig = this.brokerController.getTopicConfigManager()
.selectTopicConfig(requestHeader.getTopic());
// 如果沒指定隊列,就隨機指定一個隊列
if (queueIdInt < 0) {
queueIdInt = randomQueueId(topicConfig.getWriteQueueNums());
}
// 將消息包裝爲 MessageExtBrokerInner
MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
msgInner.setTopic(requestHeader.getTopic());
msgInner.setQueueId(queueIdInt);
// 省略處理 msgInner 的流程
...
CompletableFuture<PutMessageResult> putMessageResult = null;
Map<String, String> origProps = MessageDecoder
.string2messageProperties(requestHeader.getProperties());
String transFlag = origProps.get(MessageConst.PROPERTY_TRANSACTION_PREPARED);
// 發送事務消息
if (transFlag != null && Boolean.parseBoolean(transFlag)) {
...
// 發送事務消息
putMessageResult = this.brokerController.getTransactionalMessageService()
.asyncPrepareMessage(msgInner);
} else {
// 發送普通消息
putMessageResult = this.brokerController.getMessageStore().asyncPutMessage(msgInner);
}
return handlePutMessageResultFuture(putMessageResult, response, request, msgInner,
responseHeader, mqtraceContext, ctx, queueIdInt);
}
這個方法是在準備消息的發送數據,所做的工作如下:
- 如果沒指定隊列,就隨機指定一個隊列,一般情況下不會給消息指定隊列的,但如果要發送順序消息,就需要指定隊列了,這點後面再分析。
- 構造
MessageExtBrokerInner
對象,就是將producer
上送的消息包裝下,加上一些額外的信息,如消息標識msgId
、發送時間、topic
、queue
等。 - 發送消息,這裏只是分爲兩類:事務消息與普通消息,這裏我們主要關注普通消息,事務消息後面再分析。
進入普通消息的發送方法DefaultMessageStore#asyncPutMessage
:
public CompletableFuture<PutMessageResult> asyncPutMessage(MessageExtBrokerInner msg) {
...
// 保存到 commitLog
CompletableFuture<PutMessageResult> putResultFuture = this.commitLog.asyncPutMessage(msg);
...
}
繼續進入CommitLog#asyncPutMessage
方法:
public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {
msg.setStoreTimestamp(System.currentTimeMillis());
msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
AppendMessageResult result = null;
StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
String topic = msg.getTopic();
int queueId = msg.getQueueId();
final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
|| tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
// 延遲消息
if (msg.getDelayTimeLevel() > 0) {
// 延遲級別
if (msg.getDelayTimeLevel() > this.defaultMessageStore
.getScheduleMessageService().getMaxDelayLevel()) {
msg.setDelayTimeLevel(this.defaultMessageStore
.getScheduleMessageService().getMaxDelayLevel());
}
topic = TopicValidator.RMQ_SYS_SCHEDULE_TOPIC;
queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
// 保存真正的 topic 與 queueId
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID,
String.valueOf(msg.getQueueId()));
msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
// 換了一個topic與隊列
msg.setTopic(topic);
msg.setQueueId(queueId);
}
}
long elapsedTimeInLock = 0;
MappedFile unlockMappedFile = null;
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
putMessageLock.lock();
try {
long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
this.beginTimeInLock = beginLockTimestamp;
...
// 追加到文件中
result = mappedFile.appendMessage(msg, this.appendMessageCallback);
...
elapsedTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
beginTimeInLock = 0;
} finally {
putMessageLock.unlock();
}
...
}
在源碼裏,這個方法也是非常長,這裏刪減了大部分,只看關鍵點:
- 如果發送的是延遲消息,先保存原始的
topic
與queueId
,然後使用延遲隊列專有的topic
與queueId
- 將消息寫入到文件中
將消息寫入到文件的操作是在MappedFile#appendMessage(...)
方法中進行,關於這塊就不過多分析了,我們直接看官方的描述(鏈接:https://github.com/apache/rocketmq/blob/master/docs/cn/design.md):
rocketMq
消息存儲架構圖消息存儲架構圖中主要有下面三個跟消息存儲相關的文件構成。
(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
索引。在上面的
RocketMQ
的消息存儲整體架構圖中可以看出,RocketMQ
採用的是混合型的存儲結構,即爲Broker
單個實例下所有的隊列共用一個日誌數據文件(即爲CommitLog
)來存儲。RocketMQ
的混合型存儲結構(多個Topic
的消息實體內容都存儲於一個CommitLog中
)針對Producer
和Consumer
分別採用了數據和索引部分相分離的存儲結構,Producer
發送消息至Broker
端,然後Broker
端使用同步或者異步的方式對消息刷盤持久化,保存至CommitLog
中。只要消息被刷盤持久化至磁盤文件CommitLog
中,那麼Producer
發送的消息就不會丟失。正因爲如此,
Consumer
也就肯定有機會去消費這條消息。當無法拉取到消息後,可以等下一次消息拉取,同時服務端也支持長輪詢模式,如果一個消息拉取請求未拉取到消息,Broker
允許等待30s的時間,只要這段時間內有新消息到達,將直接返回給消費端。這裏,RocketMQ
的具體做法是,使用Broker
端的後臺服務線程—ReputMessageService
不停地分發請求並異步構建ConsumeQueue
(邏輯消費隊列)和IndexFile
(索引文件)數據。
當消息寫入commitlog
文件後,producer
發送消息的流程就結束了,接下來就是是消息的分發及消費流程了。
4. 總結
本文主要分析了 broker 接收producer
消息的流程,流程如下:
- 處理消息接收的底層服務爲 netty,在
BrokerController#start
方法中啓動 - netty服務中,處理消息接收的
channelHandler
爲NettyServerHandler
,最終會調用SendMessageProcessor#processRequest
來處理消息接收 - 消息接收流程的最後,
MappedFile#appendMessage(...)
方法會將消息內容寫入到commitLog
文件中。
本文的分析就到這裏了,下一篇我們繼續分析commitLog
文件的後續處理。
限於作者個人水平,文中難免有錯誤之處,歡迎指正!原創不易,商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
本文首發於微信公衆號 Java技術探祕,如果您喜歡本文,歡迎關注該公衆號,讓我們一起在技術的世界裏探祕吧!