RocketMQ源码解析-Broker消息存储ConsumeQueue

·  阅读 324

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

以下源码基于Rocket MQ 4.7.0

1. ConsumeQueue格式

ConsumeQueue.png

在之前就分析过,每一条ConsumeQueue数据是定长的20bytes,然后每一个文件存储30w条数据。存储是按照topic和queueId进行分类存储。那么ConsumeQueue的作用:

  • 前8个byte保存了CommitLog的偏移量,根据这个偏移量能够找到该条消息的CommitLog文件所在的位置
  • 中间的四个字节保存了消息大小根据前面的八个字节就能随机读出消息。
  • 最后八个字节用于处理消费者tag的过滤

这里我们只分析ConsumeQueue文件的生成过程,其他的会在后续的其他文章中分析

2. ConsumeQueue持久化过程

ConsumeQueue持久化也是RocketMQ三大持久化之一,所以通过源码分析找到,ConsumeQueue持久化是用过一个 ReputMessageService 来提供持久化服务,这个类是 ReputMessageService 类中的一个内部类:

class ReputMessageService extends ServiceThread {

     private volatile long reputFromOffset = 0;
	//省略代码
}
复制代码

继承了RocketMQ自定义的 ServiceThread 服务线程。所以个服务就是个线程。看一下Run方法:

        @Override
        public void run() {
            DefaultMessageStore.log.info(this.getServiceName() + " service started");

            while (!this.isStopped()) {
                try {
                    Thread.sleep(1);
                    this.doReput();
                } catch (Exception e) {
                    DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
                }
            }

            DefaultMessageStore.log.info(this.getServiceName() + " service end");
        }
复制代码

主要是通过调用doReput方法:

        private void doReput() {
            //获取读取数据的位置--默认是从0开始,对于运行一段时间的系统会进行设置
			if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
                log.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
                    this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
                this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
            }
            for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {

                if (DefaultMessageStore.this.getMessageStoreConfig().isDuplicationEnable()
                    && this.reputFromOffset >= DefaultMessageStore.this.getConfirmOffset()) {
                    break;
                }
				//获取CommitLog中可以使用的数据结果
                SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
                if (result != null) {
                    try {
                        this.reputFromOffset = result.getStartOffset();

                        for (int readSize = 0; readSize < result.getSize() && doNext; ) {
							//获取一条CommitLog数据出来
                            DispatchRequest dispatchRequest =
                                DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
									
                            int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();
							
                            if (dispatchRequest.isSuccess()) {
                                if (size > 0) {
									//调用处理数据生成ConsumeQueue
                                    DefaultMessageStore.this.doDispatch(dispatchRequest);

									//对于Broker Master进行处理
                                    if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
                                        && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
                                        DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
                                            dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
                                            dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
                                            dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
                                    }
									//向前推进offset
                                    this.reputFromOffset += size;
                                    readSize += size;
                                    if (DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE) {
                                        DefaultMessageStore.this.storeStatsService
                                            .getSinglePutMessageTopicTimesTotal(dispatchRequest.getTopic()).incrementAndGet();
                                        DefaultMessageStore.this.storeStatsService
                                            .getSinglePutMessageTopicSizeTotal(dispatchRequest.getTopic())
                                            .addAndGet(dispatchRequest.getMsgSize());
                                    }
                                } else if (size == 0) {
									//读取下一个文件
                                    this.reputFromOffset = DefaultMessageStore.this.commitLog.rollNextFile(this.reputFromOffset);
                                    readSize = result.getSize();
                                }
                            } else if (!dispatchRequest.isSuccess()) {

                                if (size > 0) {
                                    log.error("[BUG]read total count not equals msg total size. reputFromOffset={}", reputFromOffset);
                                    this.reputFromOffset += size;
                                } else {
                                    doNext = false;
                                    // If user open the dledger pattern or the broker is master node,
                                    // it will not ignore the exception and fix the reputFromOffset variable
                                    if (DefaultMessageStore.this.getMessageStoreConfig().isEnableDLegerCommitLog() ||
                                        DefaultMessageStore.this.brokerConfig.getBrokerId() == MixAll.MASTER_ID) {
                                        log.error("[BUG]dispatch message to consume queue error, COMMITLOG OFFSET: {}",
                                            this.reputFromOffset);
                                        this.reputFromOffset += result.getSize() - readSize;
                                    }
                                }
                            }
                        }
                    } finally {
                        result.release();
                    }
                } else {
                    doNext = false;
                }
            }
        }
复制代码
  1. 读取CommitLog中当前次数能够处理的数据。(数据可能在不同的文件中)
  2. 循环处理当前批次的数据将数据转换为 DispatchRequest
  3. 通过 DefaultMessageStore.doDispatch 去分发 DispatchReques t数据

下面来看一下 DefaultMessageStore.doDispatch 方法是如何对数据进行分发的:

    public void doDispatch(DispatchRequest req) {
        for (CommitLogDispatcher dispatcher : this.dispatcherList) {
            dispatcher.dispatch(req);
        }
    }
复制代码

通过这里可以看出来有一个 CommitLogDispatcher 接口, 这里其实就分别处理来生成 ConsumeQueue和IndexFile两种文件。

CommitLogDispatcher 在RocketMQ中有三种实现:

  1. CommitLogDispatcherBuildConsumeQueue -- 处理ConsumeQueue的生成
  2. CommitLogDispatcherBuildIndex -- 处理IndexFile的生成
  3. CommitLogDispatcherCalcBitMap -- 处理计算bit Map

下面来看一下CommitLogDispatcherBuildConsumeQueue的实现:

    class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {

        @Override
        public void dispatch(DispatchRequest request) {
            final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
            switch (tranType) {
                case MessageSysFlag.TRANSACTION_NOT_TYPE:
                case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
                    DefaultMessageStore.this.putMessagePositionInfo(request);
                    break;
                case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
                case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
                    break;
            }
        }
    }
复制代码

这里TRANSACTION_PREPARED_TYPE和TRANSACTION_ROLLBACK_TYPE这两种数据类型不需要处理。数据处理只要是通过 DefaultMessageStore.this.putMessagePositionInfo(request) 这段代码处理。

    public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
        ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
        cq.putMessagePositionInfoWrapper(dispatchRequest);
    }
复制代码

通过topic和queueId找到对应的ConsumeQueue。

 public void putMessagePositionInfoWrapper(DispatchRequest request) {
        final int maxRetries = 30;
        boolean canWrite = this.defaultMessageStore.getRunningFlags().isCQWriteable();
        for (int i = 0; i < maxRetries && canWrite; i++) {
            long tagsCode = request.getTagsCode();
            if (isExtWriteEnable()) {
                ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
                cqExtUnit.setFilterBitMap(request.getBitMap());
                cqExtUnit.setMsgStoreTime(request.getStoreTimestamp());
                cqExtUnit.setTagsCode(request.getTagsCode());

                long extAddr = this.consumeQueueExt.put(cqExtUnit);
                if (isExtAddr(extAddr)) {
                    tagsCode = extAddr;
                } else {
                    log.warn("Save consume queue extend fail, So just save tagsCode! {}, topic:{}, queueId:{}, offset:{}", cqExtUnit,
                        topic, queueId, request.getCommitLogOffset());
                }
            }
            boolean result = this.putMessagePositionInfo(request.getCommitLogOffset(),
                request.getMsgSize(), tagsCode, request.getConsumeQueueOffset());
            if (result) {
                if (this.defaultMessageStore.getMessageStoreConfig().getBrokerRole() == BrokerRole.SLAVE ||
                    this.defaultMessageStore.getMessageStoreConfig().isEnableDLegerCommitLog()) {
                    this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(request.getStoreTimestamp());
                }
                this.defaultMessageStore.getStoreCheckpoint().setLogicsMsgTimestamp(request.getStoreTimestamp());
                return;
            } else {
                // XXX: warn and notify me
                log.warn("[BUG]put commit log position info to " + topic + ":" + queueId + " " + request.getCommitLogOffset()
                    + " failed, retry " + i + " times");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    log.warn("", e);
                }
            }
        }

        // XXX: warn and notify me
        log.error("[BUG]consume queue can not write, {} {}", this.topic, this.queueId);
        this.defaultMessageStore.getRunningFlags().makeLogicsQueueError();
    }
复制代码

上面代码通过处理 putMessagePositionInfo 方法处理数据:

private boolean putMessagePositionInfo(final long offset, final int size, final long tagsCode,
        final long cqOffset) {

        if (offset + size <= this.maxPhysicOffset) {
            log.warn("Maybe try to build consume queue repeatedly maxPhysicOffset={} phyOffset={}", maxPhysicOffset, offset);
            return true;
        }

        this.byteBufferIndex.flip();
        this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
        this.byteBufferIndex.putLong(offset);
        this.byteBufferIndex.putInt(size);
        this.byteBufferIndex.putLong(tagsCode);

        final long expectLogicOffset = cqOffset * CQ_STORE_UNIT_SIZE;

        MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(expectLogicOffset);
        if (mappedFile != null) {

            if (mappedFile.isFirstCreateInQueue() && cqOffset != 0 && mappedFile.getWrotePosition() == 0) {
                this.minLogicOffset = expectLogicOffset;
                this.mappedFileQueue.setFlushedWhere(expectLogicOffset);
                this.mappedFileQueue.setCommittedWhere(expectLogicOffset);
                this.fillPreBlank(mappedFile, expectLogicOffset);
                log.info("fill pre blank space " + mappedFile.getFileName() + " " + expectLogicOffset + " "
                    + mappedFile.getWrotePosition());
            }

            if (cqOffset != 0) {
                long currentLogicOffset = mappedFile.getWrotePosition() + mappedFile.getFileFromOffset();

                if (expectLogicOffset < currentLogicOffset) {
                    log.warn("Build  consume queue repeatedly, expectLogicOffset: {} currentLogicOffset: {} Topic: {} QID: {} Diff: {}",
                        expectLogicOffset, currentLogicOffset, this.topic, this.queueId, expectLogicOffset - currentLogicOffset);
                    return true;
                }

                if (expectLogicOffset != currentLogicOffset) {
                    LOG_ERROR.warn(
                        "[BUG]logic queue order maybe wrong, expectLogicOffset: {} currentLogicOffset: {} Topic: {} QID: {} Diff: {}",
                        expectLogicOffset,
                        currentLogicOffset,
                        this.topic,
                        this.queueId,
                        expectLogicOffset - currentLogicOffset
                    );
                }
            }
            this.maxPhysicOffset = offset + size;
            return mappedFile.appendMessage(this.byteBufferIndex.array());
        }
        return false;
    }
复制代码

后续的数据其实和CommitLog的数据差不多,都是通过MappedFile进行数据落地。

3. 总结

总结一下ConsumeQueue生成的过程:

  1. 启动一个ReputMessageService服务线程
  2. 然后根据reputFromOffset去读取本次的CommitLog数据(没有或者有)
  3. 将每条数据包装为DispatchRequest
  4. DefaultMessageStore.doDispatch的方法进行处理数据,内部主要是通过CommitLogDispatcher的实现来进行处理
  5. 生成完ConsumeQueue纪录后的后续处理
  6. 重复2-4的步骤

每次处理完成一轮后会睡1毫秒的时间。并不是消息生成端发送了消息消费端就能够里面消费数据,而是要经过处理后生成ConsumeQueue才能消费。

上面说了能够消费的数据需要将CommitLog转化成为ConsumeQueue才能进行消费下面就来看一下,进行验证,将 ReputMessageService 的睡眠时间改为一分钟代码如下:

@Override
public void run() {
    DefaultMessageStore.log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        try {
            //Thread.sleep(1);
            //将睡眠时间改为1分钟
            TimeUnit.MINUTES.sleep(1);
            this.doReput();
        } catch (Exception e) {
            DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }

    DefaultMessageStore.log.info(this.getServiceName() + " service end");
}
复制代码

启动修改后的MQ Broker。

ConsumeQueue睡眠时间修改.gif

然后启动消费者,同时用生产者发送消息:

ConsumeQueue睡眠时间修改-1.gif

看一下两个时间的间隔:

1623405444983(发送时间)=2021-06-11 17:57:24

1623405552338(消费时间)=2021-06-11 17:59:12

通过两个时间发现不是立马进行消费。所以验证了前面的猜测。

每次处理完成一轮后会睡1毫秒的时间。并不是消息生成端发送了消息消费端就能够里面消费数据,而是要经过处理后生成ConsumeQueue才能消费。

我是蚂蚁背大象,文章对你有帮助点赞关注我,文章有不正确的地方请您斧正留言评论~谢谢!

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改