RocketMQ的消息存储机制浅谈(基于4.x.x)

  1. RocketMQ的介绍

    1.   起源

      RocketMQ是阿里巴巴在2012年开发的分布式消息中间件,专为万亿级超大规模的消息处理而设计,具有高吞吐量、低延迟、海量堆积、顺序收发等特点。它是阿里巴巴双十一购物狂欢节和众多大规模互联网业务场景的必备基础设施。在同一年,阿里巴巴正式开源了RocketMQ的第一个版本。
      2015年,RocketMQ在消息传递方面迎来了一批重量级功能发布,包括事务消息、SQL过滤、轨迹追踪、定时消息、高可用多活等,以满足阿里巴巴日益丰富的业务场景。由于这些优势,RocketMQ 取代了阿里巴巴自主研发的另一款MQ产品Notify,成为阿里巴巴的首选消息中间件,实现了内部应用的百分百接入。在2016年,RocketMQ在阿里云上开发了首个全托管服务,帮助大量数字化转型的企业构建现代应用,并开始体验大规模的云计算实践。同年,RocketMQ被捐赠给Apache基金会,并入选孵化器项目,旨在未来为更多开发者服务。
      2017年从Apache基金会毕业后,RocketMQ被指定为顶级项目(TLP)。
      RocketMQ的设计理念结合了多项高性能存储技术,包括顺序写入、内存映射、零拷贝技术等,使其在大规模消息处理场景中表现出色。
    1.     创始团队

    头像 名称 Apache Id Github Id 参与时间

    image

     

    Xiaorui Wang vintagewang @vintagewang Create RocketMQ in 2012

    image

    Qingshan Lin linhill @hill007299 Since @2013

    image

    Qiudi Yang jodie @jodie.yang Since @2013

    image

    Yubao Fu fuyou @fuyou001 Since @2013

    image

    Jixiang Jin lollipop @lollipopjin Since @2014

    image

    Li Zhou   @zhouli11 Since @2014
    Zhongliang Chen chenzlalvin @chenzlalvin Since @2015

    image

    Xinyu Zhou yukon @zhouxinyu Since @2016
    Meiping Zhang   @gongyi-zmp Since @2016

    image

    ZhenDong Liu dongeforever @dongeforever Since @2016
  1. MQ的几种消息存储模式的介绍

    1.   基于JDBC数据库存储模式(mysql)

      JDBC存储模式允许消息中间件使用关系型数据库进行消息持久化存储。这种方式可以借助数据库的事务特性来保证消息的可靠性,但通常性能较低,不适合高吞吐量场景。支持的数据包括MySQL、Oracle、DB2等主流关系型数据库。
    1.   KahaDB存储模式

      KahaDB是ActiveMQ从5.4版本开始的默认持久化存储方案,它提供了容量提升和恢复能力。KahaDB的前身是AMQ消息存储,基于文件存储方式,但KahaDB对其进行了优化和改进。KahaDB由一组日志文件组成,使用了内存索引来提高消息检索速度,同时提供了更快的恢复能力。在KahaDB中,所有消息和事务日志存储在一个单独的可追加日志文件中,同时有一个内存索引缓存来快速定位消息。
    1.   LevelDB存储模式

      LevelDB是Google开发的一种高性能键值存储数据库,具有高效的读写性能。虽然搜索结果中没有详细提及LevelDB,但它在消息中间件中也有应用,通常用于需要高性能持久化的场景。LevelDB能够提供比传统关系型数据库更好的写入性能,同时保证数据的持久化和可靠性。
      缺点
      这不是一个 SQL 数据库,它没有关系数据模型,不支持 SQL 查询,也不支持索引。同时只能有一个进程(可能是具有多线程的进程)访问一个特定的数据库。该程序库没有内置的 client-server 支持,有需要的用户必须自己封装。
    1.   内存存储模式

      内存存储模式将消息保存在内存中而不是持久化到磁盘,提供了极高的读写性能,但会在服务重启或崩溃时丢失消息。这种模式适用于对性能要求极高但允许消息丢失的场景,如实时数据传输、实时统计等。
  1. RocketMQ选择的消息存储模式

      RocketMQ采用了自主研发的混合型存储结构,所有消息主题的消息都存储CommitLog文件中,然后通过构建ConsumeQueue和IndexFile来提供消息检索服务。这种设计使得RocketMQ在保证高吞吐量的同时,提供了良好的可靠性和扩展性。
    1.   RocketMQ的存储模式具有以下特点

    • 所有消息共用CommitLog:在单个Broker实例中,所有的队列共享一个CommitLog文件,即所有消息顺序写入CommitLog文件。这种设计减少了磁盘竞争,避免了因为队列增加导致的I/O等待增高,提高了写入性能。
    • 异步构建索引:通过CommitLogDispatcher.dispatch方法异步分发请求并构建出ConsumeQueue文件和IndexFile文件。这种异步处理方式降低了对主写入流程的影响,提高了整体吞吐量。
    • 零拷贝技术:RocketMQ采用MappedByteBuffer内存映射技术,将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率。
    • 顺序写入:消息主要是顺序写入日志文件,当文件写满后,再写入下一个文件。顺序写入大大提高了磁盘IO利用率,避免了随机写入带来的性能开销。
    1.   选择混合型的存储模式的原因

      RocketMQ选择这种存储模式的原因在于它需要在保证高吞吐量的同时,处理大量的消息堆积。阿里巴巴的业务场景(如双十一购物节)需要消息中间件能够处理亿级消息堆积,同时保持稳定的性能表现。传统的数据库存储或简单的文件存储无法同时满足这些需求,因此RocketMQ设计了这种混合型存储结构。
  1. RocketMQ消息的基本信息和发送的基本流程

    1.   消息发送的类型

      • 同步发送
      消息生产者调用发送的API后,需要等待消息服务器返回本次消息发送的结果。
    • 异步发送
      消息生产者调用发送的API后,无须等待消息服务器返回本次消息发送的结果,只需要提供一个回调函数,供消息发送客户端在收到响应结果后回调。
    • 单项发送
      消息生产者调用消息发送的API后,无须等待消息服务器返回本次消息发送的结果,并且无须提供回调函数,这表示压根就不关心本次消息发送是否成功,其实现原理与异步消息发送相同,只是消息发送客户端在收到响应结果后什么都不做了,并且没有重试机制。
    1.   消息的类型

      主要有普通消息,批量消息,事物消息,延时消息(定时消息)。
    1.   发送消息的方式

    • 单条发送
    • 批量发送
      将同一主题的多条消息一起打包发送到消息服务端,减少网络调用次数,提高网络传输效率。当然,并不是在同一批次中发送的消息数量越多,性能就越好,判断依据是单条消息的长度,如果单条消息内容比较长,则打包发送多条消息会影响其他线程发送消息的响应时间,并且单批次消息发送总长度不能超过Default MQProducer#maxMessageSize。
    1.   消息发送流程

          主要流程为消息验证,查找路由信息,选择消息队列和发送消息四个主要节点。
      消息发送时序图
     

    whiteboard_exported_image

     

  2. 消息存储的流程

  1. 存储文件

RocketMQ存储路径为${ROCKET_HOME}/store,主要存储文件如下图所示。RocketMQ主要的存储文件夹。
  • commitlog:消息存储目录。
  • config:运行期间的一些配置信息,主要包括下列信息。
    • consumerFilter.json:主题消息过滤信息。
    • consumerOffset.json:集群消费模式下的消息消费进度。
    • delayOffset.json:延时消息队列拉取进度。
    • subscriptionGroup.json:消息消费组的配置信息。
    • topics.json: topic配置属性。
  • consumequeue:消息消费队列存储目录。
  • index:消息索引文件存储目录。
  • abort:如果存在abort文件,说明Broker非正常关闭,该文件默认在启动Broker时创建,在正常退出之前删除。
  • checkpoint:检测点文件,存储CommitLog文件最后一次刷盘时间戳、ConsumeQueue最后一次刷盘时间、index文件最后一次刷盘时间戳。
  1. 重要的概念

RocketMQ消息的存储是由ConsumeQueueCommitLog配合完成的,消息真正的物理存储文件是CommitLogConsumeQueue是消息的逻辑队列,存储topic相关信息,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。
消息存储文件结构说明
  1. CommitLog

      消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容。单个文件大小默认1G,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件。
  1. ConsumeQueue

      消息消费的逻辑队列,作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息TagHashCode值。从实际物理存储来说,ConsumeQueue对应每个TopicQueueId下面的文件。单个文件大小约5.72M,每个文件由30W条数据组成,每个文件默认大小为600万个字节,当一个ConsumeQueue类型的文件写满了,则写入下一个文件。
  1. IndexFile

      因为所有的消息都存在CommitLog中,如果要实现根据key查询消息的方法,就会变得非常困难,所以为了解决这种业务需求,有了IndexFile的存在。用于为生成的索引文件提供访问服务,通过消息Key值查询消息真正的实体内容。在实际的物理存储上,文件名则是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存2000W个索引。IndexFile(索引文件)则只是为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程。
  1. MappedFileQueue(相当于多个mapperFile集合)

      MappedFileQueue是MappedFile的管理容器,MappedFileQueue对存储目录进行封装,例如CommitLog文件的存储路径为${ROCKET_HOME}/store/commitlog/,该目录下会存在多个内存映射文件MappedFile。
      对连续物理存储的抽象封装类,源码中可以通过消息存储的物理偏移量位置快速定位该offset所在。对MappedFile(具体物理存储位置的抽象)、创建、删除MappedFile等操作。
      MappedFileQueue相关属性
    //String storePath:存储目录
    private final String storePath;
    //单个文件的存储大小
    private final int mappedFileSize;
    //MappedFile集合
    private final CopyOnWriteArrayList<MappedFile> mappedFiles = new CopyOnWriteArrayList<MappedFile>();
    //创建MappedFile服务类
    private final AllocateMappedFileService allocateMappedFileService;
    //当前刷盘指针,表示该指针之前的所有数据全部持久化到磁盘
    private long flushedWhere = 0;
    //当前数据提交指针,内存中ByteBuffer当前的写指针,该值大于、等于flushedWhere
    private long committedWhere = 0;
    //文件被flush到磁盘的时间撮
    private volatile long storeTimestamp = 0;
    

      

  1. MappedFile(可以认为是一个commitLog文件的映射)

      文件存储的直接内存映射业务抽象封装类,源码中通过操作该类,可以把消息字节写入PageCache缓存区(commit),或者原子性地将消息持久化的刷盘(flush)。
  1. 消息存储流程

    1. 预处理检查:判断Broker状态、角色和消息有效性
    2. 延迟消息处理:如消息有延迟等级,修改主题和队列ID
    3. 获取MappedFile:获取下一个可写的文件对象
    4. MapperFile初始化
    5. 加锁写入:保证线程安全
    6. 消息写入ByteBuffer
    7. 返回结果:创建AppendMessageResult返回写入状态
      消息存储流程
     

    image

     

  2. putMessage

      详细的putMessage代码流程
      
      1 public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
      2     // Set the storage time
      3 msg.setStoreTimestamp(System.currentTimeMillis());
      4 // Set the message body BODY CRC (consider the most appropriate setting
      5 // on the client)
      6 msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
      7 // Back to Results
      8 AppendMessageResult result = null;
      9 
     10 StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
     11 
     12 String topic = msg.getTopic();
     13 int queueId = msg.getQueueId();
     14 //延迟消息处理:如消息有延迟等级,修改主题和队列ID
     15 final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
     16 if (tranType == MessageSysFlag.TRANSACTION_NOT_TYPE
     17 || tranType == MessageSysFlag.TRANSACTION_COMMIT_TYPE) {
     18     // Delay Delivery
     19 if (msg.getDelayTimeLevel() > 0) {
     20         if (msg.getDelayTimeLevel() > this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel()) {
     21             msg.setDelayTimeLevel(this.defaultMessageStore.getScheduleMessageService().getMaxDelayLevel());
     22         }
     23 
     24         topic = ScheduleMessageService.SCHEDULE_TOPIC;
     25         queueId = ScheduleMessageService.delayLevel2QueueId(msg.getDelayTimeLevel());
     26 
     27         // Backup real topic, queueId
     28 MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_TOPIC, msg.getTopic());
     29         MessageAccessor.putProperty(msg, MessageConst.PROPERTY_REAL_QUEUE_ID, String.valueOf(msg.getQueueId()));
     30         msg.setPropertiesString(MessageDecoder.messageProperties2String(msg.getProperties()));
     31 
     32         msg.setTopic(topic);
     33         msg.setQueueId(queueId);
     34     }
     35 }
     36 
     37 long eclipseTimeInLock = 0;
     38 MappedFile unlockMappedFile = null;
     39 // 获取MappedFile:获取下一个可写的文件对象
     40 MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
     41 //加锁写入:保证线程安全
     42 putMessageLock.lock(); //锁类型 spin or ReentrantLock ,depending on store config
     43 try {
     44     long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
     45     this.beginTimeInLock = beginLockTimestamp;
     46 
     47     // Here settings are stored timestamp, in order to ensure an orderly
     48 // global
     49 msg.setStoreTimestamp(beginLockTimestamp);
     50 
     51     if (null == mappedFile || mappedFile.isFull()) {
     52         mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
     53 }
     54     if (null == mappedFile) {
     55         log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
     56         beginTimeInLock = 0;
     57         return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
     58     }
     59     // 消息写入ByteBuffer
     60     result = mappedFile.appendMessage(msg, this.appendMessageCallback);
     61     //返回结果:创建AppendMessageResult返回写入状态
     62     switch (result.getStatus()) {
     63         case PUT_OK:
     64             break;
     65         case END_OF_FILE:
     66             unlockMappedFile = mappedFile;
     67             // Create a new file, re-write the message
     68 mappedFile = this.mappedFileQueue.getLastMappedFile(0);
     69             if (null == mappedFile) {
     70                 // XXX: warn and notify me
     71                 log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
     72                 beginTimeInLock = 0;
     73                 return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
     74             }
     75             result = mappedFile.appendMessage(msg, this.appendMessageCallback);
     76             break;
     77         case MESSAGE_SIZE_EXCEEDED:
     78         case PROPERTIES_SIZE_EXCEEDED:
     79             beginTimeInLock = 0;
     80             return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result);
     81         case UNKNOWN_ERROR:
     82             beginTimeInLock = 0;
     83             return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
     84         default:
     85             beginTimeInLock = 0;
     86             return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
     87     }
     88 
     89     eclipseTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
     90     beginTimeInLock = 0;
     91 } finally {
     92     putMessageLock.unlock();
     93 }
     94 
     95 if (eclipseTimeInLock > 500) {
     96     log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", eclipseTimeInLock, msg.getBody().length, result);
     97 }
     98 
     99 if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
    100     this.defaultMessageStore.unlockMappedFile(unlockMappedFile);
    101 }
    102 
    103 PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);
    104 
    105 // Statistics
    106 storeStatsService.getSinglePutMessageTopicTimesTotal(msg.getTopic()).incrementAndGet();
    107 storeStatsService.getSinglePutMessageTopicSizeTotal(topic).addAndGet(result.getWroteBytes());
    108 //执行刷盘
    109 handleDiskFlush(result, putMessageResult, msg);
    110 handleHA(result, putMessageResult, msg);
    111 
    112 return putMessageResult;
    113 }
    114 public AppendMessageResult appendMessage(final MessageExtBrokerInner msg, final AppendMessageCallback cb) {
    115     return appendMessagesInner(msg, cb);
    116 }
    117 
    118 public AppendMessageResult appendMessages(final MessageExtBatch messageExtBatch, final AppendMessageCallback cb) {
    119     return appendMessagesInner(messageExtBatch, cb);
    120 }
    121 
    122 public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
    123     assert messageExt != null;
    124     assert cb != null;
    125 
    126     int currentPos = this.wrotePosition.get();
    127 
    128     if (currentPos < this.fileSize) {
    129         ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
    130         byteBuffer.position(currentPos);
    131         AppendMessageResult result = null;
    132         if (messageExt instanceof MessageExtBrokerInner) {
    133             result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
    134         } else if (messageExt instanceof MessageExtBatch) {
    135             result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
    136         } else {
    137             return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
    138         }
    139         this.wrotePosition.addAndGet(result.getWroteBytes());
    140         this.storeTimestamp = result.getStoreTimestamp();
    141         return result;
    142     }
    143     log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize);
    144     return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
    145 }
  1. mapperFile初始化

      初始化方法
    public void init(final String fileName, final int fileSize,
        final TransientStorePool transientStorePool) throws IOException {
        init(fileName, fileSize);
        this.writeBuffer = transientStorePool.borrowBuffer();
        this.transientStorePool = transientStorePool;
    }
    
    private void init(final String fileName, final int fileSize) throws IOException {
        this.fileName = fileName;
        this.fileSize = fileSize;
        this.file = new File(fileName);
        this.fileFromOffset = Long.parseLong(this.file.getName());
        boolean ok = false;
    
        ensureDirOK(this.file.getParent());
    
        try {
            this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
            this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
            TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
            TOTAL_MAPPED_FILES.incrementAndGet();
            ok = true;
        } catch (FileNotFoundException e) {
            log.error("create file channel " + this.fileName + " Failed. ", e);
            throw e;
        } catch (IOException e) {
            log.error("map file " + this.fileName + " Failed. ", e);
            throw e;
        } finally {
            if (!ok && this.fileChannel != null) {
                this.fileChannel.close();
            }
        }
    }
    

      

  1. TransientStorePool

      TransientStorePool即短暂的存储池。RocketMQ单独创建了一个DirectByteBuffer内存缓存池,用来临时存储数据,数据先写入该内存映射中,然后由Commit线程定时将数据从该内存复制到与目标物理文件对应的内存映射中。RokcetMQ引入该机制是为了提供一种内存锁定,将当前堆外内存一直锁定在内存中,避免被进程将内存交换到磁盘中。
    //avaliableBuffers个数,可在broker配置文件中通过transient StorePoolSize进行设置,默认为5
    private final int poolSize;
    //每个ByteBuffer的大小,默认为mapedFileSizeCommitLog,表明TransientStorePool为CommitLog文件服务
    private final int fileSize;
    //Deque availableBuffers: ByteBuffer容器,双端队列
    private final Deque<ByteBuffer> availableBuffers;
    //其他配置
    private final MessageStoreConfig storeConf
      初始化TransientStorePool
    /**
     * It's a heavy init method.
     */
    public void init() {
        for (int i = 0; i < poolSize; i++) {
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(fileSize);
    
            final long address = ((DirectBuffer) byteBuffer).address();
            Pointer pointer = new Pointer(address);
            LibC.INSTANCE.mlock(pointer, new NativeLong(fileSize));
    
            availableBuffers.offer(byteBuffer);
        }
    }
    

      

创建数量为poolSize的堆外内存,利用com.sun.jna.Library类库锁定该批内存,避免被置换到交换区,以便提高存储性能。
  1. mapperFile写入

      mapperFile写入
    public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, final int maxBlank,
        final MessageExtBrokerInner msgInner) {
        // STORETIMESTAMP + STOREHOSTADDRESS + OFFSET <br>
    
        // PHY OFFSET
        long wroteOffset = fileFromOffset + byteBuffer.position();
    
        this.resetByteBuffer(hostHolder, 8);
        String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(hostHolder), wroteOffset);
    
        // Record ConsumeQueue information
        keyBuilder.setLength(0);
        keyBuilder.append(msgInner.getTopic());
        keyBuilder.append('-');
        keyBuilder.append(msgInner.getQueueId());
        String key = keyBuilder.toString();
        Long queueOffset = CommitLog.this.topicQueueTable.get(key);
        if (null == queueOffset) {
            queueOffset = 0L;
            CommitLog.this.topicQueueTable.put(key, queueOffset);
        }
    
        // Transaction messages that require special handling
        final int tranType = MessageSysFlag.getTransactionValue(msgInner.getSysFlag());
        switch (tranType) {
            // Prepared and Rollback message is not consumed, will not enter the
            // consumer queuec
            case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
            case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
                queueOffset = 0L;
                break;
            case MessageSysFlag.TRANSACTION_NOT_TYPE:
            case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
            default:
                break;
        }
    
        /**
         * Serialize message
         */
        final byte[] propertiesData =
            msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8);
    
        final int propertiesLength = propertiesData == null ? 0 : propertiesData.length;
    
        if (propertiesLength > Short.MAX_VALUE) {
            log.warn("putMessage message properties length too long. length={}", propertiesData.length);
            return new AppendMessageResult(AppendMessageStatus.PROPERTIES_SIZE_EXCEEDED);
        }
    
        final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8);
        final int topicLength = topicData.length;
    
        final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length;
    
        final int msgLen = calMsgLength(bodyLength, topicLength, propertiesLength);
    
        // Exceeds the maximum message
        if (msgLen > this.maxMessageSize) {
            CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength
                + ", maxMessageSize: " + this.maxMessageSize);
            return new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED);
        }
    
        // Determines whether there is sufficient free space
        if ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) {
            this.resetByteBuffer(this.msgStoreItemMemory, maxBlank);
            // 1 TOTALSIZE
            this.msgStoreItemMemory.putInt(maxBlank);
            // 2 MAGICCODE
            this.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE);
            // 3 The remaining space may be any value
            // Here the length of the specially set maxBlank
            final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
            byteBuffer.put(this.msgStoreItemMemory.array(), 0, maxBlank);
            return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgId, msgInner.getStoreTimestamp(),
                queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
        }
    
        // Initialization of storage space
        this.resetByteBuffer(msgStoreItemMemory, msgLen);
        // 1 TOTALSIZE
        this.msgStoreItemMemory.putInt(msgLen);
        // 2 MAGICCODE
        this.msgStoreItemMemory.putInt(CommitLog.MESSAGE_MAGIC_CODE);
        // 3 BODYCRC
        this.msgStoreItemMemory.putInt(msgInner.getBodyCRC());
        // 4 QUEUEID
        this.msgStoreItemMemory.putInt(msgInner.getQueueId());
        // 5 FLAG
        this.msgStoreItemMemory.putInt(msgInner.getFlag());
        // 6 QUEUEOFFSET
        this.msgStoreItemMemory.putLong(queueOffset);
        // 7 PHYSICALOFFSET
        this.msgStoreItemMemory.putLong(fileFromOffset + byteBuffer.position());
        // 8 SYSFLAG
        this.msgStoreItemMemory.putInt(msgInner.getSysFlag());
        // 9 BORNTIMESTAMP
        this.msgStoreItemMemory.putLong(msgInner.getBornTimestamp());
        // 10 BORNHOST
        this.resetByteBuffer(hostHolder, 8);
        this.msgStoreItemMemory.put(msgInner.getBornHostBytes(hostHolder));
        // 11 STORETIMESTAMP
        this.msgStoreItemMemory.putLong(msgInner.getStoreTimestamp());
        // 12 STOREHOSTADDRESS
        this.resetByteBuffer(hostHolder, 8);
        this.msgStoreItemMemory.put(msgInner.getStoreHostBytes(hostHolder));
        //this.msgBatchMemory.put(msgInner.getStoreHostBytes());
        // 13 RECONSUMETIMES
        this.msgStoreItemMemory.putInt(msgInner.getReconsumeTimes());
        // 14 Prepared Transaction Offset
        this.msgStoreItemMemory.putLong(msgInner.getPreparedTransactionOffset());
        // 15 BODY
        this.msgStoreItemMemory.putInt(bodyLength);
        if (bodyLength > 0)
            this.msgStoreItemMemory.put(msgInner.getBody());
        // 16 TOPIC
        this.msgStoreItemMemory.put((byte) topicLength);
        this.msgStoreItemMemory.put(topicData);
        // 17 PROPERTIES
        this.msgStoreItemMemory.putShort((short) propertiesLength);
        if (propertiesLength > 0)
            this.msgStoreItemMemory.put(propertiesData);
    
        final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
        // Write messages to the queue buffer
        byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen);
    
        AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, msgLen, msgId,
            msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
    
        switch (tranType) {
            case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
            case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
                break;
            case MessageSysFlag.TRANSACTION_NOT_TYPE:
            case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
                // The next update ConsumeQueue information
                CommitLog.this.topicQueueTable.put(key, ++queueOffset);
                break;
            default:
                break;
        }
        return result;
    }
    

      

  1. mapperFile提交

      提交
    public int commit(final int commitLeastPages) {
        if (writeBuffer == null) {
            //no need to commit data to file channel, so just regard wrotePosition as committedPosition.
            return this.wrotePosition.get();
        }
        if (this.isAbleToCommit(commitLeastPages)) {
            if (this.hold()) {
                commit0(commitLeastPages);
                this.release();
            } else {
                log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
            }
        }
    
        // All dirty data has been committed to FileChannel.
        if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
            this.transientStorePool.returnBuffer(writeBuffer);
            this.writeBuffer = null;
        }
    
        return this.committedPosition.get();
    }
    
    protected void commit0(final int commitLeastPages) {
        int writePos = this.wrotePosition.get();
        int lastCommittedPosition = this.committedPosition.get();
    
        if (writePos - this.committedPosition.get() > 0) {
            try {
                ByteBuffer byteBuffer = writeBuffer.slice();
                byteBuffer.position(lastCommittedPosition);
                byteBuffer.limit(writePos);
                this.fileChannel.position(lastCommittedPosition);
                this.fileChannel.write(byteBuffer);
                this.committedPosition.set(writePos);
            } catch (Throwable e) {
                log.error("Error occurred when commit data to FileChannel.", e);
            }
        }
    }
    
    private boolean isAbleToFlush(final int flushLeastPages) {
        int flush = this.flushedPosition.get();
        int write = getReadPosition();
    
        if (this.isFull()) {
            return true;
        }
    
        if (flushLeastPages > 0) {
            return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= flushLeastPages;
        }
    
        return write > flush;
    }
    
    protected boolean isAbleToCommit(final int commitLeastPages) {
        int flush = this.committedPosition.get();
        int write = this.wrotePosition.get();
    
        if (this.isFull()) {
            return true;
        }
    
        if (commitLeastPages > 0) {
            return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= commitLeastPages;
        }
    
        return write > flush;
    }
 
  1. 消息文件的持久化方式--刷盘机制

      RocketMQ的存储与读写是基于JDK NIO的内存映射机制(MappedByteBuffer)的,消息存储时首先将消息追加到内存中,再根据配置的刷盘策略在不同时间刷盘。如果是同步刷盘,消息追加到内存后,将同步调用MappedByteBuffer的force()方法;如果是异步刷盘,在消息追加到内存后会立刻返回给消息发送端。RocketMQ使用一个单独的线程按照某一个设定的频率执行刷盘操作。通过在broker配置文件中配置flushDiskType来设定刷盘方式,可选值为ASYNC_FLUSH(异步刷盘)、SYNC_FLUSH(同步刷盘),默认为异步刷盘。
  1. 同步刷盘

同步刷盘指的是在消息追加到内存映射文件的内存中后,立即将数据从内存写入磁盘文件。

image

 

public void handleDiskFlush(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {
    // Synchronization flush
if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
        final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
        if (messageExt.isWaitStoreMsgOK()) {
            GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
            service.putRequest(request);
            boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
            if (!flushOK) {
                log.error("do groupcommit, wait for flush failed, topic: " + messageExt.getTopic() + " tags: " + messageExt.getTags()
                    + " client address: " + messageExt.getBornHostString());
                putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
            }
        } else {
            service.wakeup();
        }
    }
    // Asynchronous flush 异步刷盘
else {
        if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
            flushCommitLogService.wakeup();
        } else {
            commitLogService.wakeup();
        }
    }
}

  

  1. 异步刷盘

      异步刷盘指的是broker将消息存储到pagecache后就立即返回成功,然后开启一个异步线程定时执行FileChannel的force方法,将内存中的数据定时写入磁盘,默认间隔时间为500ms。
时序图展示了两种异步刷盘场景,其核心区别在于是否使用了堆外内存池(TransientStorePool)。
  1. 开启堆外内存池(场景一)
    1. 路径:消息先写入堆外内存,再由 CommitRealTimeService 线程提交到 FileChannel,最后由 FlushRealTimeService 线程刷盘。
    2. 优点:
      • 写入内存的速度远快于直接写入文件映射的PageCache,即使发生GC(垃圾收集)停顿也不会影响页面缓存,性能更高。
      • 内存级别的读写分离机制,即写入消息时主要面对堆外内存,而读取消息(消息消费)时主要面对pagecache
      • 该机制使消息直接写入堆外内存,然后异步写入pagecache,相比每条消息追加直接写入pagechae,最大的优势是实现了批量化消息写入
    3. 缺点:在极端情况下(如断电),尚未提交到FileChannel的消息会丢失。
  2. 未开启堆外内存池(场景二,默认模式)
    1. 路径:消息直接写入 MappedFile 对应的 MappedByteBuffer(即操作系统的PageCache),然后由 FlushRealTimeService 线程定期刷盘。
    2. 特点:这是RocketMQ默认的模式,在性能和可靠性之间取得了一个平衡。
  1. 刷盘取舍

      同步刷盘的优点是能保证消息不丢失,即向客户端返回成功就代表这条消息已被持久化到磁盘,但这是以牺牲写入性能为代价的,不过因为RocketMQ的消息是先写入pagecache,所以消息丢失的可能性较小,如果能容忍一定概率的消息丢失或者在丢失后能够低成本的快速重推,可以考虑使用异步刷盘策略。
  1. 总结

  1. 消息存储为什么那么高效

  1. 顺序写入机制

  • CommitLog 采用顺序追加写入
  • 避免磁盘随机IO,提升吞吐量
  • 单个文件默认1GB,写满后创建新文件
  1. 内存映射技术

  • 使用 MappedByteBuffer 内存映射文件
  • 充分利用 OS PageCache
  • 减少用户态与内核态数据拷贝
  1. 刷盘策略

  • 同步刷盘(高可靠)FlushDiskType = SYNC_FLUSH
  • 异步刷盘(高性能) FlushDiskType = ASYNC_FLUSH
  1. 索引构建流程

消息存储 → CommitLog → 异步构建 → ConsumeQueue → 消费者读取
IndexFile → 按Key查询
  1. 文件组织方式

store/
├── commitlog/
│ └── 00000000000000000000 # CommitLog文件
├── consumequeue/
│ └── TopicA/
│ └── 0/00000000000000000000 # 消费队列文件
└── index/
└── 00000000000000000000 # 索引文件
这种存储架构保证了 RocketMQ 的高性能、高可靠性和可扩展性。
 
posted @ 2025-11-12 16:40  一个小小小码农  阅读(0)  评论(0)    收藏  举报