RocketMQ消息存储

前言

分布式队列因为有高可靠性的要求,所以数据要进行持久化存储。
在这里插入图片描述

  1. 消息生成者发送消息
  2. MQ收到消息,将消息进行持久化,在存储中新增一条记录
  3. 返回ACK给生产者
  4. MQ push 消息给对应的消费者,然后等待消费者返回ACK
  5. 如果消息消费者在指定时间内成功返回ack,那么MQ认为消息消费成功,在存储中删除消息,即执行第6步;如果MQ在指定时间内没有收到ACK,则认为消息消费失败,会尝试重新push消息,重复执行4、5、6步骤
  6. MQ删除消息

存储介质

  • 关系型数据库DB

Apache下开源的另外一款MQ—ActiveMQ(默认采用的KahaDB做消息存储)可选用JDBC的方式来做消息持久化,通过简单的xml配置信息即可实现JDBC消息存储。由于,普通关系型数据库(如Mysql)在单表数据量达到千万级别的情况下,其IO读写性能往往会出现瓶颈。在可靠性方面,该种方案非常依赖DB,如果一旦DB出现故障,则MQ的消息就无法落盘存储会导致线上故障

  • 文件系统

目前业界较为常用的几款产品(RocketMQ/Kafka/RabbitMQ)均采用的是消息刷盘至所部署虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式。除非部署MQ机器本身或是本地磁盘挂了,否则一般是不会出现无法持久化的故障问题。

性能对比 文件系统>关系型数据库DB

消息的存储和发送

消息存储
磁盘如果使用得当,磁盘的速度完全可以匹配上网络的数据传输速度。目前的高性能磁盘,顺序写速度可以达到600MB/s, 超过了一般网卡的传输速度。但是磁盘随机写的速度只有大概100KB/s,和顺序写的性能相差6000倍!因为有如此巨大的速度差别,好的消息队列系统会比普通的消息队列系统速度快多个数量级。RocketMQ的消息用顺序写,保证了消息存储的速度。

消息发送
Linux操作系统分为【用户态】和【内核态】,文件操作、网络操作需要涉及这两种形态的切换,免不了进行数据复制。
一台服务器 把本机磁盘文件的内容发送到客户端,一般分为两个步骤:

1)read;读取本地文件内容;

2)write;将读取的内容通过网络发送出去。

这两个看似简单的操作,实际进行了4次数据复制,分别是:

  1. 从磁盘复制数据到内核态内存;
  2. 从内核态内存复制到用户态内存;
  3. 然后从用户态内存复制到网络驱动的内核态内存;
  4. 最后是从网络驱动的内核态内存复 制到网卡中进行传输。
    在这里插入图片描述
    通过使用mmap的方式,可以省去向用户态的内存复制,提高速度。这种机制在Java中是通过MappedByteBuffer实现的

RocketMQ充分利用了上述特性,也就是所谓的“零拷贝”技术,提高消息存盘和网络发送的速度。

这里需要注意的是,采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G 的文件至用户态的虚拟内存,这也是为何RocketMQ默认设置单个CommitLog日志数据文件为1G的原因了

消息存储结构
在这里插入图片描述

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

上面有提到CommitLog日志数据文件为1G,这里设置这么大是因为这个存储文件中包括Topic、QueueId、Message的元数据信息的,1G存满后会自动创建1G的文件进行存储!那么这里从1G的文件中检索数据肯定会麻烦,性能肯定不咋地,那么这里就能体现ConsumeQueue的作用,ConsumeQueue中存储的是消息的索引,如消费者需要消费某条消息时,并不是直接去CommitLog1G的文件中去查询的,是先在ConsumeQueue中得到消息的索引,找到索引对应的消息位置,然后再去CommitLog中精准的定位到某条数据,这种处理方式就能加快CommitLog读取的速度,ConsumeQueue中是不存储消息内容的,只存储索引的,ConsumeQueue是个MessageQueue一一对应的,ConsumeQueue是在内存中的,如果系统宕机后,那么重启时就会在CommitLog中从新获取一遍。

在这里插入图片描述
在这里插入图片描述

  • abort:该文件在Broker启动后会自动创建,正常关闭Broker,该文件会自动消失,若在没启动Broker情况下,发现这个文件是存在的,则说明之前的Broker关闭是非正常关闭的
  • checkpoint:其中存储着commitLog,consumerqueue,index文件的最后刷盘时间戳
  • commitlog:其中存放commitLog文件,存储消息的元数据
  • config:存放Broker运行期间的一些配置数据
  • consumerqueue:存储消息在CommitLog的索引,队列就是存放在这个目录中的
  • index:index存放消息索引文件indexFile,为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过index来查找消息的方法不影响发送与消费消息的主流程,我们在消费消息的时候主要是使用ConsumerQueue!
  • lock:运行期间使用到的全局锁

CommitLog

说明:我们这里称CommitLog为CommitLog文件,但是在源码中该文件被称为mappedFile

这里的CommitLog其实也就是我们broker配置文件中配置的消息存储目录,如下图,这里也能查看出这个文件大小为1G!小于等于1G,这里并不是只有一个CommitLog文件,当存储数据大于1G时会在创建文件进行存储!文件名由20位10进制数构成,标识当前文件的第一条消息其实位移偏移量

注意:一个Broker中仅包含一个CommitLog目录,所有的mappedFile文件都是存放在该目录中的,即无论当前Broker中存放着多少Topic消息,这些消息都是被顺序写入到mappedFile文件中的,也就是说,这些消息在Broker中存放时并没有被按照Topic进行分类

在这里插入图片描述

ConsumerQueue

下图就是ConsumerQueue,每 个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件
在这里插入图片描述

Index

Index如下图
在这里插入图片描述
RocketMQ除了通过指定Topic进行消息消费外,RocketMQ还提供根据Key进行消息查询的功能,该查询是通过store目录中的index子目录中的indexFile进行索引的快速查询,当然,这个indexFile中的索引数据是包含了可以的消息发送到Borker时写入的,如果消息中没有包含Key,则不会写入。

索引条目结构
每个Broker中会包含一组indexFile,每个indexFile都是以一个时间戳命名的,每个indexFile文件又三部分构成,indexHander、slots槽位,indexes索引数据,每个indexFile文件中包含500万个slot槽,而每个slot槽有可能挂载很多的index索引单元
在这里插入图片描述

indexHander固定40个字节,结构如下在这里插入图片描述

  • beginTimestamp:该indexFile第一条消息存储时间
  • endTimestamp:该indexFile最后一条消息存储时间
  • beginPhyoffset:该indexFile中第一条消息在commitlog中偏移量commitlog offset
  • endPyhoffset:该indexFile中最后一条消息在commitlog中偏移量commitlog offset
  • hashSlotCount:已经填充有index的slot数量(并不是每个slot槽小都挂载index索引单元,这里统计的是所有关在index索引单元的slot槽的数量)
  • indexCount:该indexFile中包含的索引个数(统计当前indexFile中所有slot槽下挂载的所有索引单元的数量之和)
    indexFile中最复杂的是Slots与indexes间的关系,在实际存储时,Indexes是在Slots后面的,但为了便于理解,将他们的关系展示如下
    在这里插入图片描述
    key的hash值%5的结果即为slot槽位,然后将该slot值修改为index索引单元的indexNo,根据这个indexNo可以计算出该index单元在indexFile中的位置,不过,该取模结果的重复率是很高的,为了解决这个问题,在每隔index索引单元中增加了preIndexNo,用于指定slot中当前index索引单元的前一个index索引单元,而slot中始终存放的是其下最新的index索引单元的indexNo,这样的话,只要找到了slot就可以找到对其最新的index索引单元,而通过这个index索引单元就可以找到其之前所有index索引单元

在这里插入图片描述

  • keyHash:消息中指定业务key的hash值
  • pyhOffset:当前key对应的消息在commitlog中的偏移量commitlog offset
  • timeDiff:当前key对应消息的存储时间与当前indexFile创建时间的时间戳
  • preIndexNo:当slot下当前index索引单元的前一个index索引单元的indexNo

indexFile文件名
indexFile的文件名为当前文件被创建的时间戳,这个时间戳作用是根据业务key进行查询时,查询条件错了key之外,还需要指定一个要查询的时间戳,表示要查询不大于该时间戳的最新的消息,这个时间戳文件名可以简化查询,提高查询效率!

indexFile创建
indexFile文件创建时机有两个

  • 当第一条带key的消息发送过来后,系统发现没有indexFile,此时会创建第一个indexFile
  • 当一个indexFile挂载的index索引单元数据操过2000W个时,会创建新的indexFile,当前key的消息发送过来后,系统会找到最新的indexFile,并且从其indexHander的最后4字节中读取

由于可以推算出,一个indexFile的最大大小是(40 + 500W * 4 + 200W * 20)字节

indexFile查询流程
当消费者通过业务key来查询相应的消息时,其需要进过一个相对复杂的查询流程,不过,在分析查询流程之前,首先要清楚几个定位计算式子

计算指定key的slot槽位序号
slot槽位号=key的hash % 500W
计算槽位序号为n的slot在indexFile中起始位置
slot(n)位置=40 + (n - 1) * 4
计算indexNo为m的index在indexFile中的位置
index(m)位置 = 40 + 500w * 4 + (m - 1)* 20

在这里插入图片描述

刷盘机制

RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复, 又可以让存储的消息量超出内存的限制。RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时 候,有两种写磁盘方式,分布式同步刷盘和异步刷盘。
在这里插入图片描述
同步刷盘
在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘, 然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写 成功的状态。

异步刷盘
在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。
那么实际企业中使用哪种模式,就要考虑安全性(同步),性能(异步)这二者之间做取舍
同步刷盘还是异步刷盘,都是通过Broker配置文件里的flushDiskType 参数设置的,这个参数被配置成SYNC_FLUSH、ASYNC_FLUSH中的 一个。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员劝退师-TAO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值