RocketMQ 架构设计

一、基础架构

RocketMQ主要由NameServer、Broker、Producer以及Consumer四部分构成。

1. NameServer

主要负责对于源数据的管理,包括了对于Topic和路由信息的管理。NameServer是一个功能齐全的服务器,其角色类似Dubbo中的Zookeeper,但NameServer与Zookeeper相比更轻量。主要是因为每个NameServer节点互相之间是独立的,没有任何信息交互。也就是去中心化,NameServer 之间不通信。

2. Producer

消息生产者,负责产生消息,一般由业务系统负责产生消息。Producer 由用户进行分布式部署,消息由Producer通过多种负载均衡模式发送到Broker集群路由解耦,发送低延时,支持快速失败。

3. Broker

消息中转角色,负责存储消息,转发消息。Broker是具体提供业务的服务器,单个Broker节点与所有的NameServer节点保持长连接及心跳,并会定时将Topic信息注册到NameServer,也就是服务注册,顺带一提底层的通信和连接都是基于Netty实现的。Broker负责消息存储,以Topic为纬度支持轻量级的队列,单机可以支撑上万队列规模,支持消息推拉模型。官网上有数据显示:具有上亿级消息堆积能力,同时可严格保证消息的有序性。

4. Consumer

消息消费者,负责消费消息,一般是后台系统负责异步消费。Consumer也由用户部署,支持PUSH和PULL两种消费模式,拉取式主动从Broker服务器拉消息、主动权由应用控制。推模式下Broker收到数据后会主动推送给消费端,是典型的发布-订阅模式,该消费模式一般实时性较高。支持集群消费和广播消息,提供实时的消息订阅机制。集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。

5. 大致流程

Broker在启动的时候会去向NameServer注册并且定时发送心跳,Producer在启动的时候会到NameServer上去拉取Topic所属的Broker具体地址,也就是服务发现,然后向具体的Broker发送消息。消费者 Consumer 同样先从nameServer获取Broker列表地址,再从Broker中消费消息。

二、消息领域模型

消息领域模型主要分为Message、Topic、Queue、Offset以及Group这几部分。

1. Topic

Topic表示消息的第一级类型,比如一个电商系统的消息可以分为:交易消息、物流消息等。一条消息必须有一个Topic。最细粒度的订阅单位,一个Group可以订阅多个Topic的消息。

2. Tag

Tag表示消息的第二级类型,比如交易消息又可以分为:交易创建消息,交易完成消息等。RocketMQ提供2级消息分类,方便灵活控制。

3. Group

组,一个组可以订阅多个Topic。在同一个消费者组下的消费者,不能同时消费同一个queue,也不可以同时消费不同Topic下的消息。

4. Message Queue

消息的物理管理单位。一个Topic下可以有多个Queue,Queue的引入使得消息的存储可以分布式集群化,具有了水平扩展能力。在 RocketMQ 中,所有消息队列都是持久化,长度无限的数据结构,所谓长度无限是指队列中的每个存储单元都是定长,访问其中的存储单元使用 Offset 来访问,offset 为 java long 类型,64 位,理论上在 100年内不会溢出,所以认为是长度无限,也可以认为 Message Queue 是一个长度无限的数组,Offset 就是下标。

Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。

三、路由链路分析

路由表结构

路由表实际是一个Map,key为Topic名称,value是一个QueueData实例列表(路由表的 value 结构是 List)。

QueueData并不是一个Queue对应一个QueueData,而是一个Broker中该Topic的所有Queue对应一个QueueData。即一个 Broker 只会有一个 QueueData,因为同一个 Topic 在同一个 Broker 上的 Queue 数量是统一配置的。

即,只要涉及到该Topic的Broker,一个Broker对应一个QueueData。QueueData中包含brokerName。同一个 Topic 在同一个 Broker 上的 Queue 数量是统一配置的。

简单来说,路由表的key为Topic名称,value则为所有涉及该Topic的BrokerName列表。

QueueData 不是单个 Queue,而是描述某个 Broker 上该 Topic 的所有 Queue,因此在路由语义上等价于 Topic 关联的 Broker 列表。

1
2
3
4
5
6
7
class QueueData {
private String brokerName;
private int readQueueNums;
private int writeQueueNums;
private int perm;
private int topicSynFlag;
}

Broker列表

实际也是一个Map。key为brokerName,value为BrokerData。一套brokerName名称相同的Master-Slave小集群对应一个BrokerData。

BrokerData中包含brokerName及一个map。该map的key为brokerId,value为该broker对应的地址。brokerId为 0 表示该broker为Master,非 0 表示Slave。

QueueData 解决”发到哪个 Broker + 有多少队列”,BrokerData 解决”这个 BrokerName 对应哪些真实 Broker 实例地址(Master/Slave)”。

1
2
3
4
Topic
└── BrokerName(逻辑 Broker)
└── BrokerId(物理 Broker 实例:Master / Slave)
└── Address(IP:Port)
1
2
3
4
class BrokerData {
String brokerName;
Map<Long, String> brokerAddrs; // brokerId -> address
}

路由设计总结

RocketMQ 的路由分为两部分:

  • QueueData 负责 Topic 到逻辑 Broker(brokerName)的映射
  • BrokerData 负责逻辑 Broker 到物理 Broker 实例(Master/Slave 地址)的映射

这种设计将 Topic 路由与 Broker 高可用解耦,避免主从切换引发大规模路由变更。

四、Queue选择策略

4.1 轮询策略

默认选择算法。该算法保证了每个Queue中可以均匀的获取到消息。该算法存在一个问题:由于某些原因,在某些Broker上的Queue可能投递延迟较严重。从而导致Producer的缓存队列中出现较大的消息积压,影响消息的投递性能。

4.2 最小投递延迟策略

该算法会统计每次消息投递的时间延迟,然后根据统计出的结果将消息投递到时间延迟最小的Queue。如果延迟相同,则采用轮询算法投递。该算法可以有效提升消息的投递性能。该算法也存在一个问题:消息在Queue上的分配不均匀。投递延迟小的Queue其可能会存在大量的消息。而对该Queue的消费者压力会增大,降低消息的消费能力,可能会导致MQ中消息的堆积。

五、负载均衡

Producer负载均衡

Producer发送消息时,默认会轮询目标Topic下的所有MessageQueue往不同的MessageQueue上发送消息,以达到让消息平均落在不同的queue上的目的。(不仅往不同的队列发送消息,而且往不同的broker发送)

而由于MessageQueue是分布在不同的Broker上的,所以消息也会发送到不同的broker上。

同时生产者在发送消息时,可以指定一个MessageQueueSelector。通过这个对象来将消息发送到自己指定的MessageQueue上。这样可以保证消息局部有序。

Consumer负载均衡

Consumer也是以MessageQueue为单位来进行负载均衡。分为集群模式和广播模式。

RocketMQ 中 Producer 和 Consumer 的负载均衡都是以 MessageQueue 为最小单位。

Producer 在发送消息时,会从 NameServer 拉取 Topic 路由信息,获取该 Topic 下的所有 MessageQueue,默认采用轮询算法在 MessageQueue 维度进行负载均衡。由于 MessageQueue 分布在不同 Broker 上,因此消息会自然地均匀发送到不同 Broker。同时,Producer 支持通过 MessageQueueSelector 让用户参与路由决策,常见做法是基于业务 Key 进行 hash,从而保证同一 Key 内的消息顺序性。

Consumer 的负载均衡发生在同一个 Consumer Group 内,同样以 MessageQueue 为单位。在集群消费模式下,每个 MessageQueue 只会被 Group 内的一个 Consumer 实例消费;而在广播消费模式下,每个 Consumer 都会消费 Topic 下的全部 MessageQueue,不存在负载均衡和 Rebalance 过程。