RocketMq存储篇(消息存储概要)

288 阅读6分钟

任何消息队列,消息存储的设计都是最核心、最重要的功能,存储的核心是IO的性能,而关于RocketMq, 以下就是这个模块的重要内容:

  • RocketMQ存储概要设计、消息发送存储流程

  • 存储文件组织与内存映射机制

  • RocketMQ存储文件

  • 消息消费队列、索引文件构建机和制

  • RocketMQ文件恢复机制

  • RocketMQ刷盘机制

  • RocketMQ文件删除机制

存储概要

RocketMQ主要存储的文件包括Comitlog文件、ConsumeQueue文件、IndexFile文件。RocketMQ将所有主题的消息存储在同一个文件中,确保消息发送时顺序写文件,尽最大的能力确保消息发送的高性能与高吞吐量。

但由于消息中间件一般是基于消息主题的订阅机制,这样便给按照消息主题检索消息带来了极大的不便。为了提高消息消费的效率,RocketMQ引入了ConsumeQueue消息队列文件,每个消息主题包含多个消息消费队列,每一个消息队列有一个消息文件

IndexFile索引文件,其主要设计理念就是为了加速消息的检索性能,根据消息的属性快速从Commitlog文件中检索消息。

image.png

  1. CommitLog:消息存储文件,所有消息主题的消息都存储在CommitLog文件中。
  2. ConsumeQueue:消息消费队列,消息到达CommitLog文件后,将异步转发到消息消费队列,供消息消费者消费。
  3. IndexFile:消息索引文件,主要存储消息Key与Offset的对应关系。
  4. 事务状态服务:存储每条消息的事务状态。

消息发送存储流程

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

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

如果日志中包含“message store is not writeable, so putMessage is forbidden”​,出现这种日志最有可能是磁盘空间不足,在写ConsumeQueue、IndexFile文件出现错误时会拒绝消息再次写入。

如下,就是校验存储状态的方法: image.png

如下,校验消息主题长度,消息属性长度的方法:

image.png

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

Step3:获取当前可以写入的Commitlog文件。

Commitlog文件存储目录为ROCKETHOME/store/commitlog目录,每一个文件默认1G,一个文件写满后再创建另外一个,以该文件中第一个偏移量为文件名,偏移量小于20位用0补齐。第一个文件初始偏移量为0,第二个文件的1073741824,代表该文件中的第一条消息的物理偏移量为1073741824,这样根据物理偏移量能快速定位到消息。MappedFileQueue可以看作是{ROCKET_HOME}/store/commitlog目录,每一个文件默认1G,一个文件写满后再创建另外一个,以该文件中第一个偏移量为文件名,偏移量小于20位用0补齐。第一个文件初始偏移量为0,第二个文件的1073741824,代表该文件中的第一条消息的物理偏移量为1073741824,这样根据物理偏移量能快速定位到消息。MappedFileQueue可以看作是{ROCKET_HOME}/store/commitlog文件夹,而MappedFile则对应该文件夹下一个个的文件。

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

putMessageLock有两种实现,一种是非公平模式下的ReentrantLock,一种是CAS的锁。 image.png

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

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

Step7:创建全局唯一消息ID,消息ID有16字, 消息ID组成但为了消息ID可读性,返回给应用程序的msgId为字符类型,可以通过UtilAll. bytes2string方法将msgId字节数组转换成字符串,通过UtilAll.string2bytes方法将msgId字符串还原成16个字节的字节数组,从而根据提取消息偏移量,可以快速通过msgId找到消息内容。

image.png

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

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

  1. TOTALSIZE:该消息条目总长度,4字节。
  2. MAGICCODE:魔数,4字节。固定值0xdaa320a7。
  3. BODYCRC:消息体crc校验码,4字节
  4. QUEUEID:消息消费队列ID,4字节。
  5. FLAG:消息FLAG, RocketMQ不做处理,供应用程序使用,默认4字节。
  6. QUEUEOFFSET:消息在消息消费队列的偏移量,8字节。
  7. PHYSICALOFFSET:消息在CommitLog文件中的偏移量,8字节。
  8. SYSFLAG:消息系统Flag,例如是否压缩、是否是事务消息等,4字节。
  9. BORNTIMESTAMP:消息生产者调用消息发送API的时间戳,8字节。
  10. BORNHOST:消息发送者IP、端口号,8字节。
  11. STORETIMESTAMP:消息存储时间戳,8字节。
  12. STOREHOSTADDRESS:Broker服务器IP+端口号,8字节。
  13. RECONSUMETIMES:消息重试次数,4字节。
  14. Prepared Transaction Offset:事务消息物理偏移量,8字节。
  15. BodyLength:消息体长度,4字节。
  16. Body:消息体内容,长度为bodyLenth中存储的值。
  17. TopicLength:主题存储长度,1字节,表示主题名称不能超过255个字符。
  18. Topic:主题,长度为TopicLength中存储的值。
  19. PropertiesLength:消息属性长度,2字节,表示消息属性长度不能超过65536个字符。
  20. Properties:消息属性,长度为PropertiesLength中存储的值

截取部分如下图:

image.png

Step10:如果消息长度+END_FILE_MIN_BLANK_LENGTH大于CommitLog文件的空闲空间,则返回AppendMessageStatus.END_OF_FILE, Broker会重新创建一个新的CommitLog文件来存储该消息。从这里可以看出,每个CommitLog文件最少会空闲8个字节,高4字节存储当前文件剩余空间,低4字节存储魔数:CommitLog.BLANK_MAGIC_CODE。

Step11:将消息内容存储到ByteBuffer中,然后创建AppendMessageResult。这里只是将消息存储在MappedFile对应的内存映射Buffer中,并没有刷写到磁盘。

  1. AppendMessageStatus status:消息追加结果,取值PUT_OK:追加成功;END_OF_FILE:超过文件大小;MESSAGE_SIZE_EXCEEDED:消息长度超过最大允许长度:PROPERTIES_SIZE_EXCEEDED:消息属性超过最大允许长度;UNKNOWN_ERROR:未知异常。
  2. long wroteOffset:消息的物理偏移量。
  3. String msgId:消息ID。
  4. long storeTimestamp:消息存储时间戳。
  5. long logicsOffset:消息消费队列逻辑偏移量,类似于数组下标。
  6. long pagecacheRT = 0:当前未使用。
  7. int msgNum = 1:消息条数,批量消息发送时消息条数。

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

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

Step14:DefaultAppendMessageCallback#doAppend只是将消息追加在内存中,需要根据是同步刷盘还是异步刷盘方式,将内存中的数据持久化到磁盘,关于刷盘操作后面会详细介绍。然后执行HA主从同步复制,主从同步