RocketMQ因为有高可靠性的要求(宕机不丢失数据),所以数据要进行持久化存储。所以RocketMQ采用文件进行存储。

物理文件结构

RocketMQ的数据默认存储在${user.home}/store目录下,可以通过修改broker.conf中的参数storePathRootDir的值进行设置。

物理目录结构大致如下:

├── abort
├── checkpoint
├── commitlog
│   ├── 00000000000000000000
│   └── 00000000001073741824
├── config
│   ├── consumerFilter.json
│   ├── consumerFilter.json.bak
│   ├── consumerOffset.json
│   ├── consumerOffset.json.bak
│   ├── delayOffset.json
│   ├── delayOffset.json.bak
│   ├── subscriptionGroup.json
│   ├── topics.json
│   └── topics.json.bak
├── consumequeue
│   ├── TopicTest
│   │   ├── 0
│   │   │   └── 00000000000000000000
│   │   ├── 1
│   │   │   └── 00000000000000000000
│   │   ├── 2
│   │   │   └── 00000000000000000000
│   │   └── 3
│   │       └── 00000000000000000000
│   └── TopicTest1
│       ├── 0
│       │   └── 00000000000000000000
│       └── 1
│           └── 00000000000000000000
├── index
│   └── 20220706092335158
└── lock

目录结构说明:

  • commitLog:消息存储目录
  • config:运行期间一些配置信息
  • consumerqueue:消息消费队列存储目录
  • index:消息索引文件存储目录
  • abort:如果存在改文件则Broker非正常关闭
  • checkpoint:文件检查点,存储CommitLog文件最后一次刷盘时间戳、consumerqueue最后一次刷盘时间,index索引文件最后一次刷盘时间戳。

逻辑结构

【RocketMQ】消息的存储设计_java

RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。

  • CommitLog:存储消息的元数据
  • ConsumerQueue:存储消息在CommitLog的索引
  • IndexFile:为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程

【RocketMQ】消息的存储设计_java_02

CommitLog

CommitLog以物理文件的方式存放,每台Broker上的CommitLog被本机器所有ConsumeQueue共享。在CommitLog中,一个消息的存储长度是不固定的, RocketMQ采取一些机制,尽量向CommitLog中顺序写,但是随机读。commitlog文件默认大小为lG ,可通过在broker配置文件中设置mappedFileSizeCommitLog属性来改变默认大小。

CommitLog作为消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容不是定长的。

单个文件大小默认1G, 文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。

每个Rocket实例只会往一个commitlog文件中写,写完一个接着写下一个。indexFile和ComsumerQueue中都有消息对应的物理偏移量,通过物理偏移量就可以计算出该消息位于哪个CommitLog文件上。

CommitLog目前存储的MappedFile文件有两种内容类型:

  • MESSAGE:消息信息
  • BLANK:文件不足以存储消息时的空白占位

MESSAGE

第几位

字段

字节数

数据类型

说明

1

MsgLen

4

Int

消息总长度

2

MagicCode

4

Int

MESSAGE_MAGIC_CODE

3

BodyCRC

4

Int

消息内容CRC

4

QueueId

4

Int

消息队列编号

5

Flag

4

Int

flag

6

QueueOffset

8

Long

消息队列位置

7

PhysicalOffset

8

Long

物理位置。在 CommitLog 的顺序存储位置。

8

SysFlag

4

Int

MessageSysFlag

9

BornTimestamp

8

Long

生成消息时间戳

10

BornHost

8

Long

生效消息的地址+端口

11

StoreTimestamp

8

Long

存储消息时间戳

12

StoreHost

8

Long

存储消息的地址+端口

13

ReconsumeTimes

4

Int

重新消费消息次数

14

PreparedTransationOffset

8

Long

15

BodyLength + Body

4 + bodyLength

Int + Bytes

内容长度 + 内容

16

TopicLength + Topic

1 + topicLength

Byte + Bytes

Topic长度 + Topic

17

PropertiesLength + Properties

2 + PropertiesLength

Short + Bytes

拓展字段长度 + 拓展字段

BLANK

第几位

字段

字节数

数据类型

说明

1

maxBlank

4

Int

空白长度

2

MagicCode

4

Int

BLANK_MAGIC_CODE

ConsumeQueue

ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个Message Queue都有一个对应的 ConsumeQueue文件。

为了加速ConsumeQueue消息条目的检索速度与节省磁盘空间,每一个 Consumequeue条目不会存储消息的全量信息,存储结构如下:

第几位

字段

字节数

数据类型

说明

1

offset

8

Long

在commitLog中的偏移量

2

size

4

Int

消息的大小

3

tagsCode

8

Long

tag标签

ConsumeQueue是消息的逻辑队列,相当于字典的目录,用来指定消息在物理文件CommitLog上的位置。

ConsumeQueue对应一个topic的一个队列结构,由topic和queueId可以唯一创建一个ConsumeQueue结构,同样ConsumeQueue包含一个MappedFileQueue结构,而MappedFileQueue结构由多个MappedFile文件组成,每个文件的大小为30000020(300000 ConsumeQueue.CQ_STORE_UNIT_SIZE),其中20是queue中每个存储单元的大小。

consumequeue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:${user.home}/store/consumequeue/{topic}/{queueId}/{fileName}。

ConsumeQueue即为CommitLog文件的索引文件, 其构建机制是当消息到达Commitlog文件后 由专门的线程产生消息转发任务,从而构建消息消费队列文件(ConsumeQueue )与索引文件(Index File)。

存储机制这样设计有以下几个好处:

  1. CommitLog顺序写 ,可以大大提高写入效率。(实际上,磁盘有时候会比你想象的快很多,有时候也比你想象的慢很多,关键在如何使用,使用得当,磁盘的速度完全可以匹配上网络的数据传输速度。目前的高性能磁盘,顺序写速度可以达到600MB/s,超过了一般网卡的传输速度,这是磁盘比想象的快的地方,但是磁盘随机写的速度只有大概lOOKB/s,和顺序写的性能相差6000倍!)
  2. 虽然是随机读,但是利用操作系统的pagecache机制,可以批量地从磁盘读取,作为cache存到内存中,加速后续的读取速度。
  3. 为了保证完全的顺序写,需要ConsumeQueue这个中间结构,因为ConsumeQueue里只存偏移量信息,所以尺寸是有限的,在实际情况中,大部分的ConsumeQueue能够被全部读入内存,所以这个中间结构的操作速度很快,可以认为是内存读取的速度。此外为了保证CommitLog和ConsumeQueue的一致性,CommitLog里存储了Consume Queues、Message Key、Tag等所有信息,即使ConsumeQueue丢失,也可以通过CommitLog完全恢复出来。

IndexFile

RocketMQ还支持通过MessageID或者MessageKey来查询消息;使用ID查询时,因为ID就是用broker+offset生成的(这里msgId指的是服务端的),所以很容易就找到对应的commitLog文件来读取消息。但是对于用MessageKey来查询消息,RocketMQ则通过构建一个index来提高读取速度。

index存的是索引文件,这个文件用来加快消息查询的速度。消息消费队列RocketMQ专门为消息订阅构建的索引文件,提高根据主题与消息检索消息的速度 ,使用Hash索引机制,具体是Hash槽与Hash冲突的链表结构。

【RocketMQ】消息的存储设计_消息存储_03

IndexFile 也是定长的,从单个文件的数据结构来说,这是实现了一种简单原生的哈希拉链机制。当一条新的消息索引进来时,首先使用 hash 算法命中黄色部分 500w 个 slot 中的一个,如果存在冲突就使用拉链解决,将最新索引数据的 next 指向上一条索引位置。同时将消息的索引数据 append 至文件尾部(绿色部分),这样便形成了一条当前 slot 按照时间存入的倒序的链表。

Index Header

第几位

字段

字节数

数据类型

说明

1

beginTimestamp

8

Long

开始插入时间

2

endTimestamp

8

Long

最后插入时间

3

beginPhyOffset

8

Long

第一个索引对应CommitLog的偏移量

4

endPhyOffset

8

Long

最后一个索引对应CommitLog的偏移量

5

hashSlotCount

4

Integer

槽位使用数

6

indexCount

4

Integer

索引总数

Slot

第几位

字段

字节数

数据类型

说明

1

absSlotPos

4

int

索引在Content中的位置

Content

第几位

字段

字节数

数据类型

说明

1

keyHash

4

Int

key的hashcode

2

phyOffset

8

Long

CommitLog中的偏移量

3

timeDiff

8

Long

消息的延迟时间

4

indexCount

8

Long

上一次槽内的indexCount

Config

config文件夹中存储着Topic和Consumer等相关信息。主题和消费者群组相关的信息就存在在此。

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

过期文件删除

由于RocketMQ操作CommitLog,ConsumeQueue文件是基于内存映射机制并在启动的时候会加载CommitLog,ConsumeQueue目录下的所有文件,为了避免内存与磁盘的浪费,不可能将消息永久存储在消息服务器上,所以需要引入一种机制来删除己过期的文件。

删除过程分别执行清理消息存储文件(Commitlog)与消息消费队列文件(ConsumeQueue文件),消息消费队列文件与消息存储文件共用一套过期文件机制。

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

触发文件清除操作的是一个定时任务,而且只有定时任务,文件过期删除定时任务的周期由该删除决定,默认每10s执行一次。

过期判断

文件删除主要是由这个配置属性:fileReservedTime,文件保留时间。也就是从最后一次更新时间到现在,如果超过了该时间,则认为是过期文件,可以删除。

另外还有其他两个配置参数:

  • deletePhysicFilesInterval:删除物理文件的时间间隔(默认是100MS),在一次定时任务触发时,可能会有多个物理文件超过过期时间可被删除,因此删除一个文件后需要间隔deletePhysicFilesInterval这个时间再删除另外一个文件,由于删除文件是一个非常耗费IO的操作,会引起消息插入消费的延迟(相比于正常情况下),所以不建议直接删除所有过期文件。
  • destroyMapedFileIntervalForcibly:在删除文件时,如果该文件还被线程引用,此时会阻止此次删除操作,同时将该文件标记不可用并且纪录当前时间戳destroyMapedFileIntervalForcibly这个表示文件在第一次删除拒绝后,文件保存的最大时间,在此时间内一直会被拒绝删除,当超过这个时间时,会将引用每次减少1000,直到引用小于等于0为止,即可删除该文件。

删除条件

  1. 指定删除文件的时间点,RocketMQ通过deleteWhen设置一天的固定时间执行一次。删除过期文件操作,默认为凌晨4点。
  2. 磁盘空间是否充足,如果磁盘空间不充足(DiskSpaceCleanForciblyRatio。磁盘空间强制删除文件水位。默认是85),会触发过期文件删除操作。

另外还有RocketMQ的磁盘配置参数:

  1. 物理使用率大于diskSpaceWarningLevelRatio(默认90%可通过参数设置),则会阻止新消息的插入。
  2. 物理磁盘使用率小于diskMaxUsedSpaceRatio(默认75%) 表示磁盘使用正常。