1 前言
2 RocketMQ中的高可用机制
RocketMQ分布式集群是通过 Master
和 Slave
的配合达到高可用性的。
Master和Slave的区别:
- 在Broker的配置文件中,参数
brokerId
的值为0
,表明这个Broker是Master
,大于0
表明这个Broker是Slave
,同时brokerRole
参数也会说明这个Broker是Master
还是Slave
。Master
角色的Broker
支持读和写
,Slave
角色的Broker仅支持读
。
2.1 集群部署模式
2.1.1 单master 模式
只有一个 master 节点,称不上是集群,一旦这个 master 节点宕机,那么整个服务就不可用。
- 优点: 本地开发测试,配置简单,同步刷盘消息不会丢失。
- 缺点: 不可靠,如果宕机会导致服务不可用。
2.1.2 多master 模式
多个 master 节点组成集群,单个 master 节点宕机或者重启对应用没有影响
。
- 优点:所有模式中性能最高(一个Topic 的可以分布在不同的master,进行横向拓展)。
- 缺点:单个master 节点宕机期间,
未被消费的消息
在节点恢复之前不可用
,消息的实时性就受到影响。
2.1.3 多master 多slave + 异步复制模式
从节点
(Slave)就是复制主节点的数据,对于生产者完全感知不到,对于消费者正常情况下也感知不到。(只有当 Master 不可用或者繁忙的时候, Consumer 会被自动切换到从 Slave 读。)
在多 master 模式的基础上,每个 master 节点都有至少一个
对应的 slave。master 节点可读可写,但是 slave 只能读不能写,类似于 mysql 的主备模式。
- 优点: 一般情况下都是master消费,在master 宕机或超过负载时,消费者可以从slave 读取消息,消息的实时性不会受影响,性能几乎和多master一样。
- 缺点:使用异步复制的同步方式有可能会有消息丢失的问题。(Master 宕机后,生产者发送的消息没有消费完,同时到Slave 节点的数据也没有同步完)。
2.1.4 多master 多slave 主从同步复制+异步刷盘(最优推荐)
- 优点:主从同步复制模式能保证数据不丢失。
- 缺点:发送单个消息响应时间会略长,性能相比异步复制
低10%左右
。
2.1.5 DLedger[^1]
类似于Zookeeper
的集群选举模式
(raft协议)。
DLedger
是一套基于Raft协议
的分布式日志存储组件
,也是 RocketMQ 实现新的高可用多副本架构的关键。
RocketMQ 4.5
版本发布后,可以采用 RocketMQ on DLedger
方式进行部署。DLedger CommitLog
代替了原来的 CommitLog
,使得 CommitLog 拥有了选举复制
能力,然后通过角色透传的方式,raft 角色透传给外部 broker 角色,leader 对应原来的 master,follower 和 candidate 对应原来的 slave。
因此 RocketMQ 的 broker 拥有了自动故障转移
的能力,在一组 broker 中如果 Master 挂了,能够依靠 DLedger 自动选主能力重新选出一个 leader,然后通过角色透传变成新的 Master。
但是存在一些缺点:
- 想要具备选举切换的能力,单组 Broker 内的副本数必须
3 副本及以上
(Raft协议决定); - 副本
ACK
需要严格遵循 Raft 协议多数派的限制,3 副本需要 2 副本 ACK 后才能返回,5 副本需要 3 副本 ACK 后才能返回,副本越多可能耗时也可能越长。(这个也是最重要的一点); - DLedger 模式下,由于存储库使用了
OpenMessaging DLedger
存储,因此无法复用 RocketMQ 原生的存储和复制的能力(比如 transientStorePool 和零拷贝能力),且对维护造成了困难。
在RocketMQ5.0
版本新增了DLedger Controller模式
来解决上面对的痛点。
2.1.6 DLedger Controller模式架构[^2]
DLedger Controller模式的核心思想:将其作为一个选主组件,并且是一个可选择
、松耦合
的组件。当部署 DLedger Controller 组件后,原本 Master-Slave 部署模式下 Broker 组就拥有 Failover
能力。
其中:
DledgerController
:利⽤ DLedger ,构建⼀个保证元数据强⼀致性的 DLedger Controller 控制器,利⽤ Raft 选举会选出⼀个 Active DLedger Controller 作为主控制器,DLedger Controller 可以内嵌在 Nameserver中,也可以独立的部署。其主要作用是,用来存储和管理 Broker 的 SyncStateSet 列表,并在某个 Broker 的 Master Broker 下线或⽹络隔离时,主动发出调度指令来切换 Broker 的 Master。SyncStateSet
:主要表示⼀个 broker 副本组中跟上 Master 的 Slave 副本加上 Master 的集合。主要判断标准是 Master 和 Slave 之间的差距。当 Master 下线时,我们会从 SyncStateSet 列表中选出新的 Master。 SyncStateSet 列表的变更主要由 Master Broker 发起。Master通过定时任务判断和同步过程中完成 SyncStateSet 的Shrink 和 Expand,并向选举组件 Controller 发起 Alter SyncStateSet 请求。AutoSwitchHAService
:一个新的 HAService,在 DefaultHAService 的基础上,支持 BrokerRole 的切换,支持 Master 和 Slave 之间互相转换 (在 Controller 的控制下) 。此外,该 HAService 统一了日志复制流程,会在 HA HandShake 阶段进行日志的截断。ReplicasManager
:作为一个中间组件,起到承上启下的作用。对上,可以定期同步来自 Controller 的控制指令,对下,可以定期监控 HAService 的状态,并在合适的时间修改 SyncStateSet。ReplicasManager 会定期同步 Controller 中关于该 Broker 的元数据,当 Controller 选举出一个新的 Master 的时候,ReplicasManager 能够感知到元数据的变化,并进行 BrokerRole 的切换。
核心设计[^3]:
详细可参考设计思想.
Controller 部署有两种方式。
- 嵌入于 NameServer 进行部署
- 独立部署,需要单独部署 Controller 组件
详情参考主备自动切换模式部署.
2.2 刷盘与主从同步
2.2.1 刷盘
刷盘,即将数据从内存写入磁盘。在RocketMQ中就是将消息落地,这样既能保证断电后恢复
, 又可以让存储的消息量超出内存的限制
。RocketMQ 为了提高性能,会尽可能地保证磁盘的顺序写
。消息在通过 Producer 写入 RocketMQ 的时候,有两种写磁盘方式。
2.2.1.1 同步刷盘
生产者发送的每一条消息都在保存到磁盘成功后
才返回告诉生产者成功。这种方式不会存在消息丢失的问题,但是有很大的磁盘IO开销,性能有一定影响。
2.2.1.2 异步刷盘
生产者发送的每一条消息并不是立即保存到磁盘
,而是暂时缓存
起来,然后就返回生产者成功。随后再异步
的将缓存数据保存
到磁盘。
有两种情况:
定期
将缓存中更新的数据进行刷盘- 当缓存中更新的数据条数达到
某一设定值
后进行刷盘。
这种异步的方式会存在消息丢失
(在还未来得及同步到磁盘的时候宕机),但是性能很好。默认是这种模式
。
2.2.2 主从同步
集群环境下需要部署多个 Broker,Broker 分为两种角色:
- 一种是 master,
既可以写也可以读
,其brokerId=0
,只能有一个
; - 另外一种是 slave,
只允许读
,其brokerId
为非 0
。
一个 master 与多个 slave 通过指定相同的brokerClusterName
被归为一个 broker set
(broker 集)。通常生产环境中,我们至少需要 2 个 broker set
。Slave 用于复制 master 的数据。
一个 Broker 组有 Master 和 Slave,消息需要从 Master 复制到 Slave 上,有同步
和异步
两种复制方式。
2.2.2.1 同步复制
生产者发送的每一条消息都至少同步复制到一个 slave 后
才返回告诉生产者成功,即同步双写
。
在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟
,降低系统吞吐量
。
2.2.2.2 异步复制
生产者发送的每一条消息只要写入 master 就返回告诉生产者成功。然后再异步复制
到 slave。
在异步复制方式下,系统拥有较低的延迟
和较高的吞吐量
,但是如果 Master 出了故障,有些数据因为没有被写入 Slave,有可能会丢失
。
2.2.3 配置参数及意义
broker参数 | 意义 |
---|---|
brokerId=0 | 代表主 |
brokerId=1 | 代表从(大于 0 都代表从) |
brokerRole=SYNC_MASTER | 同步复制(主从) |
brokerRole=ASYNC_MASTER | 异步复制(主从) |
flushDiskType=SYNC_FLUSH | 同步刷盘 |
flushDiskType=ASYNC_FLUSH | 异步刷盘 |
3 消息生产的高可用机制
在创建Topic的时候,把Topic的多个Message Queue
创建在多个Broker组
上(相同Broker名称,不同 brokerId的机器组成一个Broker组),这样当一个Broker组的Master不可用后,其他组的Master仍然可用,Producer仍然可以发送消息。
3.1 高可用消息生产流程
TopicA
创建在双主BrokerA
和BrokerB
中,每一个Broker 中有4 个队列
;- 选择队列时,默认使用
轮训
的方式,比如发送一条消息A 时,选择BrokerA
中的Q4
; - 如果发送成功,消息A发送结束;
- 如果消息发送失败,默认会采用
重试机制
,这里有一个规避策略
(默认配置):- 默认
不启用
Broker故障延迟机制(规避策略):如果是BrokerA宕机,上一次路由选择的是BrokerA中的Q4,那么再次重发的队列选择是BrokerA中的Q1。但是这里的问题就是消息发送很大可能再次失败,引发再次重复失败,带来不必要的性能损耗。为什么会默认这么设计?
1、某一时间段,从NameServer
中读到的路由中包含了不可用的主机
;
2、不正常的路由信息也是只是一个短暂的时间而已。生产者
每隔30s更新一次路由信息,而NameServer
认为broker不可用需要经过120s
。所以生产者要发送时认为broker不正常
(从NameServer拿到)和实际Broker不正常
有延迟。
- 默认
为什么默认不启用?
如果所有的 Broker 都触发了故障规避
,并且 Broker 只是那一瞬间压力大
,那岂不是明明存在可用的 Broker,但经过你这样规避,反倒是没有 Broker 可用来,那岂不是更糟糕了。
- 启用Broker 故障延迟机制:开启延迟规避机制,一旦消息发送失败(不是重试的)会将 BrokerA“悲观”地认为在接下来的一段时间内
不可用
,在未来某一段时间内所有的客户端不会向该 Broker 发送消息。
注意,这里的规避仅仅只针对
消息重试
,例如在一次消息发送过程中如果遇到消息发送失败,规避 BrokerA,但是在下一次消息发送时,即再次调用DefaultMQProducer
的send 方法
发送消息时,还是会选择 BrokerA 的消息进行发送,只有继续发送失败后,重试时再次规避 BrokerA。
4 消息消费的高可用机制
在Consumer的配置文件中,并不需要设置是从Master读还是从Slave读,当Master不可用
或者繁忙
的时候,Consumer会被自动切换
到从Slave 读。有了自动切换Consumer这种机制,当一个Master角色的机器出现故障后,Consumer仍然可以从Slave读取消息,不影响Consumer程序。这就达到了消费端的高可用性。
什么是Master繁忙呢?
这个繁忙其实是RocketMQ服务器的内存不够
导致的。
比如,10w的消息,只消费了2w,还有8w待消费,而master最多只能用10GB的OS cache,只能缓存5w条数据,还有3w就要去磁盘中拉取,此时他就会认为可能是master负载太高了,你去slave拉取吧。
4.1 消息消费重试
消费端如果发生消息失败,没有提交成功,消息默认情况下
会进入重试队列中。
重试队列的名字其实是跟消费群组
有关,不是主题
,因为一个主题可以有多个群组消费。
4.1.1 顺序消息的重试
对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断
进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。
在玩顺序消息时。consumer消费消息失败时,不能返回reconsume_later
,这样会导致乱序
,应该返回suspend_current_queue_a_moment
,意思是先等一会,一会儿再处理这批消息,而不是放到重试队列里。
4.1.2 无序消息的重试
对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态
达到消息重试的结果。
无序消息的重试只针对集群消费方式
生效;广播方式不提供
失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。
4.1.3 重试次数
如下表所示:
第几次重试 | 与上次重试的间隔时间 | 第几次重试 | 与上次重试的间隔时间 |
---|---|---|---|
1 | 10 秒 | 9 | 7 分钟 |
2 | 30 秒 | 10 | 8 分钟 |
3 | 1 分钟 | 11 | 9 分钟 |
4 | 2 分钟 | 12 | 10 分钟 |
5 | 3 分钟 | 13 | 20 分钟 |
6 | 4 分钟 | 14 | 30 分钟 |
7 | 5 分钟 | 15 | 1 小时 |
8 | 6 分钟 | 16 | 2 小时 |
如果消息重试 16次
后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟
之内进行 16 次重试,超过这个时间范围消息将不再重试投递。
注意: 一条消息无论重试多少次,这些重试消息的
Message ID 不会改变
。
4.1.4 自定义消息最大重试次数
消息队列 RocketMQ 允许 Consumer 启动的时候设置最大重试次数
,重试时间间隔将按照如下策略:
- 最大重试次数
小于等于 16 次
,则重试时间间隔同上表描述。 - 最大重试次数
大于 16 次
,超过 16 次的重试时间间隔均为每次 2 小时
。
另外:
- 消息最大重试次数的设置对
相同Group ID
下的所有 Consumer 实例有效。 - 如果只对
相同Group ID
下两个 Consumer 实例中的其中一个设置了MaxReconsumeTimes
,那么该配置对两个 Consumer 实例均生效。 - 配置采用
覆盖
的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置。
4.2 死信队列
当一条消息初次
消费失败,消息队列 RocketMQ 会自动
进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下
无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息
(Dead-Letter Message),存储死信消息的特殊队列称为死信队列
(Dead-Letter Queue)。
4.2.1 特性
死信消息
具有以下特性:
- 不会再被消费者正常消费。
- 有效期与正常消息相同,均为
3天
,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。
死信队列
具有以下特性:
- 不会再被消费者正常消费。
- 一个死信队列对应一个
Group ID
, 而不是对应单个消费者实例。 - 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
- 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。
一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要您对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台
重新发送
该消息,让消费者重新消费一次。
5 负载均衡
5.1 Producer负载均衡
Producer端,每个实例在发消息的时候,默认会轮询
所有的message queue
发送,以达到让消息平均落在不同的queue上。而由于queue可以散落在不同的broker,所以消息就发送到不同的broker下,如下图:
5.2 Consumer负载均衡
5.2.1 集群模式
在集群消费模式
下,每条消息只需要投递到订阅这个topic的Consumer Group
下的一个实例
即可。RocketMQ采用主动拉取
的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message queue。
而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照queue的数量和实例的数量平均分配queue给每个实例。
默认的分配算法是AllocateMessageQueueAveragely
,还有另外一种平均的算法是AllocateMessageQueueAveragelyByCircle
,也是平均分摊每一条queue,只是以环状轮流分queue的形式。
还有其他分配策略,比如:
AllocateMessageQueueByConfig
、AllocateMessageQueueByMachineRoom
、AllocateMessageQueueConsistentHash
、AllocateMachineRoomNearby
。
AVG_BY_CIRCLE
, 跟AVG
类似,只是分到的queue不是连续的。比如一共12个Queue,3个consumer,则第一个consumer接收queue1,4,7,9的消息;第二个接收2,5,8,11;第三个接收3,6,9,12。“我一个,你一个,他一个”,AVG则是“前四个是我的,中间四个是你的,剩下的是他的”;CONSISTENT_HASH
,使用一致性hash算法来分配Queue,用户需自定义虚拟节点的数量;MACHINE_ROOM
,将queue先按照broker划分几个computer room
,不同的consumer只消费某几个broker上的消息;CONFIG
,用户启动时指定消费哪些Queue的消息。
需要注意的是,集群模式下,queue
都是只允许分配只一个实例,这是由于如果多个实例同时消费一个queue的消息,由于拉取哪些消息是consumer主动控制的,那样会导致同一个消息在不同的实例下被消费多次
,所以算法上都是一个queue只分给一个consumer实例,一个consumer实例可以允许同时分到不同的queue。
通过增加consumer实例去分摊queue的消费,可以起到水平扩展的消费能力的作用。而在有实例下线的时候,会重新触发负载均衡,这时候原来分配到的queue将分配到其他实例上继续消费。
如果consumer 实例的数量比message queue 的总数量还多的话,多出来的consumer 实例将无法分到queue,因此需要控制让
queue 的总数量大于等于consumer 的数量
。
5.2.2 广播模式
由于广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以也就没有消息被分摊消费的说法。
6 参考文献
[^1] Docker 部署 RocketMQ Dledger 集群模式( 版本v4.7.0)
[^2] RocketMQ5.0 DLedger Controller模式
[^3] 主备自动切换模式部署