RocketMq broker启动如何恢复commitlog的offset

目录

1.介绍

2.启动流程

3.源码解读如何恢复commitlog的offset

1.介绍

        RocketMQ,作为阿里巴巴开源的分布式消息中间件,以其高吞吐、低延迟、高可用的特性,在众多企业级应用中扮演着至关重要的角色。在RocketMQ的体系架构中,Broker作为消息存储和转发的核心组件,其启动过程及文件加载机制是确保系统稳定运行的关键一环。本文将深入探讨RocketMQ Broker的启动流程,特别是它如何高效、有序地加载关键配置文件与数据文件,为读者揭开这一核心组件内部运作的神秘面纱。在RocketMQ中,Broker负责接收来自Producer的消息,存储这些消息,并在Consumer请求时进行消息分发。为了实现高性能与高可用,Broker的初始化过程设计得既精巧又高效,这其中包括了对配置文件、存储文件的加载,以及必要的服务端口绑定等步骤。

2.启动流程

        这里分析使用默认的DefaultMessageStore

        

3.源码解读如何恢复commitlog的offset

  1. 在RocketMQ中,Broker接收到消息并将其写入CommitLog后突然遭遇断电情况,重启后的数据恢复机制至关重要以防止消息丢失和资源浪费。针对这个问题,RocketMQ采用了一种持久化和检查点机制来解决。具体来说,Broker在写入CommitLog的同时,会对消息的写入位置(即offset)进行记录。这个位置信息会被定期持久化到磁盘上的一个名为StoreCheckpoint的文件中。当Broker重启时,它会首先读取StoreCheckpoint文件来获取上一次成功写入CommitLog的最后一个offset值。在Broker重启期间,它会依据checkpoint记录的offset继续服务,不会重新创建新的CommitLog文件,从而避免因仅写入少量消息而导致大文件资源浪费的问题。但是问题在于文件间并没有事物保证一致性,所以在加载过程中RocketMQ使用了遍历commitlog来确定offset。下面看下源码处理流程。省略初始化对应流程。
  2. 在这里先要了解一下rocketmq文件系统,每一个MappedFileQuene都在操作系统中映射了若干个文件
  3. 看一下org.apache.rocketmq.broker.BrokerController#recoverAndInitService中recover对应方法如何加载并覆盖offset
        //恢复并初始化。  只保留恢复相关
        public boolean recoverAndInitService() throws CloneNotSupportedException {
    
          ......
            if (messageStore != null) {
                registerMessageStoreHook();
                result = this.messageStore.load();
            }
    
          ......
        }

    转到org.apache.rocketmq.store.DefaultMessageStore#load方法中,省略无关代码,这里可以看到主要是实现了commitlog和consumerQueue文件加载,并映射成MappedFileQueue

    @Override
    public boolean load() {
        boolean result = true;
        boolean lastExitOK = !this.isTempFileExist();
        // load Commit Log
        // 加载commitlog文件 并在内存中映射出MappedFile
        result = this.commitLog.load();
        // load Consume Queue
        // 加载consumerQuene文件 并在内存中映射出MappedFile
        result = result && this.consumeQueueStore.load();
        //加载checkpoint
        this.storeCheckpoint = new StoreCheckpoint();
        // 恢复入口,关键地方
        this.recover(lastExitOK);
        return result;
    }

    接下来文件如何加载映射org.apache.rocketmq.store.MappedFileQueue#doLoad加载过程,这里可以看到,文件中写,刷新,提交指针三个原子指针都是都放在了文件结尾。如果不进行二次修改,最后一个文件只写入了一条消息的文件,在后续写入时会判断文件已满,创建新文件继续写入,造成资源浪费。如何重制这个指针

    AtomicIntegerFieldUpdater<DefaultMappedFile> WROTE_POSITION_UPDATER;
    AtomicIntegerFieldUpdater<DefaultMappedFile> COMMITTED_POSITION_UPDATER;
    AtomicIntegerFieldUpdater<DefaultMappedFile> FLUSHED_POSITION_UPDATER;
    
    public boolean doLoad(List<File> files) {
        for (int i = 0; i < files.size(); i++) {
             File file = files.get(i);
             MappedFile mappedFile = new DefaultMappedFile(file.getPath(), mappedFileSize);
             mappedFile.setWrotePosition(this.mappedFileSize);
             mappedFile.setFlushedPosition(this.mappedFileSize);
             mappedFile.setCommittedPosition(this.mappedFileSize);
             this.mappedFiles.add(mappedFile);
        }
        return true;
    }
  4. 加载后需要恢复指针位置,在org.apache.rocketmq.store.DefaultMessageStore#recover方法中看到2个recover,恢复consumerQueue,恢复commitLog,通过解析最后三个文件完成恢复
    private void recover(final boolean lastExitOK) throws RocksDBException {
        boolean recoverConcurrently = this.isRecoverConcurrently();
        // recover consume queue
        this.recoverConsumeQueue();
        long maxPhyOffsetOfConsumeQueue = this.consumeQueueStore.getMaxPhyOffsetInConsumeQueue();
        // recover commitlog
        if (lastExitOK) {
            this.commitLog.recoverNormally(maxPhyOffsetOfConsumeQueue);
        } else {
            this.commitLog.recoverAbnormally(maxPhyOffsetOfConsumeQueue);
        }
    }
    1. 首先恢复CQ(consumerQueue)队列指针位置org.apache.rocketmq.store.ConsumeQueue#recover
          public void recover() {
              // 从文件映射队列中获取文件列表
              final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
              if (!mappedFiles.isEmpty()) {
                  // 从倒数第3个文件开始 如果小于3个就从第0个到最后
                  int index = mappedFiles.size() - 3;
                  if (index < 0) {
                      index = 0;
                  }
      
                  int mappedFileSizeLogics = this.mappedFileSize;
                  MappedFile mappedFile = mappedFiles.get(index);
                  ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
                  long processOffset = mappedFile.getFileFromOffset();
                  long mappedFileOffset = 0;
                  long maxExtAddr = 1;
                  while (true) {
                      // CQ_STORE_UNIT_SIZE 这个是CQ文件的格式  offset+size+tagsCode  4+4+8
                      for (int i = 0; i < mappedFileSizeLogics; i += CQ_STORE_UNIT_SIZE) {
                          // 读取对应三个值
                          long offset = byteBuffer.getLong();
                          int size = byteBuffer.getInt();
                          long tagsCode = byteBuffer.getLong();
      
                          if (offset >= 0 && size > 0) {
                              mappedFileOffset = i + CQ_STORE_UNIT_SIZE;
                              this.setMaxPhysicOffset(offset + size);
                              if (isExtAddr(tagsCode)) {
                                  maxExtAddr = tagsCode;
                              }
                          } else {
                              break;
                          }
                      }
                      // 说明一个文件处理结束  进行下一个文件处理,可能会多,后续会进行处理,
                      // 毕竟文件底层写入无法保证原子性
                      if (mappedFileOffset == mappedFileSizeLogics) {
                          index++;
                          if (index >= mappedFiles.size()) {
                              break;
                          } else {
                              mappedFile = mappedFiles.get(index);
                              byteBuffer = mappedFile.sliceByteBuffer();
                              processOffset = mappedFile.getFileFromOffset();
                              mappedFileOffset = 0;
                          }
                      } else {
                          break;
                      }
                  }
                  // 全部处理完后将3个指针设置为当前文件处理完的位置
                  processOffset += mappedFileOffset;
                  this.mappedFileQueue.setFlushedWhere(processOffset);
                  this.mappedFileQueue.setCommittedWhere(processOffset);
                  this.mappedFileQueue.truncateDirtyFiles(processOffset);
      
              }
          }
      
  5.  恢复commitlog位置org.apache.rocketmq.store.CommitLog#recoverNormally
       public void recoverNormally(long maxPhyOffsetOfConsumeQueue) throws RocksDBException {
            boolean checkCRCOnRecover = this.defaultMessageStore.getMessageStoreConfig().isCheckCRCOnRecover();
            boolean checkDupInfo = this.defaultMessageStore.getMessageStoreConfig().isDuplicationEnable();
            final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
            if (!mappedFiles.isEmpty()) {
                // 还是从倒数第三位开始 如果小于3就全部
                int index = mappedFiles.size() - 3;
                if (index < 0) {
                    index = 0;
                }
    
                MappedFile mappedFile = mappedFiles.get(index);
                ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
                long processOffset = mappedFile.getFileFromOffset();
                long mappedFileOffset = 0;
                long lastValidMsgPhyOffset = this.getConfirmOffset();
                // normal recover doesn't require dispatching
                boolean doDispatch = false;
                while (true) {
                    //解析文件,看看是否可以反序列化成正常的消息格式
                    DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover, checkDupInfo);
                    int size = dispatchRequest.getMsgSize();
                    // Normal data
                    if (dispatchRequest.isSuccess() && size > 0) {
                        lastValidMsgPhyOffset = processOffset + mappedFileOffset;
                        mappedFileOffset += size;
                        this.getMessageStore().onCommitLogDispatch(dispatchRequest, doDispatch, mappedFile, true, false);
                    }
                    // Come the end of the file, switch to the next file Since the
                    // return 0 representatives met last hole,
                    // this can not be included in truncate offset
                    // 这里有一种情况  如果文件剩余位置不足以写下完整的消息,就会有个特殊处理,写下可用的空间,和一个标志位
                    // 反序列化结果会走到这里,会也同样认为文件遍历结束,进行下一个文件处理
                    else if (dispatchRequest.isSuccess() && size == 0) {
                        this.getMessageStore().onCommitLogDispatch(dispatchRequest, doDispatch, mappedFile, true, true);
                        index++;
                        if (index >= mappedFiles.size()) {
                            // Current branch can not happen
                            log.info("recover last 3 physics file over, last mapped file " + mappedFile.getFileName());
                            break;
                        } else {
                            mappedFile = mappedFiles.get(index);
                            byteBuffer = mappedFile.sliceByteBuffer();
                            processOffset = mappedFile.getFileFromOffset();
                            mappedFileOffset = 0;
                            log.info("recover next physics file, " + mappedFile.getFileName());
                        }
                    }
                    // Intermediate file read error
                    // 遍历到文件的最后写入位置了
                    else if (!dispatchRequest.isSuccess()) {
                        if (size > 0) {
                        }
                        break;
                    }
                }
                // 文件名偏移量加遍历出的偏移量
                processOffset += mappedFileOffset;
    
                if (this.defaultMessageStore.getBrokerConfig().isEnableControllerMode()) {
                    if (this.defaultMessageStore.getConfirmOffset() < this.defaultMessageStore.getMinPhyOffset()) {
                        this.defaultMessageStore.setConfirmOffset(this.defaultMessageStore.getMinPhyOffset());
                    } else if (this.defaultMessageStore.getConfirmOffset() > processOffset) {
                        this.defaultMessageStore.setConfirmOffset(processOffset);
                    }
                } else {
                    this.setConfirmOffset(lastValidMsgPhyOffset);
                }
    
                // Clear ConsumeQueue redundant data
                // 判断是否需要清除ConsumeQueue的冗余数据 如果CQ中有大于最大偏移量的数据,说明是冗余数据
                if (maxPhyOffsetOfConsumeQueue >= processOffset) {
                    this.defaultMessageStore.truncateDirtyLogicFiles(processOffset);
                }
    
                // 这里将commitlog的三个指针的位置都已经找到了最后一次写入位置
                this.mappedFileQueue.setFlushedWhere(processOffset);
                this.mappedFileQueue.setCommittedWhere(processOffset);
                this.mappedFileQueue.truncateDirtyFiles(processOffset);
            } else {
                // Commitlog case files are deleted
                this.mappedFileQueue.setFlushedWhere(0);
                this.mappedFileQueue.setCommittedWhere(0);
                this.defaultMessageStore.getQueueStore().destroy();
                this.defaultMessageStore.getQueueStore().loadAfterDestroy();
            }
        }

    4.结语

            在本文中,我们聚焦于RocketMQ Broker启动流程中的关键环节,尤其是对其在遭遇断电等异常状况下如何妥善处理已接收并写入CommitLog的消息进行了详尽解读。RocketMQ依靠其严谨构建的数据持久化与恢复机制,成功化解了潜在的消息丢失风险以及不必要的磁盘资源浪费难题。当Broker在执行消息写入CommitLog操作后遭受意外断电并重新启动时,系统会依赖事先持久保存的文件,精确检索出最后一次成功写入CommitLog的offset值,以此作为重启后继续提供服务的初始基准。如此设计,既规避了由于盲目创建新文件所可能导致的大规模磁盘空间无效占用问题,同时也确保了在系统重启后,任何已成功写入的消息均能得到妥善保留,充分展示了RocketMQ作为业界领先的分布式消息中间件产品在确保数据完整性与资源利用率方面的卓越表现。综上所述,RocketMQ Broker的启动加载机制及其面对断电重启场景下的恢复策略,有力彰显了其坚实的技术底蕴和高度可靠性。对此内在工作机制的透彻理解和娴熟运用,无疑将深化广大用户对RocketMQ核心特性的认知,并在实践中有力促进消息系统的平稳运行与数据安全性的确保,进而赋能企业信息化建设与业务连续性发展。

下一章写一下rocketmq的几种刷盘策略

  • 17
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值