Rocketmq offset進度管理

qian_348840260發表於2020-10-19

下文以DefaultMQPushConsumerImpl叢集模式消費訊息為例。

概述

訊息消費完成後,需要將消費進度儲存起來,即前面提到的offset。廣播模式下,同消費組的消費者相互獨立,消費進度要單獨儲存;叢集模式下,同一條訊息只會被同一個消費組消費一次,消費進度會參與到負載均衡中,故消費進度是需要共享的。

消費者端

提交offset入口

入口在org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService#processConsumeResult中的最後一段邏輯:

 public void processConsumeResult(
        final ConsumeConcurrentlyStatus status,
        final ConsumeConcurrentlyContext context,
        final ConsumeRequest consumeRequest
    ) {
        ------------------省略-----------------------

        long offset = consumeRequest.getProcessQueue().removeMessage(consumeRequest.getMsgs());
        if (offset >= 0 && !consumeRequest.getProcessQueue().isDropped()) {
            this.defaultMQPushConsumerImpl.getOffsetStore().updateOffset(consumeRequest.getMessageQueue(), offset, true);
        }
    }

訊息消費完成(不論成功或失敗)後,將訊息從ProcessQueue中移除,同時返回ProcessQueue中最小的offset,使用這個offset值更新消費進度,removeMessage返回的offset有兩種情況,一是已經沒有訊息了,返回
ProcessQueue最大offset+1,二是還有訊息,則返回未消費訊息的最小offset。舉個例子,ProcessQueue中有offset為101-110的10條訊息,如果全部消費完了,返回的offset為111;如果101未消費完成,102-110消費完成,則返回的offset為101,這種情況下如果消費者異常退出,會出現重複消費的風險,所以要求消費邏輯冪等。

updateOffset邏輯

看RemoteBrokerOffsetStore的updateOffset()邏輯,將offset更新到記憶體中,這裡RemoteBrokerOffsetStore使用ConcurrentHashMap儲存MessageQueue的消費進度:

    @Override
    public void updateOffset(MessageQueue mq, long offset, boolean increaseOnly) {
        if (mq != null) {
            AtomicLong offsetOld = this.offsetTable.get(mq);
            if (null == offsetOld) {
                offsetOld = this.offsetTable.putIfAbsent(mq, new AtomicLong(offset));
            }

            if (null != offsetOld) {
                if (increaseOnly) {
                    MixAll.compareAndIncreaseOnly(offsetOld, offset);
                } else {
                    offsetOld.set(offset);
                }
            }
        }
    }

可以看到,這裡將offset更新到記憶體中就返回了,並沒有向broker端提交,具體提交邏輯有兩種方式:

方式1:拉取訊息時順帶提交

來看DefaultMQPushConsumerImpl類的pullMessage方法

    public void pullMessage(final PullRequest pullRequest) {
        final ProcessQueue processQueue = pullRequest.getProcessQueue();
        ------------------此處省略500字----------------------
        boolean commitOffsetEnable = false;
        long commitOffsetValue = 0L;
        if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) {
            commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);
            if (commitOffsetValue > 0) {
                commitOffsetEnable = true;
            }
        }

        
        int sysFlag = PullSysFlag.buildSysFlag(
            commitOffsetEnable, // commitOffset
            true, // suspend
            subExpression != null, // subscription
            classFilter // class filter
        );
        try {
            this.pullAPIWrapper.pullKernelImpl(
                pullRequest.getMessageQueue(),
                subExpression,
                subscriptionData.getExpressionType(),
                subscriptionData.getSubVersion(),
                pullRequest.getNextOffset(),
                this.defaultMQPushConsumer.getPullBatchSize(),
                sysFlag,
                commitOffsetValue,
                BROKER_SUSPEND_MAX_TIME_MILLIS,
                CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
                CommunicationMode.ASYNC,
                pullCallback
            );
        } catch (Exception e) {
            log.error("pullKernelImpl exception", e);
            this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
        }
    }

程式碼中,我們可以看到 通過this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY);從記憶體中讀到待提交的offset值,並將commitOffsetEnable設定為true. 核心方法pullKernelImpl將引數sysFlag和commitOffsetValue傳遞到broker端。

broker端處理拉取訊息的Processor是PullMessageProcessor。我們重點觀察一下processRequest方法中設定offset的程式碼。

        boolean storeOffsetEnable = brokerAllowSuspend;
        storeOffsetEnable = storeOffsetEnable && hasCommitOffsetFlag;
        storeOffsetEnable = storeOffsetEnable
            && this.brokerController.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE;
        if (storeOffsetEnable) {
            this.brokerController.getConsumerOffsetManager().commitOffset(RemotingHelper.parseChannelRemoteAddr(channel),
                requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getCommitOffset());
        }

方式2:拉取訊息時順帶提交

消費端每隔5s,通過persistAll向broker端提交offset值。

上翻邏輯,在MQClientInstance啟動的時候會註冊定時任務,每5s執行一次persistAllConsumerOffset(),最終呼叫到persistAll()。

    private void persistAllConsumerOffset() {
        Iterator<Entry<String, MQConsumerInner>> it = this.consumerTable.entrySet().iterator();
        while (it.hasNext()) {
            Entry<String, MQConsumerInner> entry = it.next();
            MQConsumerInner impl = entry.getValue();
            impl.persistConsumerOffset();
        }
    }
    //DefaultMQPushConsumerImpl
    @Override
    public void persistConsumerOffset() {
        try {
            this.makeSureStateOK();
            Set<MessageQueue> mqs = new HashSet<MessageQueue>();
            Set<MessageQueue> allocateMq = this.rebalanceImpl.getProcessQueueTable().keySet();
            mqs.addAll(allocateMq);

            this.offsetStore.persistAll(mqs);
        } catch (Exception e) {
            log.error("group: " + this.defaultMQPushConsumer.getConsumerGroup() + " persistConsumerOffset exception", e);
        }
    }
//RemoteBrokerOffsetStore
@Override
    public void persistAll(Set<MessageQueue> mqs) {
        if (null == mqs || mqs.isEmpty())
            return;

        final HashSet<MessageQueue> unusedMQ = new HashSet<MessageQueue>();

        for (Map.Entry<MessageQueue, AtomicLong> entry : this.offsetTable.entrySet()) {
            MessageQueue mq = entry.getKey();
            AtomicLong offset = entry.getValue();
            if (offset != null) {
                if (mqs.contains(mq)) {
                    try {
                        this.updateConsumeOffsetToBroker(mq, offset.get());
                        log.info("[persistAll] Group: {} ClientId: {} updateConsumeOffsetToBroker {} {}",
                            this.groupName,
                            this.mQClientFactory.getClientId(),
                            mq,
                            offset.get());
                    } catch (Exception e) {
                        log.error("updateConsumeOffsetToBroker exception, " + mq.toString(), e);
                    }
                } else {
                    unusedMQ.add(mq);
                }
            }
        }

        if (!unusedMQ.isEmpty()) {
            for (MessageQueue mq : unusedMQ) {
                this.offsetTable.remove(mq);
                log.info("remove unused mq, {}, {}", mq, this.groupName);
            }
        }
    }

broker端

broker端的具體邏輯在ConsumerManageProcessor,處理(1)查詢消費者列表(2)更新offset(3)查詢offset 三種請求。具體處理邏輯在ConsumerOffsetManager

ConsumerOffsetManager

使用

ConcurrentMap<String/* topic@group */, ConcurrentMap<Integer/*queueid*/, Long>>

來儲存所有offset資訊,大map的key為topic@group,小map的key為queueid。對記憶體的讀寫操作這裡不再詳細分析。
分析到此處,offset全是儲存在記憶體中,而這個offset必然是要持久化的,持久化的邏輯在哪裡?
ConsumerOffsetManager繼承自ConfigManager,ConfigManager的load方法是從檔案中載入資料到記憶體,persist方法是從記憶體持久化資料到檔案,查詢具體呼叫,在BrokerController中有如下邏輯:

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    try {
                        BrokerController.this.consumerOffsetManager.persist();
                    } catch (Throwable e) {
                        log.error("schedule persist consumerOffset error.", e);
                    }
                }
            }, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

即預設5s將資料持久化到檔案中

 

參考文件:

1. https://blog.csdn.net/yankunhaha/article/details/100061337

2. https://blog.csdn.net/GAMEloft9/article/details/103999826

相關文章