关于RocketMQ的高性能设计,你真的了解吗?

RocketMQ的高性能设计

谎言不会伤人,真相才是快刀。

哈喽大家好,我是际遇,今天继续跟大家聊一聊RocketMQ的高性能设计吧!

其实经过了很多年的阿里巴巴的双十一的验证,RocketMQ除了稳定以外,也保持着极高的性能(不然阿里早就不用啦)。这也是很多企业在技术选型的时候会把RocketMQ纳入考虑范围的原因。

先说结论,它的高性能设计主要体现在三个方面:

  • 数据存储设计
  • 动态伸缩能力
  • 消息实时投递

先说说数据存储设计吧,数据存储设计主要包括了RocketMQ的顺序写盘,消费队列的设计,消息跳跃读,数据零拷贝。

是不是看到这头都大了?

我刚开始看的时候也是觉得,这些术语看的我比上数学课都犯困,还学习?怎么学?

不急,我们还是拆开一点点来分解一下。

数据存储设计

RocketMQ的数据存储的核心主要由两个部分组成,CommitLog(数据存储文件)和ConsumeQueue(消费队列文件)。

简单来说,我们按步骤分解一下:

  1. Producer会先把消息发送到Broker服务器
  2. Broker会把所有收到的消息都存储在CommitLog文件中
  3. CommitLog文件会将消息转发到ConsumeQueue文件
  4. ConsumeQueue文件提供给各个Consumer消费

为了方便理解,画个图咱们了解一下:

顺序写盘

在说顺序写盘之前,我们先回顾一下子盘读写要经历的三个动作:

  • 寻道:磁头移动到指定磁道,需要找到数据在哪个物理位置,时间很长。
  • 旋转延迟:等待指定扇区旋转至磁头下,机械硬盘和每分钟多少转有关,时间很短。
  • 数据传输:数据通过系统总线从磁盘传输到内存,时间很短。

其中,磁盘读写最慢的动作是寻道,缩短寻道时间就能有效提升磁盘读写的速度,那最优的方式就是不用寻道啦。

  • 随机写:随机写会导致磁头不停的更换磁道,时间都花在寻道上了。
  • 顺序写:顺序写几乎不用换磁道,当然也不是完全不换,总之,其寻道时间在这里可以忽略不计。

前面我们说了CommitLog文件,它是负责存储消息数据的文件,所有Topic的消息都会先存在CommitLog文件中,消息数据写入CommitLog文件是加锁串行追加写入。

RocketMQ为了保证消息发送的高吞吐量,使用了单个文件存储所有的Topic的消息,也就是说,每个CommitLog文件存储了多个Topic的消息,这样虽然保证了消息存储的时候是完全的顺序写,但是又会给读取的时候带来困难。

那RocketMQ是怎么解决的呢?

RocketMQ的做法很简单,当消息到达CommitLog文件后,会通过异步线程几乎实时的将消息发送给消费队列文件(ConsumeQueue),每个CommitLog文件的默认大小是1GB,当写满后再写新的文件。

这样就导致了大量的I/O都顺序写在同一个CommitLog文件。重点来了,其文件名是按照该文件起始的总的字节偏移量offset命名,文件名固定的长度为20位,不足的前面补0,如:

  • 第一个文件起始偏移量是0,文件名为:00000000000000000000。
  • 第二个文件起始偏移量是1024 x 1024 x 1024 = 1073741824 (1 GB = 1073741824B),那第二个文件名为0000000001073741824。

这样加的好处是,在消费消息时能够根据偏移量offset来快速定位到消息存储在某个CommitLog文件,从而加快了检索的速度。

消费队列设计

其实前面我们已经发现了,消费Broker中的消息,本质上就是读取文件,但是问题在于消息数据文件中所有的Topic的消息是混合在一起的,但是消费消息的时候又是区分Topic来消费的。

这就导致如果消费消息的时候也读取CommitLog文件的话,会导致消费消息就变得性能差,吞吐量低。

那怎么解决的呢?

前面我们已经知道了,RocketMQ设计了ConsumeQueue来解决这个问题,它负责存储消费队列文件,在消息写入CommitLog文件的时候,通过异步线程转发到ConsumeQueue文件,然后提供给Consumer消费。

这里我们看之前的图应该就能知道,ConsumeQueue文件中并不存储具体的消息数据,只存储了CommitLog的offset偏移量,消息大小,消息Tag Hashcode。

每个Topic在某个Broker下是对应多个队列的,默认是四个,每一条记录的大小是20B,默认一个文件会存储30万个记录,文件名和CommitLog一样,也是按照字节偏移量来命名,文件名的默认长度是20位,不足补0。

  • 第一个文件起始偏移量是0,文件名是000000000000000000000,与CommitLog一样。
  • 第二个文件其实偏移量是20 x 30w = 6000000,第二个文件名是00000000000006000000。

如果是在集群的模式下,Broker会记录客户端对每个消费队列的消费偏移量,定位到ConsumeQueue里相应的记录,并通过CommitLog的Offset定位到CommitLog文件里的该条消息。

消息跳跃读取

RocketMQ中还是用了操作系统中的Page Cache机制(一种缓存机制), RocketMQ读取消息依赖操作系统的PageCache,PageCache命中率越高,那么RocketMQ的读取性能就越高,操作系统会尽量预读取数据,使应用直接访问磁盘的概率变低。

消息队列文件的读取流程:

  • 检查要读取的数据是否在上次预读取的Cache中。
  • 如果没有命中Cache,操作系统从磁盘中读取对应的数据页,并将该数据页之后的连续几页一起读取到Cache中,在将应用需要的数据返回,这种方式就叫做跳跃读取。
  • 如果命中的Cache,上次缓存的数据有效,操作系统认为在顺序读盘,则继续扩大混存的数据范围,将之前缓存的数据页之后的几页数据再读取到Cache中。

这里我们小小的扩展一下,在计算机中,CPU,RAM(内存),DISK(硬盘)的速度是不相同的,相信大家都知道的,按照速度高低排列一下:CPU>RAM>DISK。

还有一点是,它们之间的速度和容量差距是指数级别的,为了折中一下,就有了在CPU和RAM之间使用CPU Cache来提高访存速度,在RAM和DISK之间使用Page Cache提高系统对文件的访问速度。

数据零拷贝

这个相信各位也应该有所耳闻了,在网络通信的过程中呢,通常情况下对文件的读写要多经历一次数据拷贝,例如写文件数据要从用户态拷贝到内核态,再由内核态写入物理文件。

所谓的零拷贝指的就是用户态与内核态之间不存在拷贝。

在Java 的NIO中会提到相同的东西,到时候细说一下。

RocketMQ中的文件读写主要通过Java NIO中的 MappedByteBuffer来进行文件映射。利用了Java NIO中的FIleChannel模型,可以直接将物理文件映射到缓冲区的PageCache,少了一次数据拷贝的过程,提高了读写的速度。

动态伸缩能力

所谓动态伸缩呢,我们可以想象一下类似双11和618的时候,我们需要增加服务器(伸)来应对暴增的流量,但并不是所有的时候都有这么大的流量,所以当流量回归到正常水平的时候,为了避免服务器的资源(成本)浪费,此时就需要减少服务器(缩)。

我们从两个方面来说一下:

  • 消息队列扩容/缩容:一个Consumer实例同时消费多个消息队列中的消息。如果一个Topic的消息量特别大,但是Broker集群的水位压力还是很低,就可以对该Topic进行扩容,Topic的消息队列数跟消费速度是成正比的。消息队列的数可以在创建Topic的时候指定,也可在运行中修改。相反,就可以缩容。
  • Broker集群扩容/缩容:同样的, 如果一个Topic的消息量特别大,但是Broker集群的水位很高,此时就需要对Broker机器扩容,扩容的方式也很简单,直接加机器部署Broker就行了。新的Broker启动后会向NameServer注册,Producer和Consumer通过NameServer发现新的Broker并更新路由信息,反之亦然。

消息实时投递

消息的高性能除了以上说到的,还体现在消息发送到存储之后能否立即被客户端消费,这就涉及到了消息的实时投递,Consumer消费消息的实时性与获取消息的方式有很大的关系。

我们知道,任何一款消息中间件都会有两种获取消息的方式,Push推模式和Pull拉模式。这两种模式呢,不能是谁好谁不好,只是他们适用于不同的场景:

  • Push推模式:

    • 优点:Consumer能实时的接收到新的消息数据。
    • 缺点:Consumer消费不过来,缓冲区溢出。而且一个Topic往往对应多个ConsumerGroup,服务端一条消息会产生多次推送,给服务端造成不小的压力。
  • Pull拉模式:

    • 优点:可以根据消费速度选择合适的时机拉去消息消费。
    • 缺点:拉取的间隔时间不好控制。

其实这两种获取消息方式的缺点都很明显,单一的方式很难应对复杂的消费场景。

所以呢,RocketMQ提供了一种推/拉结合的长轮询机制来平衡这哥俩的缺点。

长轮询本质上其实是对普通Pull模式的优化,所以呢,还是以Consumer轮询的方式主动发送拉取请求到服务端Broker,Broker检测到有新的消息就返回给Consumer,如果没有就不返回,挂起当前请求缓存到本地,Broker有个线程去检查挂起请求,等到新消息产生时再返回Consumer。

对了,平常我们使用的DefaulMQPushConsumer的实现就是推拉结合的。

说白了,也就是消费者会不断地问Broker:"有新的消息可以给我消费了没?"

然后Broker要么回答:"有了!马上发给你。"

要么一直不说话,等到有新消息了再回答你。

后记

本来之前想着RocketMQ的高性能设计不需要太长的篇幅就可以讲完的,没想到整理了一下,有点小多。

那就后续再写RokcetMQ的高可用部分吧。

今天就不emo了,emo不起来了。

本来想写一点自己怎么入坑的这个行业,写了快一万字了,才刚到一半,有点小累。

好啦,今天就到这里啦!

欲知后事如何,且听下回分解。

相关推荐
AI人工智能+电脑小能手4 小时前
【大白话说Java面试题 第87题】【Mysql篇】第17题:分布式事务的实现原理?
java·数据库·分布式·mysql·面试
红尘散仙4 小时前
我把终端小说阅读器接上了 AI Agent:TRNovel 现在能用 skill 生成书源了
人工智能·后端·rust
来杯@Java5 小时前
图书管理系统(基于springboot+vue前后端分离的项目)计算机毕业设计java
java·spring boot·spring·vue·毕业设计·mybatis·课程设计
卷毛的技术笔记5 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
编程大师哥5 小时前
匿名函数 lambda + 高阶函数
java·python·算法
会编程的土豆6 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木6 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
Cosolar6 小时前
从零写一个 Attention Is All You Need
人工智能·面试·架构
adrninistrat0r6 小时前
Java调用链MCP分析工具
java·python·ai编程
喵个咪6 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm