rocketmq-深入消費源碼

更多請移步我的博客
對比看兩種消費方式的實現:順序消費與併發消費。這裏對順序消費只關注消費端,不關心producer與broker怎麼處理順序消息,假設架構及策略已保證消息的全局或者局部順序性。通過構建假定前提,我們可以忽略本次討論的非重點內容。以下仍以Push方式爲例。

消費方式

宏觀上看rmq自身是一個生產-消費模式,在他各個角色的具體實現中也不乏生產-消費模式的使用。DefaultMQPushConsumerImpl中消息的拉取及拉取成功後的消費均採用生產-消費的方式進行組織(PullRequestConsumeRequest),消費的流程在之前rocketmq-消息重複分析一文中記錄的很清楚,不再展開。

不論是順序還是併發消費,都使用ProcessQueue做爲本地消息的存儲介質,每個MessageQueue對應一個ProcessQueue,該關係保存在RebalanceImpl中,每次複雜均衡時可能發生改變。ProcessQueue內部用TreeMap<Long, MessageExt>來保存消息,key是消息的offset。TreeMap內部使用的紅黑樹,根據樹的特性可知消息的本地存儲是offset有序的,併發消費時DefaultMQPushConsumerImpl去拉取消息會根據offset(RebalanceImpl#getMaxSpan)的跨度判斷是否限流。

併發

關於併發消費的源碼在rocketmq-消息重複分析中已有分析。

這裏在強調下其ack機制的實現,ack是發送響應的過程,來確保消息的送達,在rmq實現中,爲了確保消息“至少消費一次”語義,採用 offset + sendback 的方式來實現。

在併發消費下,不論消費成功還是失敗offset都會記錄爲本次消費的最大一個offset,對於消費失敗的消息,rmq的consumer會再次發回broker,如果此步驟也失敗,降級爲本地延遲消費,然後重複消費步驟。

順序

順序分全局順序和局部順序。全局有序的話就一個隊列一個消費者;局部有序情況下,按照某個業務爲度將統一緯度的消息發送到指定隊列(可以通過自定義發送策略實現),消費者順序消費分配到的隊列消息。PS:廣播與集羣略有差異,以下默認集羣消費。

rocketmq-消息重複分析中有談到負載均衡的流程,在負載均衡完成的最後有這麼一個操作:

// RebalanceImpl
// omit

// Rebalance後,更新本地的queue信息,消費者提交PullRequest,從新隊列拉取消息
this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);

//omit

這方法的三個參數意義是:當前消費者分配到哪個topic下的哪些隊列,當前消費者的消費方式是否爲順序。

下面的代碼需要注意對isOrder的使用。

// RebalanceImpl

// 1. 刪除已經不再訂閱的messageQueue
// omit

// 2. 訂閱新的messageQueue,並封裝新的PullRequest
List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
for (MessageQueue mq : mqSet) {
    // 如果爲新增messageQueue,需要添加到本地記錄
    if (!this.processQueueTable.containsKey(mq)) {

        // 如果爲順序消費,並且鎖messageQueue失敗,則忽略該messageQueue
        if (isOrder && !this.lock(mq)) {
            log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
            continue;
        }

        // 刪除之前的消費進度
        this.removeDirtyOffset(mq);
        ProcessQueue pq = new ProcessQueue();

        // 計算消費進度,參見 ConsumeFromWhere,該值在啓動消費者時設置
        long nextOffset = this.computePullFromWhere(mq);

        // 進行本地存根,若爲新存根則發起PullRequest
        if (nextOffset >= 0) {
            ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
            if (pre != null) {
                log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
            } else {
                log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
                PullRequest pullRequest = new PullRequest();
                pullRequest.setConsumerGroup(consumerGroup);
                pullRequest.setNextOffset(nextOffset);
                pullRequest.setMessageQueue(mq);
                pullRequest.setProcessQueue(pq);
                pullRequestList.add(pullRequest);
                changed = true;
            }
        } else {
            log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
        }
    }
}

// 3. 提交PullRequest進行消息拉取
// omit

我們看到在爲順序消費時,多了一步對messageQueue的加鎖操作,看他做了些什麼事情。

// RebalanceImpl

public boolean lock(final MessageQueue mq) {
    // 找到該messageQueue所在的broker,該broker必須是master並且必須是該messageQueue所在的broker
    FindBrokerResult findBrokerResult = this.mQClientFactory.findBrokerAddressInSubscribe(mq.getBrokerName(), MixAll.MASTER_ID, true);
    if (findBrokerResult != null) {
        LockBatchRequestBody requestBody = new LockBatchRequestBody();
        requestBody.setConsumerGroup(this.consumerGroup);
        requestBody.setClientId(this.mQClientFactory.getClientId());
        requestBody.getMqSet().add(mq);

        try {
        	// 請求broker鎖定該messageQueue
        	// broker在RebalanceLockManager中嘗試鎖定messageQueue,若鎖定成功則保存MessageQueue與clientId的映射關係
        	// 一個messageQueue只能被一個client鎖定,來確保一個messageQueue在順序情況下只能被一個消費者訂閱,保證消費的順序性
            Set<MessageQueue> lockedMq =
                this.mQClientFactory.getMQClientAPIImpl().lockBatchMQ(findBrokerResult.getBrokerAddr(), requestBody, 1000);
            for (MessageQueue mmqq : lockedMq) {
            	// 更新本地存根
                ProcessQueue processQueue = this.processQueueTable.get(mmqq);
                if (processQueue != null) {
                    processQueue.setLocked(true);
                    // 記錄鎖定時間
                    processQueue.setLastLockTimestamp(System.currentTimeMillis());
                }
            }
            
            // 返回鎖定結果
            return lockedMq.contains(mq);
        } catch (Exception e) {
            log.error("lockBatchMQ exception, " + mq, e);
        }
    }

    return false;
}

在順序消費的模式下,負載均衡時需要鎖定隊列來避免多個消費者同時訂閱一個messageQueue的情況,併發消費模式不需要考慮這些問題。在消息的拉取時,順序消費也略有不同。我們以Push方式爲例來看。


// DefaultMQPushConsumerImpl

// 檢查messageQueue是否被鎖定
if (processQueue.isLocked()) {
    // 如果是第一次拉取該messageQueue
    if (!pullRequest.isLockedFirst()) {
        final long offset = this.rebalanceImpl.computePullFromWhere(pullRequest.getMessageQueue());
        boolean brokerBusy = offset < pullRequest.getNextOffset();
        log.info("the first time to pull message, so fix offset from broker. pullRequest: {} NewOffset: {} brokerBusy: {}",
            pullRequest, offset, brokerBusy);
        if (brokerBusy) {
            log.info("[NOTIFYME]the first time to pull message, but pull request offset larger than broker consume offset. pullRequest: {} NewOffset: {}",
                pullRequest, offset);
        }

        pullRequest.setLockedFirst(true);
        pullRequest.setNextOffset(offset);
    }
} else {
    // 如果messageQueue未被鎖定,等待一段時間再次嘗試拉取,ConsumeMessageOrderlyService啓動時會啓動任務定時去鎖定所有消費的messageQueue
    this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
    log.info("pull message later because not locked in broker, {}", pullRequest);
    return;
}

MessageListenerOrderly接口中有這麼一段註釋A MessageListenerOrderly object is used to receive asynchronously delivered messages orderly. one queue,one thread。意思是順序消費一個queue對應一個消費線程。因爲要順序消費,必然要保證前一個消費後才能消費後面一個,所以多線程在此處沒有存在的必要性。

// ConsumeMessageOrderlyService

// 如果發生負載均衡當前消費者不再處理該messageQueue時,processQueue會被標記爲刪除
if (this.processQueue.isDropped()) {
    log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
    return;
}

// 每個messageQueue對應一把鎖,同一時刻只能有一個線程在消費一個messageQueue
final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
synchronized (objLock) {
    // 檢查消費模式及鎖的有效性
    if (MessageModel.BROADCASTING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
        || (this.processQueue.isLocked() && !this.processQueue.isLockExpired())) {
        final long beginTime = System.currentTimeMillis();
        for (boolean continueConsume = true; continueConsume; ) {
            if (this.processQueue.isDropped()) {
                log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
                break;
            }

            // 集羣模式下檢查是否上鎖
            if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
                && !this.processQueue.isLocked()) {
                log.warn("the message queue not locked, so consume later, {}", this.messageQueue);
                ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
                break;
            }

            // 集羣模式下檢查鎖是否過期
            if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())
                && this.processQueue.isLockExpired()) {
                log.warn("the message queue lock expired, so consume later, {}", this.messageQueue);
                ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 10);
                break;
            }

            // 消費循環時長不能超過MAX_TIME_CONSUME_CONTINUOUSLY
            long interval = System.currentTimeMillis() - beginTime;
            if (interval > MAX_TIME_CONSUME_CONTINUOUSLY) {
                ConsumeMessageOrderlyService.this.submitConsumeRequestLater(processQueue, messageQueue, 10);
                break;
            }

            // 一次消費的消息數量,默認爲1
            final int consumeBatchSize =
                ConsumeMessageOrderlyService.this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();

            // 從本地緩存獲取消息
            List<MessageExt> msgs = this.processQueue.takeMessags(consumeBatchSize);
            if (msgs.isEmpty()) {
            	break;
            }

            final ConsumeOrderlyContext context = new ConsumeOrderlyContext(this.messageQueue);

            ConsumeOrderlyStatus status = null;

            ConsumeMessageContext consumeMessageContext = null;
            // omit hook

            long beginTimestamp = System.currentTimeMillis();
            ConsumeReturnType returnType = ConsumeReturnType.SUCCESS;
            boolean hasException = false;
            try {
            	// 對processQueue加上消費鎖,防止負載均衡時可能會發生爭搶
                this.processQueue.getLockConsume().lock();
                if (this.processQueue.isDropped()) {
                    log.warn("consumeMessage, the message queue not be able to consume, because it's dropped. {}",
                        this.messageQueue);
                    break;
                }
                
                // 參考 ConsumeOrderlyStatus
                status = messageListener.consumeMessage(Collections.unmodifiableList(msgs), context);
            } catch (Throwable e) {
                hasException = true;
            } finally {
                this.processQueue.getLockConsume().unlock();
            }
          
            // omit hook and stats preprocess
            
            // 拋出異常時
            if (null == status) {
                status = ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
            }

            // omit hook and stats
            // 處理消費結果
            continueConsume = ConsumeMessageOrderlyService.this.processConsumeResult(msgs, status, context, this);
        }
    } else {
        if (this.processQueue.isDropped()) {
            log.warn("the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
            return;
        }
        
        // 掛起片刻再次嘗試消費
        ConsumeMessageOrderlyService.this.tryLockLaterAndReconsume(this.messageQueue, this.processQueue, 100);
    }
}

再來看下怎麼處理消費結果。

// ConsumeMessageOrderlyService

public boolean processConsumeResult(
        final List<MessageExt> msgs,
        final ConsumeOrderlyStatus status,
        final ConsumeOrderlyContext context,
        final ConsumeRequest consumeRequest
    ) {
    boolean continueConsume = true;
    long commitOffset = -1L;
    if (context.isAutoCommit()) {
        switch (status) {
            case COMMIT:
            case ROLLBACK:
                log.warn("the message queue consume result is illegal, we think you want to ack these message {}",
                    consumeRequest.getMessageQueue());
            case SUCCESS:
                // 清空consumingMsgOrderlyTreeMap,並更新一些統計信息,返回下一個offset的起始
                commitOffset = consumeRequest.getProcessQueue().commit();
                break;
            case SUSPEND_CURRENT_QUEUE_A_MOMENT:
                // 檢查消息是否已經超過最大消費次數
                // 如果沒有超過,本地繼續嘗試消費
                // 如果超過,將消息發送到死信隊列,不在處理
                // 如果發送到死信隊列失敗,本地繼續嘗試消費
                if (checkReconsumeTimes(msgs)) {
                    consumeRequest.getProcessQueue().makeMessageToCosumeAgain(msgs);
                    this.submitConsumeRequestLater(
                        consumeRequest.getProcessQueue(),
                        consumeRequest.getMessageQueue(),
                        context.getSuspendCurrentQueueTimeMillis());
                    continueConsume = false;
                } else {
                    commitOffset = consumeRequest.getProcessQueue().commit();
                }
                break;
            default:
                break;
        }
    } else {
       // omit
    }

    // 更新消費偏移
    if (commitOffset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
        this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), commitOffset, false);
    }

    return continueConsume;
}

offset保存

offset的持久化可以在本地也可以在遠程(broker上,默認RemoteBrokerOffsetStore),offset並不是每次消費完成都會向broker發起持久化請求,有這麼幾個持久化入口:

  1. 定時任務,默認5S一次
  2. 每次去broker拉取消息時
  3. 消費者shutdown時

後記

ProcessQueue中已offset爲key做本地排序怎麼能保證消息的順序呢?很簡單,因爲broker是順序寫commitLog,所以後來消息的offset一定比先到的消息offset大。

ProcessQueue中爲順序消費保留了一個consumingMsgOrderlyTreeMap字段,該字段保存某次消費的消息,爲什麼需要做這個字段呢?猜測只是爲了應對批量消費的,即ConsumeMessageBatchMaxSize大於1時。

小生不才,以上如有描述有誤的地方還望各位不吝賜教 !_

貼出的源碼均基於release-4.3.2,爲了更好的表達描述的重點,貼出的源代碼會有所刪減。

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