RocketMQ 源碼閱讀 ---- 消息消費(普通消息)

RocketMQ Consumer 消費拉取的消息的方式有兩種
1.      Push方式:rocketmq 已經提供了很全面的實現,consumer 通過長輪詢拉取消息後回調 MessageListener 接口實現完成消費,應用系統只要重寫 MessageListener 的方法完成業務邏輯即可
2.      Pull方式:完全由業務系統去控制,定時拉取消息,指定隊列消費等等,當然這裏需要業務系統去根據自己的業務需求去實現
 
下面介紹 push 方式(long-polling 長輪詢方式實現):
1、DefaultMQPushConsumer 構造器初始化
DefaultMQPushConsumer 初始化 groupName,默認使用 AllocateMessageQueueAveragely 算法平均分配 queue 給 consumer。
 
3~6 訂閱給定的 Topic
 
4、創建訂閱數據對象
subExpression 爲 null,則訂閱 Topic 下全部內容
 
5、訂閱數據對象放入緩存
ConcurrentMap<String /* topic */, SubscriptionData> subscriptionInner ,放入 「4」步驟構造好的訂閱數據對象
 
6、發送心跳
如果 MQClientInstance 已經創建(步驟「12」創建),則將 consumer 心跳發送給所有 Broker
 
7、8、註冊監聽
給當前的 consumer 註冊一個回調方法,當隊列有消息的時候回調這個方法
 
9 ~ end 啓動 consumer
 
11、複製一份訂閱數據對象(其實就是 4、5 步驟,多了一個 retry+groupName 映射。重試隊列裏面 queue 看到只有1個,原來的隊列queue有4個)
複製一份步驟「4」創建的訂閱數據對象,放入緩存 ConcurrentMap<String /* topic */, SubscriptionData> subscriptionInner,topic 爲重試 topic = %RETRY%{groupname}
 
12、創建 MQClientInstance
 
13、RebalanceImpl 屬性初始化
 
14、PullAPIWrapper 初始化
 
15、offerset 存儲
BROADCASTING 存在 consumer 本地文件(局部進度。多個 consumer 消費相同 queue,消費進度是跟自己本身 consumer 有關,所以存本地就行,其他 consumer 不關心也不會用到其他的 consumer 的消費進度)
CLUSTERING 存在遠程 broker (全局進度。不同 consumer 消費不同 queue,是一個全局的進度,上報到 broker 在 consumer 宕機後才能由其他 consumer 繼續消費)
 
16、load() 加載消費進度
 
17、消息消費服務初始化
  • 順序消息消費 
  • 常規消息併發消費 ConsumeMessageConcurrentlyService
 
18、消息消費服務啓動
  • 常規消息,則啓動一個定時任務清理過期消息 cleanExpireMsg()
  • 順序消息並且是 CLUSTERING 模式,則啓動一個定時鎖隊列的任務 RebalanceImpl.lockAll()
 
19、註冊 Consumer
將 consumer 信息存入 ConcurrentMap<String/* group */, MQConsumerInner> consumerTable
 
20 ~  和之前的圖類似,這裏就不認真畫出時序關係了
20、MQClient 開始
這裏的 start,和 producer 的一樣,使用了同一個類同一個方法
 
21、Netty 服務啓動
 
22、啓動定時任務
(1)如果沒有設置 nameserver 地址,則定時獲取 nameserver 地址
(2)定時從 NameServer 更新 topic 路由信息(包含消費者 Set<MessageQueue> subscribeInfo 、生產者 TopicPublishInfo)
(3)定時清理下線的 Broker;給所有 Broker 發送心跳(包含訂閱關係)
(4)定時持久化所有消費者 Offset(即消費進度)。 存儲方式參考步驟「15」
(5)定時調整線程池
 
23、拉消息線程啓動
push 方式,卻使用  this.pullMessageService.start(); 拉消息服務,因爲這是一個推拉結合的實現。
 
24~28 負載均衡服務
遍歷步驟「5、11」賦值的 subscriptionInner
(1)Push 模式的均衡:DefaultMQPushConsumerImpl  
  •         無序 
            <1> 集羣模式
                            // topicSubscribeInfoTable 是 client 從 nameserver 上拉的路由信息
                        a. ConcurrentMap<String/* topic */, Set<MessageQueue>> topicSubscribeInfoTable = new ConcurrentHashMap<String, Set<MessageQueue>>();  
                            Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic)
                            
topicSubscribeInfoTable
        "%RETRY%please_rename_unique_group_name_4" -> 
                MessageQueue [topic=%RETRY%please_rename_unique_group_name_4, brokerName=LAPTOP-P9TNK0JN, queueId=0]
        
        "TopicTest" ->
                MessageQueue [topic=TopicTest, brokerName=LAPTOP-P9TNK0JN, queueId=2]
                MessageQueue [topic=TopicTest, brokerName=LAPTOP-P9TNK0JN, queueId=3]
                MessageQueue [topic=TopicTest, brokerName=LAPTOP-P9TNK0JN, queueId=0]
                MessageQueue [topic=TopicTest, brokerName=LAPTOP-P9TNK0JN, queueId=1]
 
                           
                           
 
29、查詢 ConsumerId 列表
// cidAll = 10.240.131.92@15804
 List<String> cidAll = findConsumerIdList(final String topic, final String group) 。先根據 group 去 broker address (優先使用 master,沒有master 隨機一個 slaver 地址    )查找所有 consumer list(還記得之前說過的 client 會定時向 broker 註冊自己的信息麼 「爲什麼 client 要向 broker 發心跳(this.mQClientFactory.sendHeartbeatToAllBrokerWithLock())?  發 group 信息給 broker」)。
 
 
30、使用負載均衡算法分配隊列
  allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,List<String> cidAll) 。然後根據步驟「1」中的分配算法 AllocateMessageQueueAveragely 給 consumr group 分配 queue。
                             AllocateMessageQueueAveragely  平均分配如下圖:比如默認一個 topic 有 4 個 queue,2個人消費,就是一人分兩個,平均分。根據 clientId,分配到的 queueid 固定。
                                                        
                          
                              AllocateMessageQueueAveragelyByCircle 循環平均分配:    
                               
31、步驟「30」負載均衡完成之後,更新本地處理隊列緩存
ProcessQueue (主要是 TreeMap<Offset, Msg> 和讀寫鎖)作用:保存了所有獲取到,但是還未被處理的消息
有了 ProcessQueue 的幫助 PushConsumer 會判斷獲取但還未處理的消息個數、 消息總大小、 Offset 的跨度,任何一個值超過定的大小就隔一段時間再拉取消息, 從而達到流量控制的目的。 此外 ProcessQueue 還可以輔助 實現順序消費的邏輯。
 
updateProcessQueueTableInRebalance(final String topic, final Set<MessageQueue> mqSet,final boolean isOrder)
                                    a. 如果隊列已經刪除或者隊列兩分鐘沒有拉取消息,則 removeUnnecessaryMessageQueue(mq, pq) 刪除無用消息隊列,並且 processQueue 置爲 drop = true 狀態
                                    b. 
                                        long nextOffset = this.computePullFromWhere(mq) 。計算從哪裏開始拉取,根據 topic、group、queueId 查詢消費進度 offset
                                        ConcurrentMap<MessageQueue, ProcessQueue> processQueueTable 增加正在被消費的隊列
                                        創建數據拉取請求 PullRequest                               
PullRequest pullRequest = new PullRequest();
pullRequest.setConsumerGroup(consumerGroup);
pullRequest.setNextOffset(nextOffset); // 消費進度
pullRequest.setMessageQueue(mq);
pullRequest.setProcessQueue(pq);
pullRequestList.add(pullRequest);
                                     this.dispatchPullRequest(pullRequestList)  :是在 PullMessageService 類中,將上一步對象賦值進 LinkedBlockingQueue<PullRequest> pullRequestQueue 。這是一個阻塞隊列,當有有數據賦值進來,則下面方法就可以開始執行 pullMessage(pullRequest) 的方法
 
向 broker 發送請求
// PullMessageService.java
@Override
public void run() {
log.info(this.getServiceName() + " service started");
 
while (!this.isStopped()) {
try {// Push Consumer 的示例,爲什麼會有 PullRequest ,這是通過 long polling 長輪詢方式達到 Push 效果。既有 pull 的優點不會壓垮 client,又有 Push 的實時性
PullRequest pullRequest = this.pullRequestQueue.take();
if (pullRequest != null) {
this.pullMessage(pullRequest);
}
} catch (InterruptedException e) {
} catch (Exception e) {
log.error("Pull Message Service Run Method exception", e);
}
}
 
log.info(this.getServiceName() + " service end");
}
 
2、獲取消費者 ()
final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup())
ConcurrentMap<String/* group */, MQConsumerInner> consumerTable
 
3、用真正實現類去拉取消息
pullMessage(pullRequest)
        判斷 processQueue.isDropped() 是否已經不可用,不可用用直接 return
        設置最後一次拉取時間戳 pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis())
        確保服務狀態正常 this.makeSureStateOK()
        限流,拉取還未消費超過默認值1000,則一定延時之後再拉。 if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue())
        限流,拉取未消費數據量大於100M,則一定延時之後再拉。if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue())
        
        消費模式:
                        正常消費(限流,拉取消息跨度太大,一定時間後再拉。 if (processQueue.getMaxSpan() > this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan()))
                        順序消費 (消費隊列必須已經鎖定,否則果斷時間再嘗試拉取)      
                                                
 
4、獲取訂閱數據
final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
 
5、創建回調對象
PullCallback pullCallback = new PullCallback()。從 broker 異步拉取成功後回調方法
 
6、從內存中讀取 commitOffsetValue
commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY) // 這裏獲取到的是 803 與 pullRequest.getNextOffset() 獲取內容一致
 
7、生成 sysFlag
這裏我獲取到的是 3
 
8、拉取消息
pullKernelImpl(mq,subExpression,expressionType,subVersion,offset,maxNums,sysFlag,commitOffset,brokerSuspendMaxTimeMillis,timeoutMillis,CommunicationMode communicationMode,PullCallback pullCallback)
 
PullMessageRequestHeader requestHeader = new PullMessageRequestHeader();
            requestHeader.setConsumerGroup(this.consumerGroup);
            requestHeader.setTopic(mq.getTopic());
            requestHeader.setQueueId(mq.getQueueId());
            requestHeader.setQueueOffset(offset);
            requestHeader.setMaxMsgNums(maxNums);
            requestHeader.setSysFlag(sysFlagInner);
            requestHeader.setCommitOffset(commitOffset);
            requestHeader.setSuspendTimeoutMillis(brokerSuspendMaxTimeMillis);  // 長輪詢使用參數。broker 最長阻塞時間,broker 沒有消息時才阻塞,有消息立刻返回。 
            requestHeader.setSubscription(subExpression);
            requestHeader.setSubVersion(subVersion);
            requestHeader.setExpressionType(expressionType);
 
        
9、獲取 broker 地址 
findBrokerAddressInSubscribe(brokerName,brokerId,onlyThisBroker)  先從本地取,沒有再去nameserve取。
 
10、11、使用 Netty 發起異步請求
pullMessage(addr,PullMessageRequestHeader,timeoutMillis,CommunicationMode,pullCallback)
將請求響應對應信息維護在 ConcurrentMap<Integer /* opaque */, ResponseFuture> responseTable,這樣異步請求成功的時候就能在回調的時候根據 opaque,就能找到請求對應的響應
 
 
broker 接受請求
1、處理consumer發來的請求
 
2、創建響應對象
 
3、響應對象賦值 opaque
 
4、獲取訂閱組配置
SubscriptionGroupConfig [groupName=please_rename_unique_group_name_4, consumeEnable=true, consumeFromMinEnable=true, consumeBroadcastEnable=true, retryQueueNums=1, retryMaxTimes=16, brokerId=0, whichBrokerWhenConsumeSlowly=1, notifyConsumerIdsChangedEnable=true]
 
5、獲取 topic 配置
ConcurrentMap<String, TopicConfig> topicConfigTable
this.topicConfigTable.get(topic)
TopicConfig [topicName=TopicTest, readQueueNums=4, writeQueueNums=4, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]
 
6、獲取消費組的信息
ConcurrentMap<String/* Group */, ConsumerGroupInfo> consumerTable
this.consumerTable.get(group)
 
7 ~ 15、獲取消息 getMessage(group,topic,queueId,offset,maxMsgNums,messageFilter)
    8、9 獲取最大偏移量
    10 獲取消費隊列
            GetMessageResult [status=OFFSET_OVERFLOW_ONE, nextBeginOffset=805, minOffset=0, maxOffset=805, bufferTotalSize=0, suggestPullingFromSlave=false]
    11、獲取隊列中最小偏移量,表示隊列中數據當前最小位置
    12、獲取隊列中最大偏移量,表示隊列中數據最大的位置。 當傳進來的下一個要讀取 offset 和 maxoffset 相同則說明沒有新的數據可讀
    13、getIndexBuffer(offset) 獲取
    14、commitLog.getMessage(offsetPy, sizePy)
    15、selectMappedBuffer(int pos, int size) 從 mappedByteBuffer 讀取信息
     
    
上面是有消息的情況,如果沒有消息,response 不立刻返回,而是靠 PullRequestHoldService.java 的 run 方法輪詢處理
//PullMessageProcessor.java 
....
case ResponseCode.PULL_NOT_FOUND:
 
                    if (brokerAllowSuspend && hasSuspendFlag) {
                        long pollingTimeMills = suspendTimeoutMillisLong;
                        if (!this.brokerController.getBrokerConfig().isLongPollingEnable()) {
                            pollingTimeMills = this.brokerController.getBrokerConfig().getShortPollingTimeMills();
                        }
 
                        String topic = requestHeader.getTopic();
                        long offset = requestHeader.getQueueOffset();
                        int queueId = requestHeader.getQueueId();
                        PullRequest pullRequest = new PullRequest(request, channel, pollingTimeMills,
                            this.brokerController.getMessageStore().now(), offset, subscriptionData, messageFilter);
                        this.brokerController.getPullRequestHoldService().suspendPullRequest(topic, queueId, pullRequest);  // broker hold 住請求
                        response = null;  // 沒消息,沒有馬上返回 response
                        break;
                    }
....
 
 
PullRequestHoldService.java
 
@Override
    public void run() {
        log.info("{} service started", this.getServiceName());
        while (!this.isStopped()) {
            try {
                if (this.brokerController.getBrokerConfig().isLongPollingEnable()) { // 允許長輪詢就多等會.. 
                    this.waitForRunning(5 * 1000);
                } else {
                    this.waitForRunning(this.brokerController.getBrokerConfig().getShortPollingTimeMills()); // 不允許長輪詢,就只延遲默認 1000ms
                }
 
                long beginLockTimestamp = this.systemClock.now();
                // 遍歷 hold 住的 request list
                // 1、查看 topic 下的 queue 有沒有新消息進來,有的話就返回 response。 
                // 2、沒有新消息,而且hold時間超過了,之前傳進來的 broker 最大掛起時間 timeoutMillis,也立即返回 response。
                // 3、剩餘的,繼續等待最後進入 1或者2
                this.checkHoldRequest();
                long costTime = this.systemClock.now() - beginLockTimestamp;
                if (costTime > 5 * 1000) {
                    log.info("[NOTIFYME] check hold request cost {} ms.", costTime);
                }
            } catch (Throwable e) {
                log.warn(this.getServiceName() + " service has exception. ", e);
            }
        }
 
        log.info("{} service end", this.getServiceName());
    }
長輪詢方式的侷限性,是在HOLD住Consumer請求的時候需要佔用資源,它適合用在消息隊列這種客戶端連接數可控的場景中。
       
 
32、消息隊列變更
messageQueueChanged(topic, mqSet, allocateResultSet)  將心跳信息發給所有 broker
 
 
從 broker 拉取消息後返回 consumer。對應代碼就是消息怎麼回調到 PullCallback
1、Netty client 的回調方法 
 
2 ~ ? 處理接收到的消息
4、從緩存中獲取 opaque 對應的響應信息
ConcurrentMap<Integer /* opaque */, ResponseFuture> responseTable
ResponseFuture responseFuture = responseTable.get(opaque);
在發送請求的時候已經賦值好了 responseTable,所以在拿到 response 的時候,根據傳回來的 opaque 就能對應上原來的 request。
 
5、刪除緩存 opaque 信息
拿到響應對象後,就可以先從緩存刪除 opaque 相應的響應信息
 
6、7、8 執行 netty 的回調,響應服務端的請求
因爲在 consumer pull message 的時候,netty 是異步調用,所以響應的時候去回調步驟「8」
 
9、回調 PullCallback 的回調方法
 
10、處理返回的結果
 
11、反序列化
 
12、設置下一個偏移量
 
13、將消息放入 ProcessQueue
將消息放進 porcessQueue 的 TreeMap 數據結構裏面,這個過程加鎖執行
TreeMap<Long, MessageExt> msgTreeMap
 
14、15、16、17 將消息提交給消費執行線程池 consumeExecutor 消費
非順序消息  ConsumeMessageConcurrentlyService 的 run 方法就會去回調 consumer 最初設置的 listener 回調方法,這樣消息就到了 consumer 測試用例重寫的方法了。
順序消息  ConsumeMessageConcurrentlyService 會在 listener 回調前進行一些操作(例如mq鎖檢查),已經調用後失敗的處理與非順序消息不同。(順序消息不能像無序消息一樣,消費失敗再次丟進 broker,這樣就亂序了,只能延遲一會再消費。)
 
18、處理消費後的結果
廣播模式消費失敗,報錯
集羣模式消費失敗,丟回 broker(sendMessageBack(final MessageExt msg, final ConsumeConcurrentlyContext context) 這個消息不是馬上就能被消費的,是有一定延遲的延遲消息),丟broker 如果失敗,則自己延遲消費(submitConsumeRequestLater(msgBackFailed, consumeRequest.getProcessQueue(), consumeRequest.getMessageQueue()))
 
19、清除這批消息
 
20、持久化 offset (這屬於消費完畢更新,當然還有之前啓動時候的定時任務持久化 offset)
集羣模式更新遠程 offset(RemoteBrokerOffsetStore)
廣播模式更新本地 offset
 
 
 
 
 
            <2> 廣播模式
 
  •         有序
 
(2)Pull 模式的均衡:DefaultMQPullConsumerImpl
        
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章