RocketMQ是怎么存储消息的?


1. 存储消息的时机

        RocketMQ对消息的操作是在内存中,保证了消息的处理效率,但是为了防止rocketMQ宕机导致消息丢失,所以需要对消息进行持久化保存。

  1. MQ收到一条消息后,需要向生产者返回一个ACK响应,并将消息存储起来。
  2. MQ推一条消息给消费者后,等待消费者的ACK响应,需要将消息标记为已消费。如果没有标记为消费,MQ会不断的尝试往消费者推送这条消息。
  3. MQ需要定期删除一些过期的消息,这样才能保证服务一直可用。可以通过配置来决定删除时机
    在这里插入图片描述


2. 消息的磁盘读写速度如何保证?

当前业界几款主流的MQ消息队列采用的存储方式主要有以下三种方式:

  1. 分布式KV存储:比如redis
  2. 文件系统:比如RocketMQ、Kafka、RabbitMQ
  3. 关系型数据库DB:比如ActiveMQ、mysql

        从存储效率来说, 文件系统 > 分布式KV存储 > 关系型数据库DB。直接操作文件系统肯定是最快和最高效的,而关系型数据库TPS一般相比于分布式KV系统会更低一些

        RocketMQ采用的是类似于Kafka的文件存储机制,即直接用磁盘文件来保存消息,但是在向磁盘读写数据时,如何保证写入速度的呢?rocketMQ是怎么做到的呢?

  1. 磁盘顺序读写
  2. 零拷贝机制

①:磁盘顺序读写

        磁盘如果使用得当,磁盘的速度完全可以匹配上网络 的数据传输速度。目前的高性能磁盘,顺序写速度可以达到600MB/s, 超过了一般网卡的传输速度。但是磁盘随机写的速度只有大概100KB/s,和顺序写的性能相差6000倍!

        因为有如此巨大的速度差别,好的消息队列系统会比普通的消息队列系统速度快多个数量级。RocketMQ的消息用顺序写,保证了消息存储的速度。

②:零拷贝机制

        Linux操作系统分为【用户态】和【内核态】,文件操作、网络操作需要涉及这两种形态的切换,免不了进行数据复制。RocketMQ充分利用“零拷贝”技术,提高消息存盘和网络发送的速度。零拷贝并不是不进行数据拷贝,而是减少数据拷贝的次数!

读数据不使用零拷贝时:

  1. 磁盘 >> 内核态
  2. 内核态 >> 用户态

读写数据使用零拷贝时:

  1. 磁盘 >> 内核态
  2. 用户态直接存储内核空间的引用,不再进行 内核态 >> 用户态 的数据拷贝

        可以看到零拷贝可以省去向用户态的内存复制,提高磁盘读写速度。这种机制在Java中是通过NIO包中的MappedByteBuffer的map()函数实现的。先将一个磁盘文件(比如一个CommitLog文件,或者是一个ConsumeQueue文件)映射到内存里来,这个所谓的内存映射是什么意思?

        有的人可能会误以为是直接把那些磁盘文件里的数据给读取到内存里来了,类似这个意思,但是并不完全是对的。因为刚开始你建立映射的时候,并没有任何的数据拷贝操作,其实磁盘文件还是停留在那里,只不过他把物理上的磁盘文件的一些地址和用户进程私有空间的一些虚拟内存地址进行了一个映射,如下图:
在这里插入图片描述
        这个地址映射的过程,就是JDK NIO包下的MappedByteBuffer.map()函数干的事情,底层就是基于mmap技术实现的。另外这里给大家说明白的一点是,这个mmap技术在进行文件映射的时候,一般有大小限制,在1.5GB~2GB之间,所以RocketMQ才让CommitLog单个文件在1GB,ConsumeQueue文件在5.72MB,不会太大。

        rocketMQ在零拷贝的基础上,使用mmap技术+pagecache技术实现高性能的文件读写,接下来就可以对这个已经映射到内存里的磁盘文件进行读写操作了,比如要写入消息到CommitLog文件,你先把一个CommitLog文件通过MappedByteBuffer的map()函数映射其地址到你的虚拟内存地址,然后写入数据到PageCache中,过一段时间之后,由os的线程异步刷入磁盘中,如下图:
在这里插入图片描述
如果我们要从磁盘文件里读取数据呢?

        那么此时就会判断一下,当前你要读取的数据是否在PageCache里?如果在的话,就可以直接从PageCache里读取了!比如刚写入CommitLog的数据还在PageCache里,此时你Consumer来消费肯定是从PageCache里读取数据的。但是如果PageCache里没有你要的数据,那么此时就会从磁盘文件里加载数据到PageCache中去,而且PageCache技术在加载数据的时候,还会将你加载的数据块的临近的其他数据块也一起加载到PageCache里去。如下图

在这里插入图片描述
        可以看到,在读写数据的时候,都仅仅发生了一次拷贝,而不是两次拷贝,所以这个性能相较于传统IO来说,肯定是提高了。


3. 消息存储结构

        rocketMQ的消息持久化在我们在搭建集群时都特意指定的文件存储路径,如下所示:
在这里插入图片描述
进入指定的store目录下,可以看到
在这里插入图片描述
下面介绍各文件含义

  1. CommitLog:存储消息的元数据(二进制数据看不懂)。produce发出的所有消息都会顺序存入到CommitLog文件当中。 CommitLog由多个文件组成,每个文件固定大小1G。以第一条消息的偏移量为文件名。
  2. ConsumerQueue:对CommitLog做索引,把消息按照Topic、队列进行归类并存储在ConsumerQueue中,但是存储的并不是消息本身,而是消息在CommitLog的索引。ConsumerQueue中存储的有消息的offset、size、Tag等等,记录当前MessageQueue被哪些消费者组消费到了哪一条CommitLog,方便消费者组快速定位到对应的消息!
  3. Index:类似于ConsumerQueue,也是对CommitLog做索引,与ConsumerQueue不同的是:为消息查询提供了一种通过key或时间区间来查询消息的方法,也是记录消息的offset、size、Tag等等
  4. abort:这个文件是RocketMQ用来判断程序是否正常关闭的一个标识文件。正常情况下,会在启动时创建,而关闭服务时删除。但是如果遇到一些服务器宕机,或者kill -9这样一些非正常关闭服务的情况,这个abort文件就不会删除,因此RocketMQ就可以判断上一次服务是非正常关闭的,后续就会做一些数据恢复的操作。
  5. checkpoint:数据存盘检查点,存储CommitLog文件最后一次刷盘时间戳、consumerquueue最后一次刷盘时间,index索引文件最后一次刷盘时间戳。
  6. config/*.json:这些文件是将RocketMQ的一些关键配置信息以能看懂的json形式进行存盘保存。例如Topic配置、消费者组配置、消费者组消息偏移量Offset 等等一些信息。
    在这里插入图片描述


4. 消息存储源码解析

        RocketMQ为了保证消息发送的高吞吐量,采用单一文件存储所有主题消息,保证消息存储是完全的顺序写,但这样给文件读取带来了不便,为此RocketMQ为了方便消息消费构建了消息消费队列文件ConsumeQueue,基于主题与队列进行组织。同时RocketMQ为消息实现了Hash索引文件IndexFile,可以为消息设置索引键,根据所以能够快速从CommitLog文件中检索消息。当消息达到CommitLog后,会通过ReputMessageService线程接近实时地将消息转发给消息消费队列文件ConsumeQueue与索引文件IndexFile。下面根据源码看一下过程

①:commitLog写入

        在RocketMQ的Broker启动时,会初始化一个核心组件messageStore.start();这个组件作为消息的存储组件,负责接受Produce发来的消息并保存到commitLog文件中,这个组件最终会调用DefaultMessageStore类中的putMessage()方法,这个方法是消息存储的核心!在方法内部又会调用commitLog.putMessage(msg)方法,如下:
在这里插入图片描述
进入commitLog类中的putMessage()方法,方法中先对延时消息进行处理
在这里插入图片描述
        然后拿到虚拟内存中的文件(使用零拷贝实现),使用顺序写入的方式把消息追加到虚拟内存里,在追加时使用lock保证同时只有一个线程往虚拟内存写入消息!mappedFile.appendMessage方法中是真正的写入逻辑
在这里插入图片描述
进入mappedFile.appendMessage方法中看一下具体的写入逻辑:就是包装消息的各种附加信息,例如msgId、offset等等,并把这些信息一并写入虚拟内存
在这里插入图片描述
由于CommitLog文件有1G的大小限制,当虚拟内存中的CommitLog被写满时,会创建一个新CommitLog文件继续写入
在这里插入图片描述
上面写入的只是虚拟内存,还要进行文件刷盘和主从同步
在这里插入图片描述
        

②:分发ConsumeQueue和IndexFile

Broker启动时会启动一个消息存储的核心组件messageStore
在这里插入图片描述

当CommitLog写入一条消息后,在DefaultMessageStore的start方法中,会启动一个后台线程reputMessageService每隔1毫秒就会去拉取CommitLog中最新更新的一批消息,然后分别转发到ComsumeQueue和IndexFile里去
在这里插入图片描述
在这里插入图片描述
        
③:过期文件删除

​         默认情况下, Broker会启动后台线程,每60秒,检查CommitLog、ConsumeQueue文件。然后对超过72小时的数据进行删除。也就是说,默认情况下, RocketMQ只会保存3天内的数据。这个时间可以通过fileReservedTime来配置。注意他删除时,并不会检查消息是否被消费了。

最后,附上一张整个文件存储流程图
在这里插入图片描述


5. 刷盘机制

        刷盘即存盘,刷盘机制是指生产者生产消息到rocketMQ后存入硬盘的方式,RocketMQ需要将消息存储到磁盘上,这样才能保证断电后消息不会丢失。同时这样才可以让存储的消息量可以超出内存的限制。RocketMQ为了提高性能,会尽量保证磁盘的顺序写。消息在写入磁盘时,有两种写磁盘的方式

  1. 同步刷盘:只有在消息真正持久化至磁盘后,RocketMQ的Broker端才会真正地返回给Producer端一个成功的ACK响应。同步刷盘对MQ消息可靠性来说是一种不错的保障,但是性能上会有较大影响,一般适用于金融业务应用领域。

  2. 异步刷盘:能够充分利用OS的PageCache的优势,只要消息写入PageCache即可将成功的ACK返回给Producer端。消息刷盘采用后台异步线程提交的方式进行,降低了读写延迟,提高了MQ的性能和吞吐量。

异步和同步刷盘的区别在于,异步刷盘时,主线程并不会阻塞,在将刷盘线程唤醒后,就会继续执行。
在这里插入图片描述
同步、异步配置方式如下:
在这里插入图片描述


6. 主从复制机制

        如果Broker以一个集群的方式部署,会有一个master节点和多个slave节点,消息需要从Master复制到Slave上。而消息复制的方式分为同步复制和异步复制。

  1. 同步复制
    同步复制是等Master和Slave都写入消息成功后才反馈给客户端写入成功的状态。在同步复制下,如果Master节点故障,Slave上有全部的数据备份,这样容易恢复数据。但是同步复制会增大数据写入的延迟,降低系统的吞吐量。

  2. 异步复制
    异步复制是只要master写入消息成功,就反馈给客户端写入成功的状态。然后再异步的将消息复制给Slave节点。在异步复制下,系统拥有较低的延迟和较高的吞吐量。但是如果master节点故障,而有些数据没有完成复制,就会造成数据丢失。

配置方式如下:
在这里插入图片描述


  • 9
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值