RocketMQ刷盘策略深层解析

205 阅读9分钟

RocketMQ刷盘策略深层解析

RocketMQ是一款高性能的分布式消息采集和传递系统,它的资料持久化和消息记录模块和应用解决了传统消息系统的底层性能和做应用实时性问题。在资料持久化过程中,刷盘策略是其核心问题之一,对消息的整体性能和数据安全性起到关键作用。本文将对RocketMQ的刷盘策略及相关核心组件进行进一步分析和讨论,以提升对这一模块的理解。

一、刷盘策略概述

在RocketMQ中,消息会被先写入到内存缓冲区(也称为PageCache),随后通过刷盘操作写入磁盘以持久化。刷盘策略相对存在两种:

  1. 同步刷盘(Synchronous Flush) :每次将消息写入磁盘前,主端会等待刷盘操作完成,以确保数据的安全性。
  2. 异步刷盘(Asynchronous Flush) :将消息写入到内存缓冲区后,即解放主端,通过后程打包并写入磁盘,优化性能。

二、核心组件分析

RocketMQ的持久化依赖多个核心组件,包括CommitLogMappedFileMappedFileQueueIndexFileCheckpointConsumeQueueDefaultMessageStore。它们共同组成了高效可靠的消息存储机制。

1. CommitLog

CommitLog是RocketMQ消息存储的核心组件,所有消息都会被顺序写入到CommitLog文件中。

数据流
  • 顺序写保证:RocketMQ通过内存映射技术(MappedByteBuffer)将磁盘文件映射到内存空间,并且始终按照写入的顺序追加到文件末尾,确保写入操作是顺序的。这样避免了随机写带来的磁盘寻址开销,大幅提高性能。
  • 分片文件管理:CommitLog文件通过MappedFileQueue按固定大小(默认1GB)切分,避免单文件过大造成管理困难。
  • 刷盘控制:消息写入后,根据刷盘策略(同步或异步)决定何时将数据写入磁盘。
源码分析

CommitLog中实现了消息存储的主要逻辑,例如:

public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
    MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
    AppendMessageResult result = mappedFile.appendMessage(msg, this.appendMessageCallback);
    switch (this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
        case SYNC_FLUSH:
            handleSyncFlush(result);
            break;
        case ASYNC_FLUSH:
            handleAsyncFlush(result);
            break;
    }
    return new PutMessageResult(PutMessageStatus.PUT_OK, result);
}
  • MappedFileQueue负责管理文件分片。
  • AppendMessageCallback封装了消息写入逻辑。
  • 根据刷盘类型执行同步或异步刷盘。
顺序写的核心机制

RocketMQ的顺序写通过MappedFileappendMessage方法实现,具体代码如下:

public AppendMessageResult appendMessage(final MessageExt message, final AppendMessageCallback cb) {
    ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
    AppendMessageResult result = cb.doAppend(this.fileFromOffset, byteBuffer, this.fileSize, message);
    this.wrotePosition.addAndGet(result.getWroteBytes());
    return result;
}
  1. MappedByteBuffer.slice():创建一个新的缓冲区,与原有的内存映射区域共享内存。
  2. doAppend:将消息追加到内存映射区域的末尾。
  3. wrotePosition:更新当前文件的写入位置,确保后续消息能正确追加。

2. MappedFile

MappedFile是RocketMQ中最基本的文件存储单元,每个文件大小固定(默认为1GB),支持内存映射方式加速I/O操作。

核心逻辑
  1. 内存映射机制:通过MappedByteBuffer将磁盘文件映射到内存。
  2. 文件切分:支持按固定大小分片,避免单文件过大。
  3. 异步刷盘优化:在异步刷盘策略下,使用后台线程定期将数据从内存刷入磁盘。
源码分析

MappedFile的核心方法:

public AppendMessageResult appendMessage(final MessageExt message, final AppendMessageCallback cb) {
    ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
    AppendMessageResult result = cb.doAppend(this.fileFromOffset, byteBuffer, this.fileSize, message);
    this.wrotePosition.addAndGet(result.getWroteBytes());
    return result;
}
  • 使用MappedByteBuffer避免传统文件I/O中的多次内存拷贝。
  • 通过wrotePosition维护当前文件的写入位置。

3. MappedFileQueue

MappedFileQueueMappedFile的管理器,负责维护文件列表并分配新的文件。

数据流
  • 文件分片管理:消息写入时,MappedFileQueue会根据当前写入位置找到对应的文件分片。
  • 自动扩展:当当前文件写满时,分配一个新的MappedFile用于后续写入。
源码分析

MappedFileQueue的文件管理逻辑:

public MappedFile getLastMappedFile(final long startOffset) {
    MappedFile mappedFile = this.getLastMappedFile();
    if (mappedFile == null || mappedFile.isFull()) {
        mappedFile = this.createMappedFile(startOffset);
        this.mappedFiles.add(mappedFile);
    }
    return mappedFile;
}
  • 自动分配新文件,确保写入连续性。
  • 通过isFull判断当前文件是否已写满。

4. IndexFile

IndexFile负责消息的索引管理,通过消息的keyoffset快速定位到CommitLog中的具体消息。

核心功能
  1. 消息存储位置索引:提供key到offset的映射。
  2. 快速查询:加速消息检索,避免全量扫描。
源码分析

IndexFile的查询实现:

public QueryOffsetResult queryOffset(final String key, final long beginTime, final long endTime) {
    int keyHash = hash(key);
    Slot slot = this.slotTable.get(keyHash);
    for (int i = slot.index; i != -1; i = this.indexTable[i].nextIndex) {
        if (this.indexTable[i].matches(key, beginTime, endTime)) {
            return new QueryOffsetResult(this.indexTable[i].offset);
        }
    }
    return null;
}
  • 通过hash表定位索引。
  • 遍历冲突链,找到符合条件的记录。

5. Checkpoint

Checkpoint组件是RocketMQ中用于记录刷盘和消费进度的元数据文件,确保在Broker重启时能够正确恢复。

核心功能
  1. 持久化刷盘进度:记录CommitLogConsumeQueue的刷盘时间点。
  2. 恢复机制:在Broker启动时,通过Checkpoint中的记录恢复最后一次刷盘的位置。
源码分析

Checkpoint的刷盘记录逻辑:

public void writeCheckpoint() {
    this.lock.lock();
    try {
        this.mappedByteBuffer.putLong(0, this.physicMsgTimestamp);
        this.mappedByteBuffer.putLong(8, this.logicsMsgTimestamp);
        this.mappedByteBuffer.putLong(16, this.indexMsgTimestamp);
        this.mappedByteBuffer.force();
    } finally {
        this.lock.unlock();
    }
}
  • 记录物理消息、逻辑消息以及索引文件的刷盘时间戳。
  • 强制将元数据写入磁盘,确保刷盘进度持久化。

在Broker启动时,Checkpoint会被加载:

public void load() {
    this.physicMsgTimestamp = this.mappedByteBuffer.getLong(0);
    this.logicsMsgTimestamp = this.mappedByteBuffer.getLong(8);
    this.indexMsgTimestamp = this.mappedByteBuffer.getLong(16);
}
  • 通过读取时间戳恢复刷盘进度。

6. ConsumeQueue

ConsumeQueue是RocketMQ中的消息消费队列,主要用于为消息消费者提供高效的消息检索功能。

核心功能
  1. 消息索引:ConsumeQueue为每个主题和消费队列生成对应的索引文件,记录消息的物理偏移量、消息大小和标志位。
  2. 快速定位:通过ConsumeQueue,消费者可以快速找到目标消息在CommitLog中的位置。
  3. 分片管理:ConsumeQueue使用固定大小(默认为30W条消息)的分片文件,避免单文件过大。
数据流
  1. 写入流程:当消息被写入CommitLog后,异步线程会根据消息的主题和队列ID生成对应的ConsumeQueue索引。
  2. 读取流程:消费者根据主题和队列ID从ConsumeQueue中读取偏移量,然后从CommitLog加载对应的消息内容。
源码分析

ConsumeQueue的写入逻辑:

public void putMessagePositionInfo(final long offset, final int size, final long tagsCode, final long storeTimestamp) {
    ByteBuffer byteBuffer = this.mappedFile.sliceByteBuffer();
    byteBuffer.putLong(offset);
    byteBuffer.putInt(size);
    byteBuffer.putLong(tagsCode);
    this.mappedFile.wrotePosition.addAndGet(CQ_STORE_UNIT_SIZE);
}
  1. 偏移量记录offset表示消息在CommitLog中的物理偏移量。
  2. 消息大小size记录消息的字节大小。
  3. 标签哈希tagsCode用于消息过滤。

读取逻辑:

public SelectMappedBufferResult getIndexBuffer(final long startIndex) {
    int pos = (int) ((startIndex % this.mappedFileSize) * CQ_STORE_UNIT_SIZE);
    ByteBuffer byteBuffer = this.mappedFile.selectMappedBuffer(pos);
    return new SelectMappedBufferResult(pos, byteBuffer);
}
  1. 根据消费偏移计算目标位置。
  2. 从分片文件中加载对应的索引信息。
优化点
  1. 批量写入:ConsumeQueue使用异步线程批量写入索引,提升性能。
  2. 高效查询:通过固定大小的记录结构,ConsumeQueue实现了O(1)的索引查询效率。

7. DefaultMessageStore

DefaultMessageStore是RocketMQ中消息存储的核心实现类,负责管理CommitLogConsumeQueue等组件,协调它们的协同工作。

核心功能
  1. 消息存储管理:协调CommitLogConsumeQueue的读写操作。
  2. 刷盘控制:通过调度线程控制CommitLogConsumeQueue的刷盘操作。
  3. 文件清理:定期清理过期的CommitLogConsumeQueue文件,释放磁盘空间。
  4. HA同步:在主从架构下,负责主从之间的消息同步。
源码分析

DefaultMessageStore的初始化:

public DefaultMessageStore(final MessageStoreConfig messageStoreConfig, 
                           final BrokerStatsManager brokerStatsManager,
                           final Message 

继续补充 DefaultMessageStore 类的深入解析和源码分析。


源码分析(续)

DefaultMessageStore 的构造函数和关键成员初始化:

public DefaultMessageStore(final MessageStoreConfig messageStoreConfig, 
                           final BrokerStatsManager brokerStatsManager,
                           final MessageArrivingListener messageArrivingListener, 
                           final BrokerConfig brokerConfig) throws IOException {
    this.messageArrivingListener = messageArrivingListener;
    this.brokerConfig = brokerConfig;
    this.messageStoreConfig = messageStoreConfig;
    this.brokerStatsManager = brokerStatsManager;
    this.allocateMappedFileService = new AllocateMappedFileService(this);

    if (messageStoreConfig.isEnableDLegerCommitLog()) {
        this.commitLog = new DLedgerCommitLog(this);
    } else {
        this.commitLog = new CommitLog(this);
    }
    
    this.consumeQueueTable = new ConcurrentHashMap<>(32);
    this.flushConsumeQueueService = new FlushConsumeQueueService();
    this.cleanCommitLogService = new CleanCommitLogService();
    this.cleanConsumeQueueService = new CleanConsumeQueueService();
    this.indexService = new IndexService(this);

    if (!messageStoreConfig.isEnableDLegerCommitLog()) {
        this.haService = new HAService(this);
    } else {
        this.haService = null;
    }
    this.reputMessageService = new ReputMessageService();
    this.scheduleMessageService = new ScheduleMessageService(this);
    this.transientStorePool = new TransientStorePool(messageStoreConfig);

    if (messageStoreConfig.isTransientStorePoolEnable()) {
        this.transientStorePool.init();
    }

    this.allocateMappedFileService.start();
    this.indexService.start();
    
    this.dispatcherList = new LinkedList<>();
    this.dispatcherList.addLast(new CommitLogDispatcherBuildConsumeQueue());
    this.dispatcherList.addLast(new CommitLogDispatcherBuildIndex());
}
  • 核心成员变量:

    • CommitLog:消息的核心存储组件。
    • ConsumeQueueTable:存储所有主题和队列对应的消费队列。
    • IndexService:用于构建消息索引。
    • ReputMessageService:负责从CommitLog中转发消息到消费队列和索引。
    • TransientStorePool:用于高性能数据写入的直接内存池。
    • AllocateMappedFileService:负责分配新的MappedFile

消息存储的核心流程
  1. 消息写入: 消息通过DefaultMessageStore进入,最终被写入到CommitLog和对应的ConsumeQueue

    • 调用putMessage方法写入单条消息。
    • 异步构建消费队列和索引文件。
  2. 刷盘

    • FlushConsumeQueueService 定期刷盘ConsumeQueue
    • CommitLog由配置选择同步或异步刷盘。
  3. 消息转发(ReputMessageService)

    • CommitLog读取消息。
    • 将消息分发到ConsumeQueueIndexFile

ReputMessageService 深入解析

ReputMessageService 是一个后台线程,负责从CommitLog中读取消息并分发到消费队列和索引文件。


class ReputMessageService extends ServiceThread {
    private volatile long reputFromOffset = 0;

    public long getReputFromOffset() {
        return reputFromOffset;
    }

    public void setReputFromOffset(long reputFromOffset) {
        this.reputFromOffset = reputFromOffset;
    }

    private void doReput() {
        while (this.isCommitLogAvailable()) {
            SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
            if (result != null) {
                try {
                    for (int readSize = 0; readSize < result.getSize();) {
                        DispatchRequest dispatchRequest = DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(
                            result.getByteBuffer(), false, false);

                        int size = dispatchRequest.getBufferSize() == -1 
                                ? dispatchRequest.getMsgSize() 
                                : dispatchRequest.getBufferSize();
                        
                        if (dispatchRequest.isSuccess()) {
                            DefaultMessageStore.this.doDispatch(dispatchRequest);
                            this.reputFromOffset += size;
                            readSize += size;
                        } else {
                            this.reputFromOffset += size;
                        }
                    }
                } finally {
                    result.release();
                }
            }
        }
    }
}

流程:

  1. reputFromOffset 记录需要分发的起始偏移量。
  2. CommitLog获取消息,通过checkMessageAndReturnSize解析。
  3. 通过doDispatch将消息分发到ConsumeQueueIndexService

关键点:

  • 确保所有写入CommitLog的消息最终都会被消费队列和索引服务接收。
  • 处理事务消息,确保只分发已提交的消息。

DefaultMessageStore 的文件清理机制

为了避免磁盘空间耗尽,DefaultMessageStore 提供了定期清理机制:

  • CleanCommitLogService:清理过期的CommitLog文件。
  • CleanConsumeQueueService:清理过期的消费队列文件。
class CleanCommitLogService {
    private void deleteExpiredFiles() {
        long fileReservedTime = DefaultMessageStore.this.getMessageStoreConfig().getFileReservedTime();
        DefaultMessageStore.this.commitLog.deleteExpiredFile(fileReservedTime, ...);
    }
}

触发条件:

  1. 文件过期时间超过设定阈值。
  2. 磁盘使用率超过配置比例(如 75%)。

优化点:

  • 分批次删除,避免对性能产生过大影响。
  • 提供手动触发清理的能力。

TransientStorePool 的高性能写入机制

TransientStorePool 是 RocketMQ 用于提升写入性能的内存池,直接使用堆外内存加速数据写入。

  • 原理:

    • 申请固定大小的DirectByteBuffer,避免频繁的内存分配和GC。
    • 消息写入时,先写入堆外内存,再由异步线程刷盘到磁盘。
  • 实现:

    public class TransientStorePool {
        private final List<ByteBuffer> pool;
        
        public ByteBuffer borrowBuffer() {
            return this.pool.remove(0);
        }
        
        public void returnBuffer(ByteBuffer buffer) {
            this.pool.add(buffer);
        }
    }
    
  • 优点:

    1. 减少系统调用和数据拷贝。
    2. 提升高并发场景下的写入性能。

总结

RocketMQ 的刷盘策略和存储机制通过 CommitLogConsumeQueueIndexFileDefaultMessageStore 等模块紧密配合,形成了高效、可靠的数据存储系统。本文深入解析了关键组件和核心源码,帮助理解 RocketMQ 的存储原理与优化设计。