RocketMQ 分布式集群是通过 Master 和 Slave 的配合达到高可用性的。
Master 和 Slave 的区别:在 Broker 的配置文件中,参数 brokerId 的值为 0 表明这个 Broker 是 Master,大于 0 表明这个 Broker 是 Slave,同时 brokerRole 参数也会说明这个 Broker 是 Master 还是 Slave。
Master 角色的 Broker 支持读和写,Slave 角色的 Broker 仅支持读,也就是 Producer 只能和 Master 角色的 Broker 连接写入消息;Consumer 可以连接 Master 角色的 Broker,也可以连接 Slave 角色的 Broker 来读取消息。
也就是只有一个 master 节点,称不上是集群,一旦这个 master 节点宕机,那么整个服务就不可用。
多个 master 节点组成集群,单个 master 节点宕机或者重启对应用没有影响。
优点:所有模式中性能最高。在多主的架构体系下,无论使用客户端还是管理界面创建Topic,一个Topic都会创建个份队列在多主中(默认是 4 个的话,双主就会有 8 个队列), 每台主 4 个队列,所以双主可以提高性能,一个 Topic 的分布在不同的 master,方便进行横向拓展。
缺点:单个 master 节点宕机期间,未被消费的消息在节点恢复之前不可用,消息的实时性就受到影响。
在多 master 模式的基础上,每个 master 节点都有至少一个对应的 slave。master 节点可读可写,但是 slave 只能读不能写,类似于 mysql 的主备模式。
优点:一般情况下都是master消费,在 master 宕机或超过负载时,消费者可以从 slave 读取消息,消息的实时性不会受影响,性能几乎和多 master 一样。
缺点:使用异步复制的同步方式有可能会有消息丢失的问题。(Master 宕机后,生产者发送的消息没有消费完,同时到 Slave 节点的数据也没有同步完)
对数据要求较高的场景,主从同步复制方式,保存数据热备份,通过异步刷盘方式,保证 rocketMQ 高吞吐量。
优点:主从同步复制模式能保证数据不丢失。
缺点:发送单个消息响应时间会略长,性能相比异步复制低 10%左右。
类似于 Zookeeper 的集群选举模式
生产时首先将消息写入到 MappedFile,内存映射文件,然后根据刷盘策略刷写到磁盘。大致的步骤可以理解成使用 MMAP 中的 MappedByteBuffer 中实际用 flip().
RocketMQ 的刷盘是把消息存储到磁盘上的,这样既能保证断电后恢复, 又可以让存储的消息量超出内存的限制。RocketMQ 为了提高性能,会尽可 能地保证磁盘的顺序写。消息在通过 Producer 写入 RocketMQ 的时候,有两种写磁盘方式,同步刷盘和异步刷盘。
SYNC_FLUSH(同步刷盘):生产者发送的每一条消息都在保存到磁盘成功后才返回告诉生产者成功。这种方式不会存在消息丢失的问题,但是有很大的磁盘 IO 开销,性能有一定影响。
ASYNC_FLUSH(异步刷盘):生产者发送的每一条消息并不是立即保存到磁盘,而是暂时缓存起来,然后就返回生产者成功。随后再异步的将缓存数据保存到磁盘,有两种情况:
集群环境下需要部署多个 Broker,Broker 分为两种角色:一种是 master,即可以写也可以读,其 brokerId=0,只能有一个;另外一种是 slave,只允许读,其 brokerId 为非 0。一个 master 与多个 slave 通过指定相同的 brokerClusterName 被归为一个 broker set(broker 集)。通常生产环境中,我们至少 需要 2 个 broker set。Slave 是复制 master 的数据。一个 Broker 组有 Master 和 Slave,消息需要从 Master 复制到 Slave 上,有同步和异步两种复制方式。
主从同步复制方式(Sync Broker):生产者发送的每一条消息都至少同步复制到一个 slave 后才返回告诉生产者成功,即“同步双写”
在同步复制方式下,如果 Master 出故障, Slave 上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量。
主从异步复制方式(Async Broker):生产者发送的每一条消息只要写入 master 就返回告诉生产者成功。然后再“异步复制”到 slave。
在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但是如果 Master 出了故障,有些数据因为没有被写 入 Slave,有可能会丢失;
同步复制和异步复制是通过 Broker 配置文件里的 brokerRole 参数进行设置的,这个参数可以被设置成 ASYNC_MASTER、 SYNC_MASTER、SLAVE 三个值中的一个。
brokerId=0 代表主
brokerId=1 代表从(大于 0 都代表从)
brokerRole=SYNC_MASTER 同步复制(主从)
brokerRole=ASYNC_MASTER 异步复制(主从)
flushDiskType=SYNC_FLUSH 同步刷盘
flushDiskType=ASYNC_FLUSH 异步刷盘
参考官方文档:https://github.com/apache/rocketmq/blob/master/docs/cn/operation.md
在创建 Topic 的时候,把 Topic 的多个 Message Queue 创建在多个 Broker 组上(相同 Broker 名称,不同 brokerId 的机器组成一个 Broker 组),这样当一个 Broker 组的 Master 不可用后,其他组的 Master 仍然可用,Producer 仍然可以发送消息。
RocketMQ 目前不支持把 Slave 自动转成 Master,如果机器资源不足, 需要把 Slave 转成 Master,则要手动停止 Slave 角色的 Broker,更改配置文件, 用新的配置文件启动 Broker。
如果是 BrokerA 宕机,上一次路由选择的是 BrokerA 中的 Q4,那么再次重发的队列选择是 BrokerA 中的 Q1,这里的问题就是消息发送很大可能再次失败。即尽管会重试 2 次,但都是发送给同一个 Broker 处理,此过程会显得不那么靠谱,即大概率还是会失败,那这样重试的意义将大打折扣。
为了解决该问题,引入了故障规避机制,在消息重试的时候,会尽量规避上一次发送的 Broker。回到上述示例,当消息发往 BrokerA Q4 队列时返回发送失败,那重试的时候,会先排除 BrokerA 中所有队列,选择 BrokerB Q1 队列,以增大消息发送的成功率。
RocketMQ 提供了两种规避策略,该参数由 sendLatencyFaultEnable 控制,默认为不开启。
sendLatencyFaultEnable 设置为 false:默认值,不开启,**延迟规避策略只在重试时生效。**例如在一次消息发送过程中如果遇到消息发送失败,重试时规避 BrokerA,但是在下一次消息发送时,还是会选择 BrokerA 的队列进行发送,只有继续发送失败后,重试时才规避 BrokerA。
sendLatencyFaultEnable 设置为 true:开启延迟规避机制。一旦消息发送失败会将 BrokerA “悲观”地认为在接下来的一段时间内都不可用,在未来某一段时间内所有的客户端不会向该 Broker 发送消息。这个延迟时间就是通过 notAvailableDuration、latencyMax 共同计算的,就首先计算本次消息发送失败所耗的时延,然后对应 latencyMax 中哪个区间,即计算在 latencyMax 的下标,然后返回 notAvailableDuration 同一个下标对应的延迟值。
如果所有的 Broker 都触发了故障规避,并且 Broker 只是那一瞬间压力大,那岂不是明明存在可用的 Broker,但经过你这样规避,反倒是没有 Broker 可用来,那岂不是更糟糕了。所以 RocketMQ 默认不启用 Broker 故障延迟机制。
在 Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave 读,当 Master 不可用或者繁忙的时候,Consumer 会被自动切换到从 Slave 读。 有了自动切换 Consumer 这种机制,当一个 Master 角色的机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 程序。这就达到 了消费端的高可用性。
Master 不可用这个很容易理解,那什么是 Master 繁忙呢?这个繁忙其实是 RocketMQ 服务器的内存不够导致的。
源码分析:org.apache.rocketmq.store. DefaultMessageStore#getMessage 方法:
当前需要拉取的消息已经超过常驻内存的大小,表示主服务器繁忙,此时才建议从从服务器拉取。
消费端如果发生消息失败,没有提交成功,消息默认情况下会进入重试队列中。
注意重试队列的名字其实是跟消费群组有关,不是主题,因为一个主题可以有多个群组消费,所以要注意
对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。
所以玩顺序消息时。consume 消费消息失败时,不能返回 reconsume_later,这样会导致乱序,应该返回 suspend_current_queue_a_moment,意思是先等一会,一会儿再处理这批消息,而不是放到重试队列里。
无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。
如果消息重试 16 次后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小 时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递。
注意: 一条消息无论重试多少次,这些重试消息的 Message ID 不会改变。
集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种):
集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回 CONSUME_SUCCESS,此后这条消息将不会再重试。
消息队列 RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略:
当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正 确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列 (Dead-Letter Queue)。
死信消息具有以下特性:
死信队列具有以下特性:
在控制台查询出现死信队列的主题信息
在消息界面根据主题查询死信消息,选择重新发送消息。
一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次。
Producer 端,每个实例在发消息的时候,默认会轮询所有的 message queue 发送,以达到让消息平均落在不同的 queue 上。而由于 queue 可以散落在不同 的 broker,所以消息就发送到不同的 broker 下。
在集群消费模式下,每条消息只需要投递到订阅这个 topic 的 Consumer Group 下的一个实例即可。RocketMQ 采用主动拉取的方式拉取并消费消息,在拉 取的时候需要明确指定拉取哪一条 message queue。
而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照 queue 的数量和实例的数量平均分配 queue 给每个实例。 默认的分配算法是 AllocateMessageQueueAveragely。
还有另外一种平均的算法是 AllocateMessageQueueAveragelyByCircle,也是平均分摊每一条 queue,只是以环状轮流分 queue 的形式
需要注意的是,集群模式下,queue 都是只允许分配只一个实例,这是由于如果多个实例同时消费一个 queue 的消息,由于拉取哪些消息是 consumer 主 动控制的,那样会导致同一个消息在不同的实例下被消费多次,所以算法上都是一个 queue 只分给一个 consumer 实例,一个 consumer 实例可以允许同 时分到不同的 queue。
通过增加 consumer 实例去分摊 queue 的消费,可以起到水平扩展的消费能力的作用。而有实例下线的时候,会重新触发负载均衡,这时候原来分配到的 queue 将分配到其他实例上继续消费。
但是如果 consumer 实例的数量比 message queue 的总数量还多的话,多出来的 consumer 实例将无法分到 queue,也就无法消费到消息,也就无法起到分 摊负载的作用了。所以需要控制让 queue 的总数量大于等于 consumer 的数量。
由于广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以也就没有消息被分摊消费的说法。 在实现上,其中一个不同就是在 consumer 分配 queue 的时候,所有 consumer 都分到所有的 queue。