目录
RocketMQ消息存储主要发生在Broker端,作为一种高可用消息中间件,RocketMQ具有独特的消息存储方式:RocketMQ消息存储分为物理存储和逻辑存储,物理存储发生在CommitLog文件中;逻辑存储发生在ConsumeQueue和Index文件中。
一、几种常用的消息存储类型比较
下表对比下业界常用的存储类型:
存储类型 | 常见的几款MQ产品 | 存储方式 | 存储效率排名 | 易于实现与快速集成排名 |
文件系统 | RocketMQ、Kafka、RabbitMQ | 消息刷盘(同步刷盘、异步刷盘) | 1 | 3 |
关系型数据库DB | ActiveMQ | 默认采用KahaDB做消息存储,可选JDBC方式做消息持久化 | 3 | 1 |
分布式KV存储 | ZeroMQ | 依赖levelDB、RocksDB和Redis等作为消息持久化存储 | 2 | 2 |
从上述表格可以看出从存储效率方面来看,文件系统存储类型胜出,而文件系统通常有异步和同步刷盘两种方式。对于消息中间件来说,本身应该尽量减少对于外部第三方中间件的依赖,因为相对于应用来说,消息中间件本身就是一种依赖,如果消息中间件又有第三方依赖,可能会导致设计过于复杂,因此采用文件系统在这方面还是有比较大的优势的。
二、RocketMQ存储文件的组成部分
RocketMQ文件存储在store文件夹里,里面包含了commitlog、config、consumequeue、index这4个文件夹和abort、checkpoint两个文件。store文件夹下的具体目录参考下面目录树:
RocketMQ
|--store
|-commitlog
| |-00000000000000000000
| |-00000000001073741824
|-config
| |-consumerFilter.json
| |-consumerOffset.json
| |-delayOffset.json
| |-subscriptionGroup.json
| |-topics.json
|-consumequeue
| |-SCHEDULE_TOPIX_XXX
| |-topicA
| |-topicB
| |-0
| |-1
| |-2
| |-3
| |-00000000000000000000
| |-00000000001073741824
|-index
| |-00000000000000000000
| |-00000000001073741824
|-abort
|-checkpoint
下文主要介绍 CommitLog、ConsumeQueue和Index。
三、物理存储之CommitLog
在RocketMQ中CommitLog的作用是存储所有topic的消息,CommitLog的存储文件地址是$HOME\store\commitlog\${fileName},每个文件默认大小是1GB,当超过1GB时,会自动创建一个新的CommitLog文件进行存储。
1、CommitLog的目录结构
CommitLog的文件名长度为20位,左边补齐零,最右边为偏移量。例如:文件名00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;假设第一个文件存满了,则会生成第二个文件00000000001073741824,起始偏移量为1073741824,依次类推。
图1 CommitLog的目录结构
CommitLog每条消息存储长度不同,其逻辑视图可以参考下图,每条消息的前4个字节存储该消息的总长度。
图2 CommitLog的逻辑视图
2、CommitLog的存储结构
CommitLog的存储结构如下:
图3 CommitLog的存储结构
编号 | 字段简称 | 字段长度(字节) | 含义 |
1 | msgLen | 4 | 消息的长度,消息的长度是整个消息体所占用的字节数的大小 |
2 | magicCode | 4 | 魔数,是固定值,MAGICCODE = 0xdaa320a7 |
3 | bodyCRC | 4 | 消息体的CRC,用于防止网络、硬件等故障导致数据与发送时不一致带来的问题,当broker重启recover时会校验 |
4 | queueId | 4 | 队列id,表示消息发到了哪个consumequeue |
5 | flag | 4 | 网络通信层标记。创建Message对象时由生产者通过构造器设定的flag值,可以用于标记是请求,还是响应亦或是oneway(producer发只管发出消息不管返回值)类型,在RemotingCommand有用到 |
6 | queueOffset | 8 | 在consumequeue中的偏移量 |
7 | physicalPosition | 8 | 代表消息在commitLog中的物理起始地址偏移量 |
8 | sysFlag | 4 | 指明消息是事务状态等消息特征,可参考MessageSysFlag类。二进制为四个字节从右往左数:
|
9 | msg born timestamp | 8 | producer发送消息的时间戳 |
10 | msg host | 8 | producer的host(address:port) |
11 | store timestamp | 8 | 消息存储的时间戳 |
12 | store host | 8 | broker的host(address:port) |
13 | reconsume time | 4 | 消息被某个订阅组重新消费了几次(订阅组之间独立计数),因为重试消息发送到了topic名字为%retry%groupName的队列queueId=0的队列中去了,成功消费一次记录为0; |
14 | prepare transaction offset | 8 | 表示是prepared状态的事务消息偏移量,RocketMQ事务消息基于两阶段提交 |
15 | body length | 4 | 消息体长度 |
16 | msg body | bodyLength | 消息体的内容 |
17 | topic length | 1 | topic的长度,topc的长度最多不能超过127个字节,超过的话存储会出错(有前置校验) |
18 | topic | topicLength | topic的内容值 |
19 | properties Length | 2 | 属性值大小 |
20 | properties | propertiesLength | RocketMQ内部用到的一些属性。例如发送消息的TAG就存放在Properties里面,Properties中的一些常用key都定义在了MessageConst里面 |
四、逻辑存储之ConsumeQueue和Index
RocketMQ的消息存储采用的是混合型的存储结构,所谓混合型存储结构,就是Broker单个实例下所有的队列共用一个CommitLog来存储,这样会导致查询的时候无法快速定位具体的某个消息。于是就有了ConsumeQueue和Index两个逻辑存储文件。
RocketMQ的消息存储架构可以参考下图。
图4 RocketMQ的消息存储架构
从上图可以看出,Producer生产任意的消息后,无论Topic是否相同,都会将消息内容存储到CommitLog中,当CommitLog存储满了,则会创建一个新的CommitLog文件继续存储新生成的消息。此外,在Broker端,会有一个后台服务线程——ReputMessageService会不停地分发请求并异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引文件)。而Consumer端可以根据ConsumeQueue中的数据查找具体的消息。IndexFile则为消息查询提供了一种通过key或时间区间来查询消息的方法。下面主要介绍下ConsumeQueue和IndexFile。
1、ConsumeQueue
RocketMQ基于主题订阅模式实现消息的消费,但是在CommitLog中存储的消息是不连续的,如果从CommitLog检索该消息文件会很慢,为了提高效率,对应的主题的队列建立了索引文件,为了加快消息的检索和节省磁盘空间,每一个ConsumeQueue条目存储了消息的关键信息CommitLog文件中的偏移量、消息长度、tag的hashcode值。下图展示消息到ConsumeQueue的原理。
图5 消息到ConsumeQueue及从ConsumeQueue消费的示意图
一个ConsumeQueue表示一个topic的一个queue,类似于kafka的一个partition,单个ConsumeQueue文件中默认包含30万个条目,ConsumeQueue文件名采取定长设计,每一个条目共20个字节,分别为8字节的CommitLog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M。
ConsumeQueue可以看成是基于topic的CommitLog索引文件,故ConsumeQueue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName},ConsumeQueue具体目录结构如下所示:
图6 ConsumeQueue目录结构
上述的TopicTest是topic名,TopicTest下有4个文件夹,分别代表不同的queueId。每个queueId下存储具体的ConsumeQueue文件,如上图的00000000000000000000文件。消费者在读取消息时,先读取ConsumeQueue,再通过ConsumeQueue中的位置信息读取CommitLog,得到原始的消息。
2、Index
IndexFile(索引文件)用于为生成的索引文件提供访问服务。Index文件的存储位置是:$HOME \store\index\${fileName},它通过MsgId或消息Key值查询消息真正的实体内容。在实际的物理存储上,文件名则是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为420M,一个IndexFile可以保存 2000W个索引。IndexFile的文件结构如下所示:
图7 IndexFile文件结构
1)IndexHead 数据
IndexHead包含了6个部分:
- beginTimestamp:该IndexFile包含消息的第一条消息的存储时间。大小为8字节。
- endTimestamp:该IndexFile包含消息的最后一条消息的存储时间。大小为8字节。
- beginPhyoffset:第一条消息在commitlog的偏移量。大小为8字节。
- endPhyoffset:最后一条消息在commitlog的偏移量。大小为8字节。
- hashSlotCount:已经填充的slot数。大小为4字节。
- indexCount:该IndexFile包含的索引个数。大小为4字节。
2)Hash槽
Hash槽的作用就是存放当前slot下最新index的序号。每当放一个新的消息的index进来,首先取MessageKey的hashCode,然后用hashCode对slot总数取模,得到应该放到哪个slot中,slot总数默认500W个。只要是取hash就必然面临hash冲突的问题,跟HashMap一样,IndexFile也是使用一个链表结构来解决hash冲突。只是这里跟HashMap稍微有点区别的地方是,slot中放的是最新index的指针。这是因为一般查询的时候肯定是优先查最近的消息。
图8 IndexFile存储示意图
从上图可以看出,IndexFile结构与hash表很相似,固定数量的slot组成数组,每个slot对应一条index链,index之间通过链表方式组织在一起。slot的值对应当前slot下最新的那个index的序号,而index中存储了当前slot下以及当前index的前一个index序号,这样就把slot下的所有index链起来了。
3)Index 条目列表
每个Index条目固定长度为20字节,存放真正的索引数据。Index总共大概有2000W,也就是说平均每个slot存4个Index。Index组成结构包含下列几种:
- hashcode:消息key的hashcode。
- phyoffset:消息对应的物理偏移量。
- timedif:该消息存储时间与第一条消息的时间戳的差值,小于0表示该消息无效。
- preIndexNo:该条目的前一条记录的 Index 索引,hash冲突时,根据该值构建链表结构。
五、总结
本文主要介绍RocketMQ的消息存储,RocketMQ消息分为物理存储(CommitLog)和逻辑存储(ConsumeQueue及Index),最开始介绍了业界常用的消息存储类型,并对这些消息存储方式进行了对比。接下来分别介绍CommitLog、ConsumeQueue及Index。通过本文可以了解到RocketMQ在消息存储设计方面的巧妙性:通过物理存储提高Producer端发送消息的速度;通过ConsumeQueue提高Consumer端消费消息的速度;Index文件提供了一种根据key或者时间来查询具体消息的渠道。