对于一款消息中间件来说,优良的数据存储设计,是实现高性能消息吞吐以及消息查询的关键所在。因为消息中间件对于外部来说就是发消息消费消息的一个平台基础设施,但是从其本身来说,需要将海量消息数据信息持久化在 RocketMQ 节点所在的服务器上,这样即便是服务器断电,重启等情况下,也不至于丢失消息数据。另外在进行消息消费的时候,RocketMQ 如何能借助自身的存储设计快速检索到对应的消息也是非常重要的,因此本文主要对 RocketMQ 存储设计进行了设计分析。
RocketMQ 对应的存储文件主要包括三类,分别是 Commitlog 文件、ConsumeQueue 文件以及 Index 文件,每一个文件都有其特殊的使命。
Commitlog 文件
当生产者将消息发送到 RocketMQ 的 Broker 之后,需要将消息进行持久化存储,防止消息数据丢失。RocketMQ 将消息数据写入存储文件 CommitLog 中,按照消息的发送顺序写入文件当中,每个文件的大小约为 1G,当达到文件大小限制后,就会创建新的 CommitLog 文件。RocketMQ 作为消息中间件来说,最主要的数据流程就是基于主题的发布-订阅模式进行消息的发布以及消费,那么当消费者根据自己订阅的 Topic 进行消息消费的时候,Broker 怎么在那么多的 CommitLog 文件中找到对应 Topic 的消息数据呢?
大家可以想一想,CommitLog 文件中的消息数据是一条一条顺序写的,最笨的方法就是遍历文件,作为一款高性能的消息中间件,显然这不是一个好的解决方案。就像从数据库查询数据的时候,遍历的效率肯定是很低的。那么我们可不可以借助数据库提升数据查询的方式,使用索引来加快消息数据的查询呢?答案是肯定的。就像 MySQL 中的索引本身需要文件保存一样,在 RocketMQ 中页有单独保存索引的文件,就是 ConsumerQueue 文件。
ConsumerQueue 文件
在 RocketMQ 中,每个 Topic 对应多个 MessageQueue,每个 MessageQueue 对应一组 ConsumerQueue 文件索引文件。ConsumerQueue 文件中存储了消息相对于 CommitLog 文件的 offset 偏移量,CommitLog 文件本身实际上也是通过偏移量来进行命名如第一个文件是 0000000000000,那么第二文件就是消息总量之和 00000001232321,往后新的文件再进行累计。为什么这么做呢?主要就是在进行消息查找的时候根据消息的偏移量通过二分查找快速定位具体的 CommitLog 文件,提升消息查找效率。需要说明的是,Broker 在进行消息写入 CommitLog 文件中就会异步将其对应的偏移量写入 ConsumerQueue 文件中。
在 ConsumerQueue 文件中实际存储了 CommitLog 文件的 offset 偏移量、消息长度以及 tag 的 hashcode,组成 20 字节的 block 块。其在 Broker 上面的存储路径大致是:…/store/consumequeue/{topic}/{queueid}/{file}。其中 topic 就是生产中订阅的主题,因此消费者在消费消息的时候,Broker 会根据其对应的 Topic 找到对应的 ConsumerQueue 文件,进而找到其索引位置,再到 CommitLog 文件中直接定位具体的消息。
Index 文件
另外 RocketMQ 的特性功能就是可以实现按照消息的属性进行消息搜索,即建立了索引 Key 的 hashcode 与物理偏移量的映射关系,根据 key 先快速定义到 commitlog 文件。
上文中为大家阐述了 RocketMQ 关于存储结构的设计,优秀的存储设计师实现高性能读写的前提。那么除了存储结构的设计,RocketMQ 也使用了一些性能优化手段来实现其强大的消息吞吐能力。
消息顺序写
前文中说过,消息进入 RocketMQ 之后,消息数据是通过顺序写的方式落到 CommitLog 文件中的。那么这里面就涉及两个问题,为什么进行顺序写以及是不是直接写磁盘文件。
1、为什么要顺序写?
当新的数据到来时,只要在之前的文件末尾进行数据追加就可以,这样的数据写入效率要比随机写入的效率高。
2、每次数据写入的时候是直接写磁盘文件吗?
我们可以反过想,如果每次都是落盘写入的话实际效率是不高的,无法满足消息中间件这种高吞吐的性能要求,因此 RocketMQ 实际是借助操作系统的 page cache 来提升写入效率的,消息并不是直接写入磁盘,认识先写入操作系统的 page cache,然后再通过异步刷盘的方式,写入 CommitLog 文件中,这样借助顺序写以及系统的 page cache 可以时间近乎内存的数据写入效率。
同步刷盘和异步刷盘
刚才所说的异步写入,其实就是 RocketMQ 的异步刷盘模式,但是大家想想这个模式有没有什么问题。为了提升数据吞吐量,消息数据过来后,并没有直接写盘,而是在系统中的 page cache 中。那么此时如果 Broker 宕机了,那么此时的消息数据是容易丢失的。所以虽然异步刷盘的写入效率高但是也存在数据丢失的风险。
1、同步刷盘
在同步刷盘的场景下,当 Broker 接受到对应的消息之后,Broker 将会把这条消息刷入磁盘的 CommitLog 中,才会返回确认消息给生产者。如果在进行消息写入的时候 Broker 挂了,那么生产者会感受到消息投递失败,一般都会都有消息重新发送的重试逻辑。
这样看消息似乎不会丢失了,但是由于每次都是先落盘,就会导致数据写入性能下降。
MMAP
在 RocketMQ 中使用了 mmap 技术来实现 Conmmitlog 文件的高性能读写,mmap 就是一种内存映射文件的方法,对于传统的文件 IO 交互来说,需要经过多次的数据复制过程才能将用户进程的数据写入硬盘或者读入程序。而 mmap 可以直接将虚拟内存中的文件与硬盘中文件地址进行映射,减少了数据拷贝的过程,从而提升了数据写入的效率。关于这部分的内容可以参见以前的文章有详细的介绍。
本文主要堆 RocketMQ 的存储设计进行了分析,围绕如何实现高性能消息写入和查询展开了阐述,希望在分析这些优秀中间的具体实现过程中,我们可以将这些优秀设计融入到具体的项目实践中,当我们遇到类似的问题的时候可以借助于这些设计思想来解决实际的问题。