RocketMQ之消息存储

1.  存储概要设计  

    

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

    2.  ConsumeQueue  消息消费队列,消息到达CommitLog文件后,将异步转发到消息消费队列,供消息消费者消费

    3.  IndexFile  消息索引文件,主要存储消息key与Offset的对应关系

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

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

2.  消息发送存储流程

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

    2.  如果消息的延迟级别大于0,将消息的原主题名称与原消息队列ID存入消息属性中,用延迟消息主题SCHEDULE_TOPIC,消息队列ID更新原先消息的主题与队列。

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

    4.  在写入CommitLog之前,先申请putMessageLock,也就是将消息存储到CommitLog文件中是串行的。

    5.  设置消息的存储时间,如果mappedFile为空,表明${ROCKET_HOME}/store/commitlog目录下不存在任何文件,说明本次消息是第一次消息发送,用偏移量0创建第一个commit文件,如果文件常见失败,抛出CREATE_MAPPEDFILE_FAILED,很有可能是磁盘空间不足或权限不足。

    6.  将消息追加到MappedFile中。首先先获取MappedFile当前写指针,如果currentPos大于或等于文件大小则表明文件已写满,抛出AppendMessageStatus.UNKNOWN_ERROR。如果currentPos小于文件大小,通过slice()方法创建一个与MappedFile的共享内存区,并设置position为当前指针。

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

      

    8.  获取该消息在消息队列的偏移量。CommitLog中保存了当前所有消息队列的当前待写入偏移量。

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

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

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

    12.  更新消息队列逻辑偏移量

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

    14.  DefaultAppendMessageCallback只是将消息追加在内存中,需要根据是同步刷盘还是异步刷盘方式,将内存中的数据持久化到磁盘   

3.  存储文件组织与内存映射 

    1.  MappedFileQueue映射文件队列

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

    2.  MappedFile内存映射文件

4.  RocketMQ存储文件

    存储路径  ${ROCKET_HOME}/store 

    1.  主要目录

        commitlog  消息存储目录

        config  运行期间一些配置信息

          consumerFilter.json  主题消息过滤信息

          consumerOffset.json  集群消费模式消息消费进度

          delayOffset.json  延时消息队列拉取进度

          subscriptionGroup.json  消息消费组配置信息

          topics.json  topic配置属性

        consumequeue  消息消费队列存储目录

        index  消息索引文件存储目录

        abort  如果存在该目录说明Broker非正常关闭

        checkpoint  文件检测点,存储commitlog文件最后一次刷盘时间戳,consumerqueue最后一次刷盘时间,index索引文件最后一次刷盘时间戳

    2.  Commitlog文件

        Commitlog文件的存储目录为${ROCKET_HOME}/store/Commitlog,可以通过在broker配置文件中设置storePathRootDir属性来改变默认路径

        Commitlog文件默认大小为1G,可通过在broker配置文件中设置mapedFileSizeCommitLog属性来改变默认大小

    3.  ConsumeQueue文件啊

        由于同一主题的消息不连续地存储在commitlog文件中,RocketMQ为了提高检索效率,设计了消息消费队列,该文件可以看作是commitlog的索引文件。

        consumequeue的第一级目录为消息这祖逖,第二级目录为主题的消息队列。

        单个consumequeue文件中默认包含30万个条目,单个文件的长度为30w*20字节

    4.  Index索引文件

        为消息建立索引        

        

    5.  checkpoint文件

        记录commitlog,consumequeue,index文件的刷盘时间点,文件固定长度为4k,其中只用该文件的前面24个字节

5.  实时更新消息消费队列与索引文件

    当消息生产者提交的消息存储在commitlog文件中,consumequeue,indexfile需要及时更新,否则消息无法及时被消费,根据消息属性查找消息也会出现较大延迟。

    RocketMQ通过开启一个线程ReputMessageService来准实时转发commitlog文件更新时间,相应的任务处理器根据转发的消息及时更新consumequeue,indexfile文件。

6.  消息队列与索引文件恢复

    由于RocketMQ存储首先将消息全量存储在commitlog文件中,然后异步生成转发任务更新consumequeue,index文件。如果消息成功存储到commitlog文件中,转发任务未成功执行, 

    1.  存储文件的加载流程

        1.  判断上一次退出是否正常。根据abort文件,如果存在,说明broker是异常退出的,commitlog与consumequeue数据有可能不一致,需要进行修复。

        2.  加载延迟队列

        3.  加载commitlog文件

        4.  加载消息消费队列

        5.  加载存储检测点 

        6.  加载索引文件

        7.  根据broker是否正常停止执行不同的恢复策略

        8.  恢复consumequeue文件后,将在commitlog实例中保存每个消息消费队列当前的存储逻辑偏移量。

    2.  broker正常停止文件恢复

        1.  broker正常停止再重启时,从倒数第三个文件开始进行恢复,如果不足3个文件,则从第一个文件开始恢复。 

        2.  遍历commitlog文件,每次取出一条消息

        3.  删除offset之后的所有文件 

    2.  broker异常停止文件恢复

        1.  首先判断文件的魔数,如果不是MESSAGE_MAGIC_CODE,返回false,表示该问价你不符合commitlog消息文件的存储格式

        2.  如果文件中第一条消息的存储时间等于0,返回false,说明该消息存储文件中未存储任何消息

        3.  对比文件第一条信息的时间戳与检测点,文件第一条信息的时间戳小于文件检测点说明该文件部分消息是可靠的,则从该文件开始恢复

        4.  如果根据前3步算法找到mappedfile,则遍历mappedfile中的消息,验证消息的合法性,并将消息重新转发到消息消费队列与索引文件

        5.  如果未找到有效mappedfile,则设置commitlog目录的flushedwhere,commited-where指针都为0,并销毁消息消费队列文件

7.  文件刷盘机制

    消息存储时首先将消息追加到内存,再根据配置的刷盘策略在不同时间进行刷写磁盘。

    如果是同步刷盘,消息追加到内存后,将同步调用mappedbyteBuffer的force()方法

    如果是异步刷盘,在消息追加到内存后立刻返回给消息发送端

    RocketMQ默认是异步刷盘

    1.  同步刷盘

        1.  构建GroupCommitRequest同步任务并提交到GroupCommitRequest

        2.  等待同步刷盘任务完成,如果超时则返回刷盘错误,刷盘成功后正常返回给调用方。        

    2.  异步刷盘

        异步刷盘根据是否开启transientStorePoolEnable机制,刷盘实现会有细微差别。如果为true,RocketMQ会单独申请一个与目标物理文件同样大小的堆外内存,该堆外内存将使用内存锁定,确保不会被置换到虚拟内存中,消息首先追加到堆外内存,然后提交到与物理文件的内存映射中,再flush到磁盘。如果为false,消息直接追加到与物理文件直接映射的内存中,然后刷到磁盘中。                                                                                                      

8.  过期文件删除机制

    如果当前写文件在一定时间间隔内没有再次被更新,则认为是过期文件,可以被删除,RocketMQ不会关注在这个文件上的消息是否全部被消费。

    默认每个文件的过期时间为72小时,通过在Broker配置文件中设置fileReservedTime来改变过期时间,单位为小时。

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

                                    

posted @ 2024-01-23 17:31  奋斗史  阅读(319)  评论(0)    收藏  举报