当前位置: 首页 > 科技观察

终于摸清了RocketMQ的存储模型

时间:2023-03-18 23:16:24 科技观察

RocketMQ的优秀性能,难免绕不开其优秀的存储模型。在这篇文章中,笔者尝试根据自己的理解来分析一下RocketMQ的存储模型,希望对大家有所启发。1.总体概述首先回顾一下RocketMQ的架构。整体架构包含四个角色:Producer:消息发布的角色。Producer通过MQ负载均衡模块选择对应的Broker集群队列进行消息传递。交付过程支持快速失败和低延迟。Consumer:消息消费角色,支持推送和拉取两种模式消费消息。NameServer:名称服务是一个非常简单的主题路由注册中心,其作用类似于Dubbo中的zookeeper,支持Broker的动态注册和发现。BrokerServer:Broker主要负责消息的存储、传递和查询以及服务的高可用保证。本文的重点是分析BrokerServer的消息存储模型。我们首先进入broker的文件存放目录。消息存储与以下三个文件密切相关:数据文件commitlog消息体和元数据存储体;consumption文件consumequeue消息消费队列,引入主要是为了提高消息消费的性能;indexfileindexfile索引文件提供了一种可以通过key或者timeinterval来查询消息。RocketMQ采用混合存储结构。单个Broker实例下的所有队列共享一个数据文件(commitlog)进行存储。producer向Broker端发送消息,然后Broker端使用同步或异步的方式将消息flush并持久化,保存到commitlog文件中。只要将消息flush并持久化到磁盘文件commitlog中,producer发送的消息就不会丢失。Broker端的后台服务线程会不断分发请求,异步构建consumequeue(消费文件)和indexfile(索引文件)。2.数据文件RocketMQ的消息数据会被写入数据文件,我们称之为commitlog。所有消息会依次写入数据文件,当文件写满后,再写入下一个文件。如上图所示,默认单个文件大小为1G,文件名长度为20位,左边补零,其余为起始偏移量。例如00000000000000000000代表第一个文件,起始偏移量为0,文件大小为1G=1073741824。当第一个文件满时,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。从上图中我们可以看出,消息是一条一条写入文件的,每条消息的格式是固定的。这种设计有三个优点:顺序写入磁盘的访问速度相对于内存来说并不快,一次磁盘IO的耗时主要取决于:寻道时间和磁盘旋转时间。提高磁盘IO性能最有效的方法是减少RandomIO,增加sequentialIO。内存和磁盘中随机和顺序读写的性能对比《 The Pathologies of Big Data 》这篇文章指出,内存中随机读写的速度远低于磁盘中顺序读写的速度。磁盘的顺序写入速度可达数百MB/s,而随机写入速度只有几百KB/s,相差数千倍。2、快速定位因为消息是一条一条写入commitlog文件,写入完成后,我们就可以得到消息的物理偏移量。每条消息的物理偏移量都是唯一的,提交日志文件名是递增的。根据消息的物理偏移量,通过二分查找可以定位到消息所在的文件,得到消息实体数据。3、通过消息offsetMsgId查询消息数据。消息offsetMsgId是Broker服务器在写入消息时生成的。消息号包含两部分:Brokerserverip+port8字节;commitlog物理偏移8个字符节。我们可以通过消息offsetMsgId定位Broker的ip地址+端口,通过物理偏移量参数定位消息实体数据。3、消费文件在介绍consumequeue文件之前,我们先回顾一下消息队列的传输模型——发布-订阅模型,这也是目前RocketMQ的传输模型。发布订阅模型具有以下特点:独立消费:相对于队列模型的匿名消费模式,发布订阅模型中消费者的身份一般称为一个订阅组(订阅关系),不同的订阅组相互独立,不会相互影响。一对多通信:基于独立身份的设计,同一主题下的消息可以被多个订阅组处理,每个订阅组可以获得全量的消息。因此,发布-订阅模型可以实现一对多的通信。所以rocketmq的文件设计必须满足发布-订阅模型的要求。那么只有commitlog文件能满足需求吗?如果有一个consumerGroup消费者订阅了topicmy-mac-topic,因为commitlog包含了所有的消息数据,查询这个topic下的message数据需要遍历数据文件commitlog,效率极低。进入rocketmq存放目录,如下图:消费文件按照topic存放,每个topic下有不同的queue。图中my-mac-topic中有16个队列;每个queue目录存放consumequeue文件,每个consumequeue文件也是顺序写入的,数据格式如下图所示。每个consumequeue包含300,000个entry,每个entry的大小为20字节,每个文件的大小为300,000*20=600,000字节,每个文件的大小约为5.72M。与commitlog文件类似,consumequeue文件的名字也是以offset命名的,通过消息的逻辑偏移量可以定位消息在哪个文件中。消费文件是按照topic-queue保存的,这特别适用于发布-订阅模型。消费者从broker获取订阅消息数据时,不需要遍历整个commitlog文件,只需要根据逻辑偏移量从consumequeue文件中查询消息偏移量,最终通过定位到提交日志文件。这样可以简化消费查询逻辑,同时由于消费者可以订阅同一主题下的不同队列或标签,也提高了系统的可扩展性。4.业务层面索引文件中每条消息的唯一标识码应该设置到keys字段,方便以后定位消息丢失问题。服务器会为每条消息创建一个索引(哈希索引),应用程序可以通过topic和key查询消息的内容以及谁消费了消息。由于是哈希索引,请保证key尽可能唯一,避免潜在的哈希冲突。//订单号StringorderId="1234567890";message.setKeys(orderId);从开源控制台根据主题和key查询消息列表:进入索引文件目录,如下图所以:索引文件名fileName为创建时间戳命名,固定单个IndexFile文件大小大约400M。IndexFile的文件逻辑结构类似于JDK的HashMap的数组加链表结构。HashMap数据结构索引文件主要由三部分组成:Header、SlotTable(默认500万条)、IndexLinkedList(默认最大2000万条)。如果订单系统发送了两条消息A和B,它们的key都是“1234567890”,我们依次存储消息A和消息B。因为两条消息的key的hash值相同,所以它们对应的hashslots(深黄色)也会相同,hashslot会保存最新消息B的indexentrynumber,以及编号值为4,这是第二个深绿色条目。消息B的索引条目信息的最后4个字节将存储上一条消息对应的索引条目编号,索引编号值为3,即消息A。5.写到最后,Databasesarespecialized–“一刀切”的做法不再适用------MongoDB设计理念RocketMQ存储模型设计的非常精巧,我觉得每一种设计都有其底层的思考,这里总结一下三点:完美适配消息队列发布和订阅模型;数据文件、消费文件、索引文件各司其职,同时以数据文件为核心异步构建消费文件+索引文件。这种模型非常容易扩展为主从复制架构;充分考虑业务查询场景,支持消息key、消息offsetMsgId查询消息数据。还支持消费者通过标签订阅主题下的不同消息,提高了消费者的灵活性。