基础概念
- Message(消息):Message 是 RocketMQ 传输的基本单元,包含了具体的业务数据以及一些元数据(如消息 ID、主题、标签、发送时间等)。消息可以是文本、二进制数据或其他任何序列化后的对象形式。
- Topic(主题):Topic 是一类消息的逻辑分类名,是 Apache RocketMQ 中消息传输和存储的顶层容器。类似于邮件系统中的邮箱地址或发布/订阅模式中的“频道”。生产者向特定的 Topic 发送消息,消费者则根据 Topic 订阅并接收消息。一个 Topic 可以被多个生产者写入,同时也能被多个消费者订阅。
- Queue(队列):每个 Topic 被划分为多个 Queue(队列),或称 MessageQueue,这些队列用于存储消息。生产者发送到 Topic 的消息会被分配到其下的各个 Queue 中;消费者则是从这些 Queue 中拉取消息进行消费。
这里的Topic和Queue都是“逻辑上”的概念,这个“逻辑上”的意思是涉及到一些MQ的使用、原理、机制的时候默认的名词概念,而不是实际物理意义上的结构和设计
- Subscription(订阅):Subscription 表示消费者对某个 Topic 消息的兴趣表达。订阅关系由消费者分组动态注册到服务端系统,并在消息传输中按照订阅关系定义的过滤规则进行消息匹配和消费进度的维护。
- Producer(生产者):生产者是消息产生的源头,将消息发送到服务端指定 Topic。
- Consumer(消费者):消费者负责从服务端中拉取消息并进行处理。
- ProducerGroup(生产者组):ProducerGroup 是一组生产者的逻辑分组,共享同样的 Topic 发送配置,实现发送端的负载均衡和容错。如果组内某个生产者失败,其他生产者可以继续工作,保证消息发送的连续性。
- ConsumerGroup(消费者组):消费者分组是 Apache RocketMQ 系统中承载多个消费行为一致的消费者的负载均衡分组。和消费者不同,消费者分组并不是运行实体,而是一个逻辑资源。分组中的消费者共同订阅同一个 Topic 并以某种策略(如广播、集群消费)消费消息。在 Apache RocketMQ 中,通过消费者分组内初始化多个消费者实现消费性能的水平扩展以及高可用容灾。
技术架构(物理架构)
Apache RocketMQ 服务端基础组件包括 NameServer、Broker、Proxy。
- Broker:RocketMQ 是一个典型的发布订阅系统,通过 Broker 节点中转和持久化数据,解耦上下游。Broker 是真实存储数据的节点,由多个水平部署但不一定完全对等的副本组构成,单个副本组的不同节点上的数据会达到最终一致。单个副本组同一时间只有一个可读写的 Master 和若干个只读的 Slave,主故障时会进行选举来容忍故障,此时单个副本组可读不可写。
- NameServer: 是独立的一个无状态组件,接受 Broker 的元数据注册并动态维护着一些映射关系,同时为客户端提供服务发现的能力。
- Proxy:先跳过
RocketMQ 部署模式分两种:
- 直连部署模式 (NameServer、Broker)
- 存储计算分离模式部署(Proxy)
直连部署模式
直连部署模式下,实际物理架构包括两个组件:
- NameServer
- Broker
Producer 和 Consumer 会和 NameServer,Broker 都维持长连接。Producer 只会向 Master 副本发送消息,Consumer 可以从 Master 或者 Slave 消费消息。
存储计算分离模式部署
存储和计算分离是一种良好的模块化设计。无状态的 Proxy 集群是数据流量的入口,提供签名校验与权限控制、计量与可观测、客户端连接管理、消息编解码处理、流量控制、多协议接入等能力。原 Broker 节点演化为以存储为核心的有状态集群,支持读写多类型消息,它们的底层是多模态存储和多元化的高效索引。存储计算分离的形态利于不同业务场景下单独调整存储或计算节点的数量,来实现扩容和缩容。网关模式接入还能带来升级简单,组网便利等好处。Proxy 和 Broker 都属于服务端组件,内网通信的延迟不会显著增加客户端收发消息的延迟。
Proxy 自身会向 NameServer 和 Broker 都建立长连接,Producer 和 Consumer 仅连接到 Proxy
通信机制
Apache RocketMQ 客户端使用 TCP 访问服务端,根据传输的数据格式分为 Remoting 协议和 gRPC 协议。
- Remoting 协议诞生较早,是组件间通信默认的私有协议。其中 Remoting Java 客户端和主仓库同步演进和迭代,而多语言客户端归属于 Apache 社区多个独立仓库。
- gRPC 协议自 RocketMQ 5.0 版本推出,以 Protobuf 定义了底层传输的数据格式(详见 RocketMQ API),旨在以云原生主流技术演进轻量、标准、易扩展的客户端服务端通信协议。使用 gRPC 协议的 SDK 是以独立仓库 RocketMQ Clients 方式演进,支持 Java/C++/.NET/Go/Rust 等众多语言。
- RocketMQ 5.0 在服务端内部也提供了基于 Protobuf + gRPC 的管控 API 实现。
RocketMQ 的接入点是什么?为了简化客户端配置的复杂度:
- 以直连模式部署的集群,客户端需要和服务端的 NameServer,Broker 进行点对点直连通信,客户端需要配置 NameServer 集群的负载均衡地址。
- 以代理模式部署的集群,无论客户端使用 Remoting 还是 gRPC 协议,客户端仅需和 Proxy 进行通信,需要将配置接入点为 Proxy 的负载均衡地址。服务端会使用协议协商技术,自动区分 Remoting 和 gRPC 协议并处理客户端的请求。
- 在受限网络环境中,客户端需要同时放通接入点的 8080 和 8081 端口。
存储机制
元数据管理
所谓元数据,就是产生的消息的原始数据。
为了提升整体的吞吐量与提供跨副本组的高可用能力,RocketMQ 服务端一般会为单个 Topic 创建多个逻辑分区,即在多个副本组(Broker:Master+Slave)上各自维护部分分区 (Partition),我们把它称为队列 (MessageQueue)。同一个副本组上同一个 Topic 的队列数相同并从 0 开始连续编号,不同副本组上的 MessageQueue 数量可以不同。
说人话:
- 先不考虑Broker,一般的Topic的消息都会被打散到多个队列中,多队列本身就是为了提升多个消费者模式下的吞吐量;
- 只有一个Broker的情况下 ,没啥可讲的,这些队列肯定是全放到这个Broker中去;
- 现在为了提升吞吐量,增加了多个Broker副本,假如某个Topic对应5个逻辑队列,对于所有的Broker而言,它们肯定需要包含全部的5个队列,但是对于某一个Broker而言,它可能只需要维护5个中的4个/3个/2个/1个队列;
高效的存储层实现
RocketMQ 存储的核心是极致优化的顺序写盘,数据以 append only 的形式不断的将新的消息追加到文件末尾。RocketMQ 使用了一种称为 MappedByteBuffer 的内存映射文件的办法,将一个文件映射到进程的地址空间,实现文件的磁盘地址和进程的一段虚拟地址关联,实际上是利用了NIO 中的 FileChannel 模型。在进行这种绑定后,用户进程就可以用指针(偏移量)的形式写入磁盘而不用进行 read / write 的系统调用,减少了数据在缓冲区之间来回拷贝的开销。当然这种内核实现的机制有一些限制,单个 mmap 的文件不能太大 (RocketMQ 选择了 1G),此时再把多个 mmap 的文件用一个链表串起来构成一个逻辑队列 (称为 MappedFileQueue),就可以在逻辑上实现一个无需考虑长度的存储空间来保存全部的消息。
单条消息的存储格式
RocketMQ 有一套相对复杂的消息存储编码用来将消息对象序列化,随后再将非定长的数据落到上述的真实的写入到文件中,存储格式中包括了索引队列的编号和位置。单条消息的存储格式如下:
构建消息的索引
在数据写入 CommitLog 后,有一个后端的 ReputMessageService 服务 (也被称为 dispatch 线程) 会异步的构建多种索引(例如 ConsumeQueue 和 Index),满足不同形式的读取和查询诉求。在 RocketMQ 的模型下,消息本身存在的逻辑队列称为 MessageQueue,而对应的物理索引文件称为 ConsumeQueue。其中 dispatch 线程会源源不断的将消息从 CommitLog 取出,再拿出消息在 CommitLog 中的物理偏移量,消息长度以及 Tag Hash 等信息作为单条消息的索引,分发到对应的消费队列,构成了对 CommitLog 的引用 (Reference)。ConsumeQueue 中单条消息占用的索引空间只有 20B。当客户端尝试从服务端拉取消息时,会先读取索引并进行过滤,随后根据索引从 CommitLog 中获得真实的消息并返回。
客户端
RocketMQ 提供了灵活的负载均衡机制,主要体现在消费者如何均衡地从消息队列中获取消息。
主要分为三种消费模式:
- Push(推送模式)
- Pull(拉取模式)
- Pop(无状态消费模式)
Push
RocketMQ 中的 Push 并不是指传统意义上的客户端完全被动接收,底层是基于长轮询机制实现。
- 长轮询:客户端与 Broker 建立长连接,并发送拉取消息的请求。如果当前没有新消息,Broker 不会立即响应,而是等待一段时间或直到有新消息到达再返回。
- 消费位点:每个消费者维护自己的消费进度(消费位点),Broker 根据这些位点信息,只推送消费者尚未消费的消息。
- 重平衡:当消费者组内的消费者实例发生变化时(如增加或减少消费者实例),RocketMQ会触发一次重平衡(Rebalance)操作,重新分配消息队列到各个消费者实例,以实现负载均衡。这个过程确保了消息的均匀消费,避免了消息积压或某些消费者空闲的情况。
Pull
Pull 模式更加主动,消费者根据自己的消费能力和需求,主动从 Broker 拉取消息。
- 主动拉取:消费者主动向Broker发送拉取请求,指定要拉取的消息数量和偏移量(或时间戳),Broker 响应包含消息或空结果。
- 位点管理和重平衡:与Push模式类似,每个消费者维护自己的消费进度,并在消费者实例变化时进行重平衡。但是,在Pull模式下,重平衡的逻辑更依赖于消费者的主动参与,消费者需要根据新的队列分配情况调整自己的拉取策略。
Pop
Push / Pull 消费模式的负载均衡是在客户端完成的,性能较高,但也有一些缺陷。
- 客户端代码逻辑复杂,客户端要实现完整的负载均衡,拉消息,位点管理,消费失败后将消息发回 Broker 重试等逻辑。这给多语言客户端的支持造成很大的阻碍。
- 消费者无法无限扩展,当消费者数量扩大到大于队列数量时,有的消费者将无法分配到队列。
- 当某些消费者僵死(hang 住)时,会造成其消费的队列的消息堆积。
在 RocketMQ 5.0 中,Pop 消费模式借助 gRPC 封装的接口,促进了轻量化多语言客户端的实现,无需在各客户端重复实现重平衡逻辑,显著提升了系统的灵活性和扩展性。该设计核心在于将重平衡、位点管理及消息重试等任务转移至服务端处理,有效避免单点故障引起的消息积压,优化了整体消息处理效率和系统的水平扩展能力。