消息存储

158 阅读5分钟

总体介绍

  1. RocketMQ存储概要设计
  2. 消息发送存储流程
  3. 存储文件组织与内存映射机制
  4. RocketMQ存储文件
  5. 消息消费队列、索引文件构建机制
  6. 文件恢复机制
  7. 刷盘机制
  8. 文件删除机制

概要设计

RocketMQ 主要存储的文件包括 Comitlog 文件、ConsumeQueue 文件、IndexFile 文件。并且单个文件都被设计为固定长度。创建新文件的时候,文件名就是第1条消息对应的的全局物理偏移量

CommitLog:消息存储文件,所有消息主题的消息都存储在 CommitLog 文件中。

ConsumerQueue:消息消费队列,消息到达 CommitLog 文件后,将异步转发到消息消息队列,供消息消费者消费。每个消息队列都有一个消息文件。

IndexFile:消息索引文件,主要存储消息key与Offset的对应关系,加速消息的检索性能,根据消息的属性快速从Comitlog 文件中检索消息

事务状态服务:存储每条消息的事务状态

定时消息服务:每一个延迟级别对应一个消息消费队列,存储延迟队列的消息拉取进度。

image.png

消息存储类

image.png

消息发送存储流程

消息存储入口:org.apache.rocketmq.store.DefaultMessageStore#putMessage

Step 1:如果当前 Broker 停止工作或 Broker 为 SLAVE 角色或当前 RocketMQ 不支持写入,则拒绝消息写入;如果消息主题长度超过 256 个字符消息属性长度超过 65536 个字符将拒绝消息写入。

Step 2:如果消息的延迟等级大于 0,将消息的原主题名称,原消息队列ID存入消息属性中,用延迟消息主题 SCHEDULE_TOPIC、消息队列 ID 更新原先消息的主题与队列。这是并发消息消费重试关键的一步

Step 3:获取当先可以写入的 Commitlog 文件。CommitLog 文件存储目录为 ${ROCKET_HOME}/store/commitlog 目录,每一个文件默认 1G,一个文件写满后再创建另外一个,以该文件中第一个偏移量为文件名,偏移量小于 20 位用 0 补齐。MappedFileQueue看作是commitlog文件夹,MappedFile看作是文件夹下一个个的文件。

image.png

Step 4:在写入 CommitLog 之前,先申请 putMessageLock,消息存储到 CommitLog 文件是串行的。

Step 5:设置消息的存储时间。

Step 6:将消息追加到 MappedFile 中。获取MappedFile文件的当前写指针curPos,curPos>=fileSize就抛出错误,否则通过slice()方法创建一个与MappedFile共享的内存区

Step 7:创建全局唯一消息 ID,消息 ID 有 16 字节。

image.png

Step 8:获取该消息在消息队列的偏移量。

Step 9:根据消息体的长度、主题的长度、属性的长度结合消息存储格式计算消息的总长度。

Step 10:如果消息长度 + END_FILE_MIN_BLANK_LENGTH 大于 CommitLog 文件的空闲空间,则返回 AppendMessageStatus.END_OF_FILE,Broker 会重新创建一个新的 CommitLog 文件来存储该消息。

Step 11:将消息 内容存储到 ByteBuffer 中,然后创建 AppendMessageResult。

Step 12:更新消息队列逻辑偏移量。

Step 13:处理完消息追加逻辑后将释放 putMessageLock 锁。

Step 14:DefalutAppendMessageCallback#doAppend 只将消息追加到内存中,需要根据同步刷盘或异步刷盘方式,将内存的数据持久化到磁盘。

存储文件组织与内存映射

RocketMQ 通过内存映射文件来提高 IO 访问性能。

MappedFileQueue 映射文件队列

MappedFileQueue 是 MappedFile 的管理容器,MappedFileQueue 是对存储目录的封装。

image.png

查找MappedFile

image.png

/**
 * 根据消息存储时间戳查找MappedFile
 * @param timestamp
 * @return
 */
public MappedFile getMappedFileByTime(final long timestamp) {
    // 从MappedFile列表第一个文件开始查找
    Object[] mfs = this.copyMappedFiles(0);

    if (null == mfs)
        return null;

    for (int i = 0; i < mfs.length; i++) {
        MappedFile mappedFile = (MappedFile) mfs[i];
        // 找到第一个最后一次的更新时间大于等于待查找时间戳的文件
        if (mappedFile.getLastModifiedTimestamp() >= timestamp) {
            return mappedFile;
        }
    }

    // 如果不存在,返回最后一个MappedFile文件
    return (MappedFile) mfs[mfs.length - 1];
}

public long getMinOffset() {

    if (!this.mappedFiles.isEmpty()) {
        try {
            // 返回第一个文件的初始偏移量,在存储文件列表,第一个文件不一定是0000
            return this.mappedFiles.get(0).getFileFromOffset();
        } catch (IndexOutOfBoundsException e) {
            //continue;
        } catch (Exception e) {
            log.error("getMinOffset has exception.", e);
        }
    }
    return -1;
}

public long getMaxOffset() {
    MappedFile mappedFile = getLastMappedFile();
    if (mappedFile != null) {
        // 返回最后一个MappedFile文件的初始偏移量+文件当前读指针
        return mappedFile.getFileFromOffset() + mappedFile.getReadPosition();
    }
    return 0;
}

public long getMaxWrotePosition() {
    MappedFile mappedFile = getLastMappedFile();
    if (mappedFile != null) {
        // 返回最后一个MappedFile文件的初始偏移量+文件当前写指针
        return mappedFile.getFileFromOffset() + mappedFile.getWrotePosition();
    }
    return 0;
}

MappedFile 内存映射文件

MappedFile 是 RocketMQ 内存映射文件的具体实现。

image.png

image.png

MappedFile

TransientStorePool

RocketMQ 引入该机制主要的原因是提供一种内存锁定,将当前对外内存锁定在内存中,避免被进程将内存交换到磁盘

TransientStorePool,短暂的存储池

  • poolSize:availableBuffers 个数,可通过在broker中配置文件中设置 transientStorePool,默认值为 5
  • fileSize:每个 ByteBuffer 大小,默认为 mappedFileSizeCommitLog,表明 TransientStorePool 为 commitlog 文件服务
  • availableBuffers:直接内存,ByteBuffer 容器,双端队列
/**
 * 创建默认的堆外内存
 * It's a heavy init method.
 */
public void init() {
    // 创建poolSize个堆外内存
    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);
    }
}

image.png

RocketMQ 存储文件

RocketMQ 存储路径为 ${ROCKET_HOME}/store

commitlog:存储消息目录

config:运行期间一些配置信息

  • consumerFilter.json:主题消息过滤信息
  • consumerOffset.json:集群消费模式消息消费进度
  • delayOffset.json:延迟消息队列拉取进度
  • subscriptionGroup.json:消息消费组配置信息
  • topics.json:topic配置属性

consumequeue:消息消息队列存储目录

index:消息索引文件存储目录

abort:如果存在 abort 文件说明 Broker 非正常关闭

checkpoint:文件检查点,存储commitlog最后一次刷盘时间戳,consumequeue最后一次刷盘时间,index索引文件最后一次刷盘时间戳。

CommitLog文件

image.png

消息的查找实现

根据偏移量与消息长度查找消息

首先根据偏移量找到所在的物理文件,然后用offset与文件长度取余得到在文件内的偏移量,从该偏移量读取Size长度的内容返回即可。 如果只根据消息偏移查找消息,则首先找到文件内的偏移量,然后尝试读取4个字节获取消息的实际长度,最后读取指定字节即可。

ConsumeQueue文件

CommitLog关于消息消费的索引文件 consume第一级目录是消息主题,第二级目录是主题的队列 为了增加Consume的消息检索速度和节省磁盘空间,每个consumequeue不会存储消息的全量信息。

image.png

image.png

单个ConsueQueue文件默认包含30w个条目,单个文件长度为30w x 20字节。一个文件可以看为是ConsueQueue条目数组。

ConsumeQueue文件:ConsumeQueue条目 数组 ConsumeQueue条目:针对单个消息的存储

  • 构建 :当消息到达CommitLog文件后,由专门的线程产生消息转发任务,从而构建消息消费队列和索引文件

Index索引文件

Hash索引文件

image.png

checkpoint文件

  • physicMsgTimestamp:commitlog文件刷盘时间点
  • logicsMsgTimestamp:消息消费队列文件刷盘时间点
  • indexMsgTimestamp:索引文件刷盘时间点

image.png

实时更新消息消费队列文件和索引文件

文件刷盘机制

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

过期文件删除机制

RocketMQ 清除过期文件的方式是:如果非当前写文件在一定时间间隔内没有再次被更新,则认为是过期文件,可以被删除,RocketMQ 不会关注这个文件上的消息是否全部被消费。默认每个文件的过期时间为 72 小时,通过在 Broker 配置文件中设置 fileReservedTime 来改变过期时间,单位是小时。

RocketMQ 会每隔 10s 调度一次 cleanFilesPeriodically,检测是否需要清除过期文件。

RocketMQ 在如下三种情况任意之一满足的情况下将继续执行三处文件操作:

1)指定删除文件的时间点,RocketMQ 通过 deleteWhen 设置一天的固定时间执行一次删除过期文件操作,默认是凌晨 4 点。

2)磁盘空间是否充足,如果磁盘空间不充足,则返回 true,表示应该出发过期文件的删除操作。

3)预留,人工触发,可以通过调用 executeDeleteFilesmanualy 方法手动出发过期文件删除。

参考