1. 概览

image-20240504161036005

消息存储和下⾯三个⽂件关系⾮常紧密:

  1. 数据⽂件 commitlog ,消息主体以及元数据的存储主体 ;
  2. 消费⽂件 consumequeue 消息消费队列,引⼊的⽬的主要是提⾼消息消费的性能 ;
  3. 索引⽂件 indexfile 索引⽂件,提供了⼀种可以通过 key 或时间区间来查询消息。

RocketMQ 采⽤的是混合型的存储结构,Broker 单个实例下所有的队列共⽤⼀个数据⽂件(commitlog)来存储

⽣产者发送消息⾄ Broker 端,然后 Broker 端使⽤同步或者异步的⽅式对消息刷盘持久化,保存⾄ commitlog ⽂件中。 只要消息被刷盘持久化⾄磁盘⽂件 commitlog 中,那么⽣产者发送的消息就不会丢失。 Broker 端的后台服务线程会不停地分发请求并异步构建 consumequeue(消费⽂件)和 indexfile(索引⽂件)

2. 数据文件

RocketMQ 的消息数据都会写⼊到数据⽂件中, 我们称之为 commitlog 。

所有的消息都会顺序写⼊数据⽂件,当⽂件写满了,会写⼊下⼀个⽂件。

image-20240504161306219

如上图所示,单个⽂件⼤⼩默认 1G , ⽂件名⻓度为 20 位,左边补零,剩余为起始偏移量,⽐如 00000000000000000000 代表了第⼀个⽂件,起始偏移量为 0 ,⽂件⼤⼩为1 G = 1073741824。

当第⼀个⽂件写满了,第⼆个⽂件为 00000000001073741824,起始偏移量为 1073741824,以此类推。

image-20240504161520850

从上图中,我们可以看到消息是⼀条⼀条写⼊到⽂件,每条消息的格式是固定的

这样设计有三点优势:

1、顺序写

磁盘的存取速度相对内存来讲并不快,⼀次磁盘 IO 的耗时主要取决于:寻道时间和盘⽚旋转时间,提⾼磁盘 IO 性 能最有效的⽅法就是:减少随机 IO,增加顺序 IO 。

image-20240504161826452

2、快速定位

因为消息是⼀条⼀条写⼊到 commitlog ⽂件 ,写⼊完成后,我们可以得到这条消息的物理偏移量。 每条消息的物理偏移量是唯⼀的, commitlog ⽂件名是递增的,可以根据消息的物理偏移量通过⼆分查找,定位消息位于那个⽂件中,并获取到消息实体数据。

3、通过消息 offsetMsgId 查询消息数据

image-20240504161748140

消息 offsetMsgId 是由 Broker 服务端在写⼊消息时⽣成的 ,该消息编号包含两个部分:

  • Broker 服务端 ip + port 8个字节;
  • commitlog 物理偏移量 8个字节 。

我们可以通过消息 offsetMsgId ,定位到 Broker 的 ip 地址 + 端⼝ ,传递物理偏移量参数 ,即可定位该消息实体数据。

3. 消费文件

在介绍 consumequeue ⽂件之前, 我们先温习下消息队列的传输模型-发布订阅模型 , 这也是 RocketMQ 当前的传输模型。

image-20240504162111962

发布订阅模型具有如下特点:

  • 消费独⽴:相⽐队列模型的匿名消费⽅式,发布订阅模型中消费⽅都会具备的身份,⼀般叫做订阅组(订阅关 系),不同订阅组之间相互独⽴不会相互影响。
  • ⼀对多通信:基于独⽴身份的设计,同⼀个主题内的消息可以被多个订阅组处理,每个订阅组都可以拿到全量消息。因此发布订阅模型可以实现⼀对多通信。

因此,rocketmq 的⽂件设计必须满⾜发布订阅模型的需求。

那么仅仅 commitlog ⽂件是否可以满⾜需求吗 ?

假如有⼀个 consumerGroup 消费者,订阅主题 book_order ,因为 commitlog 包含所有的消息数据,查询该主题下的消息数据,需要遍历数据⽂件 commitlog , 这样的效率是极其低下的。

进⼊ rocketmq 存储⽬录,显示⻅下图:

image-20240504162337987

  1. 消费⽂件按照主题存储,每个主题下有不同的队列,图中 book_order 有 16 个队列 ;
  2. 每个队列⽬录下 ,存储 consumequeue ⽂件,每个 consumequeue ⽂件也是顺序写⼊,数据格式⻅下图。

image-20240504162802334

每个 consumequeue 包含 30 万个条⽬,每个条⽬⼤⼩是 20 个字节,每个⽂件的大小是 30 万 * 20 = 60万字节,每个文件⼤⼩约5.72M 。和 commitlog ⽂件类似,consumequeue ⽂件的名称也是以偏移量来命名的,可以通过消息的逻辑偏移量定位消息位于哪⼀个⽂件⾥。

消费⽂件按照主题-队列来保存 ,这种⽅式特别适配发布订阅模型

消费者从 broker 获取订阅消息数据时,不⽤遍历整个 commitlog ⽂件,只需要根据逻辑偏移量从 consumequeue ⽂件查询消息偏移量 , 最后通过定位到 commitlog ⽂件, 获取真正的消息数据

这样就可以简化消费查询逻辑,同时因为同⼀主题下,消费者可以订阅不同的队列或者 tag ,同时提⾼了系统的可扩展性。

4. 索引文件

每个消息在业务层⾯的唯⼀标识码要设置到 keys 字段,⽅便将来定位消息丢失问题。服务器会为每个消息创建索引(哈希索引),应⽤可以通过 topic、key 来查询这条消息内容,以及消息被谁消费。

由于是哈希索引,请务必保证key尽可能唯⼀,这样可以避免潜在的哈希冲突。

1
2
3
//订单Id 
String orderId = "1234567890";
message.setKeys(orderId);

从开源的控制台中根据主题和 key 查询消息列表:

image-20240504164520047

image-20240504164624888索引⽂件名 fileName 是以创建时的时间戳命名的,固定的单个 IndexFile ⽂件⼤⼩约为 400 M 。

IndexFile 的⽂件逻辑结构类似于 JDK 的 HashMap 的数组加链表结构。

image-20240504164811277

索引⽂件主要由 Header、Slot Table (默认 500 万个条⽬)、Index Linked List(默认最多包含 2000万个条⽬)三部分组 成 。

image-20240504164948904

假如系统发送两条消息 A 和 B , 他们的 key 都是 “key” ,我们依次存储消息 A , 消息 B 。

因为这两个消息的 key 的 hash 值相同,它们对应的哈希槽(深⻩⾊)也会相同,哈希槽会保存的最新的消息 B 的索引 条⽬序号 , 序号值是 4 ,也就是第⼆个深绿⾊条⽬。

⽽消息 B 的索引条⽬信息的最后 4 个字节会保存上⼀条消息对应的索引条⽬序号,索引序号值是 3 , 也就是消息 A 。

5. 总结

RocketMQ 存储模型设计得⾮常精巧,笔者觉得每种设计都有其底层思考,这⾥总结了三点 :

1、完美适配消息队列发布订阅模型 ;

2.、数据⽂件,消费⽂件,索引⽂件各司其职 ,同时以数据⽂件为核⼼,异步构建消费⽂件 + 索引⽂件这种模式⾮常容易扩展到主从复制的架构;

3、充分考虑业务的查询场景,⽀持消息 key ,消息 offsetMsgId 查询消息数据。也⽀持消费者通过 tag 来订阅主题下的不同消息,提升了消费者的灵活性。


本站由 卡卡龙 使用 Stellar 1.29.1主题创建

本站访问量 次. 本文阅读量 次.