任何消息队列,消息存储的设计都是最核心、最重要的功能,存储的核心是IO的性能,而关于RocketMq, 以下就是这个模块的重要内容:
-
RocketMQ存储概要设计、消息发送存储流程
-
存储文件组织与内存映射机制
-
RocketMQ存储文件
-
消息消费队列、索引文件构建机和制
-
RocketMQ文件恢复机制
-
RocketMQ刷盘机制
-
RocketMQ文件删除机制
存储概要
RocketMQ主要存储的文件包括Comitlog文件、ConsumeQueue文件、IndexFile文件。RocketMQ将所有主题的消息存储在同一个文件中,确保消息发送时顺序写文件,尽最大的能力确保消息发送的高性能与高吞吐量。
但由于消息中间件一般是基于消息主题的订阅机制,这样便给按照消息主题检索消息带来了极大的不便。为了提高消息消费的效率,RocketMQ引入了ConsumeQueue消息队列文件,每个消息主题包含多个消息消费队列,每一个消息队列有一个消息文件。
IndexFile索引文件,其主要设计理念就是为了加速消息的检索性能,根据消息的属性快速从Commitlog文件中检索消息。
- CommitLog:消息存储文件,所有消息主题的消息都存储在CommitLog文件中。
- ConsumeQueue:消息消费队列,消息到达CommitLog文件后,将异步转发到消息消费队列,供消息消费者消费。
- IndexFile:消息索引文件,主要存储消息Key与Offset的对应关系。
- 事务状态服务:存储每条消息的事务状态。
消息发送存储流程
消息存储入口:org.apache.rocketmq.store.DefaultMessageStore#putMessage。
Step1:如果当前Broker停止工作或Broker为SLAVE角色或当前Rocket不支持写入则拒绝消息写入;如果消息主题长度超过256个字符、消息属性长度超过65536个字符将拒绝该消息写入。
如果日志中包含“message store is not writeable, so putMessage is forbidden”,出现这种日志最有可能是磁盘空间不足,在写ConsumeQueue、IndexFile文件出现错误时会拒绝消息再次写入。
如下,就是校验存储状态的方法:
如下,校验消息主题长度,消息属性长度的方法:
Step2:如果消息的延迟级别大于0,将消息的原主题名称与原消息队列ID存入消息属性中,用延迟消息主题SCHEDULE_TOPIC、消息队列ID更新原先消息的主题与队列,这是并发消息消费重试关键的一步。
Step3:获取当前可以写入的Commitlog文件。
Commitlog文件存储目录为{ROCKET_HOME}/store/commitlog文件夹,而MappedFile则对应该文件夹下一个个的文件。
Step4:在写入CommitLog之前,先申请putMessageLock,也就是将消存储到CommitLog文件中是串行的。
putMessageLock有两种实现,一种是非公平模式下的ReentrantLock,一种是CAS的锁。
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找到消息内容。
Step8:获取该消息在消息队列的偏移量。CommitLog中保存了当前所有消息队列的当前待写入偏移量。
Step9:根据消息体的长度、主题的长度、属性的长度结合消息存储格式计算消息的总长度。RocketMQ消息存储格式如下:
- TOTALSIZE:该消息条目总长度,4字节。
- MAGICCODE:魔数,4字节。固定值0xdaa320a7。
- BODYCRC:消息体crc校验码,4字节
- QUEUEID:消息消费队列ID,4字节。
- FLAG:消息FLAG, RocketMQ不做处理,供应用程序使用,默认4字节。
- QUEUEOFFSET:消息在消息消费队列的偏移量,8字节。
- PHYSICALOFFSET:消息在CommitLog文件中的偏移量,8字节。
- SYSFLAG:消息系统Flag,例如是否压缩、是否是事务消息等,4字节。
- BORNTIMESTAMP:消息生产者调用消息发送API的时间戳,8字节。
- BORNHOST:消息发送者IP、端口号,8字节。
- STORETIMESTAMP:消息存储时间戳,8字节。
- STOREHOSTADDRESS:Broker服务器IP+端口号,8字节。
- RECONSUMETIMES:消息重试次数,4字节。
- Prepared Transaction Offset:事务消息物理偏移量,8字节。
- BodyLength:消息体长度,4字节。
- Body:消息体内容,长度为bodyLenth中存储的值。
- TopicLength:主题存储长度,1字节,表示主题名称不能超过255个字符。
- Topic:主题,长度为TopicLength中存储的值。
- PropertiesLength:消息属性长度,2字节,表示消息属性长度不能超过65536个字符。
- Properties:消息属性,长度为PropertiesLength中存储的值
截取部分如下图:
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中,并没有刷写到磁盘。
- AppendMessageStatus status:消息追加结果,取值PUT_OK:追加成功;END_OF_FILE:超过文件大小;MESSAGE_SIZE_EXCEEDED:消息长度超过最大允许长度:PROPERTIES_SIZE_EXCEEDED:消息属性超过最大允许长度;UNKNOWN_ERROR:未知异常。
- long wroteOffset:消息的物理偏移量。
- String msgId:消息ID。
- long storeTimestamp:消息存储时间戳。
- long logicsOffset:消息消费队列逻辑偏移量,类似于数组下标。
- long pagecacheRT = 0:当前未使用。
- int msgNum = 1:消息条数,批量消息发送时消息条数。
Step12:更新消息队列逻辑偏移量。
Step13:处理完消息追加逻辑后将释放putMessageLock锁。
Step14:DefaultAppendMessageCallback#doAppend只是将消息追加在内存中,需要根据是同步刷盘还是异步刷盘方式,将内存中的数据持久化到磁盘,关于刷盘操作后面会详细介绍。然后执行HA主从同步复制,主从同步