Rocket集群核⼼概念+使用场景

138 阅读9分钟

消息主从复制

RocketMQ官⽅提供了三种集群搭建⽅式。

  • 2主2从异步通信⽅式

    • 使⽤异步⽅式进⾏主从之间的数据复制,吞吐量⼤,但可能会丢消息。
    • 使⽤ conf/2m-2s-async ⽂件夹内的配置⽂件做集群配置。
  • 2主2从同步通信⽅式

    • 使⽤同步⽅式进⾏主从之间的数据复制,保证消息安全投递,不会丢失,但影响吞 吐量
    • 使⽤ conf/2m-2s-sync ⽂件夹内的配置⽂件做集群配置。
  • 2主⽆从⽅式

    • 不存在复制消息,会存在单点故障,且读的性能没有前两种⽅式好。
    • 使⽤ conf/2m-noslave ⽂件夹内的配置⽂件做集群配置

负载均衡

RocketMQ中的负载均衡都在Client端完成,具体来说的话,主要可以分为Producer 端发送消息时候的负载均衡和Consumer端订阅消息的负载均衡。

Producer的负载均衡

Producer端在发送消息的时候,会先根据Topic找到指定的TopicPublishInfo,在获取 了TopicPublishInfo路由信息后,RocketMQ的客户端在默认⽅式下 selectOneMessageQueue()⽅法会从TopicPublishInfo中的messageQueueList中选择⼀ 个队列(MessageQueue)进⾏发送消息。具体的容错策略均在MQFaultStrategy这个 类中定义。这⾥有⼀个sendLatencyFaultEnable开关变量,如果开启,在随机递增取 模的基础上,再过滤掉not available的Broker代理。所谓的"latencyFaultTolerance", 是指对之前失败的,按⼀定的时间做退避。例如,如果上次请求的latency超过 550Lms,就退避3000Lms;超过1000L,就退避60000L;如果关闭,采⽤随机递增 取模的⽅式选择⼀个队列(MessageQueue)来发送消息,latencyFaultTolerance机 制是实现消息发送⾼可⽤的核⼼关键所在。

Snipaste_2023-11-08_14-38-08.png

Consumer的负载均衡

在RocketMQ中,Consumer端的两种消费模式(Push/Pull)都是基于拉模式来获取 消息的,⽽在Push模式只是对pull模式的⼀种封装,其本质实现为消息拉取线程在 从服务器拉取到⼀批消息后,然后提交到消息消费线程池后,⼜“⻢不停蹄”的继续 向服务器再次尝试拉取消息。如果未拉取到消息,则延迟⼀下⼜继续拉取。在两种 基于拉模式的消费⽅式(Push/Pull)中,均需要Consumer端在知道从Broker端的哪 ⼀个消息队列—队列中去获取消息。因此,有必要在Consumer端来做负载均衡,即 Broker端中多个MessageQueue分配给同⼀个ConsumerGroup中的哪些Consumer消 费。

Consumer的负责均衡可以通过consumer的api进⾏设置:

consumer.setAllocateMessageQueueStrategy(newAllocateMessageQueueAveragelyByCircle());

AllocateMessageQueueStrategy接⼝的实现类表达了不同的负载均衡策略:

a.AllocateMachineRoomNearby :基于机房近侧优先级的代理分配策略。可以指定实 际的分配策略。如果任何使⽤者在机房中活动,则部署在同⼀台机器中的代理的消 息队列应仅分配给这些使⽤者。否则,这些消息队列可以与所有消费者共享,因为 没有活着的消费者可以垄断它们

b.AllocateMessageQueueAveragely:平均哈希队列算法

c.AllocateMessageQueueAveragelyByCircle:循环平均哈希队列算法

d.AllocateMessageQueueByConfig:不分配,通过指定MessageQueue列表来消费

e.AllocateMessageQueueByMachineRoom:机房哈希队列算法,如⽀付宝逻辑机房

f.AllocateMessageQueueConsistentHash:⼀致哈希队列算法,带有虚拟节点的⼀致性 哈希环。 注意,在MessageQueue和Consumer之间⼀旦发⽣对应关系的改变,就会触发 rebalance,进⾏重新分配。

消息重试

⾮⼴播模式下,Consumer消费消息失败后,要提供⼀种重试机制,令消息再消费 ⼀次。Consumer消费消息失败通常可以认为有以下⼏种情况:

  • 由于消息本身的原因,例如反序列化失败,消息数据本身⽆法处理(例如话费 充值,当前消息的⼿机号被注销,⽆法充值)等。这种错误通常需要跳过这条 消息,再消费其它消息,⽽这条失败的消息即使⽴刻重试消费,99%也不成 功,所以最好提供⼀种定时重试机制,即过10秒后再重试。
  • 由于依赖的下游应⽤服务不可⽤,例如db连接不可⽤,外系统⽹络不可达等。 遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。这种 情况建议应⽤sleep 30s,再消费下⼀条消息,这样可以减轻Broker重试消息的 压⼒。 在代码层⾯,如果消费者返回的是以下三种情况,则消息会重试消费
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, 
                                                    ConsumeConcurrentlyContext context) {
        for (MessageExt msg : msgs) {
        System.out.println("收到的消息:"+msg);
    }
    return null;
    //return
    ConsumeConcurrentlyStatus.RECONSUME_LATER;
    //抛出异常
    }
 });

消费者返回null,或者返回 ConsumeConcurrentlyStatus.RECONSUME_LATER ,或者抛出异常,都会触发 重试。

关于重试次数

RocketMQ会为每个消费组都设置⼀个Topic名称为“%RETRY%+consumerGroup”的重 试队列(这⾥需要注意的是,这个Topic的重试队列是针对消费组,⽽不是针对每个 Topic设置的),⽤于暂时保存因为各种异常⽽导致Consumer端⽆法消费的消息。 考虑到异常恢复起来需要⼀些时间,会为重试队列设置多个重试级别,每个重试级 别都有与之对应的重新投递延时,重试次数越多投递延时就越⼤。RocketMQ对于重 试消息的处理是先保存⾄Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后 台定时任务按照对应的时间进⾏Delay后重新保存⾄“%RETRY%+consumerGroup”的 重试队列中。

与延迟队列的设置相同,消息默认会重试16次,每次重试的时间间隔如下:

  • 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

重试超过指定次数的消息,将会进⼊到死信队列中 %DLQ%my-consumergroup1 。

死信队列

死信队列⽤于处理⽆法被正常消费的消息。当⼀条消息初次消费失败,消息队列会 ⾃动进⾏消息重试;达到最⼤重试次数后,若消费依然失败,则表明消费者在正常 情况下⽆法正确地消费该消息,此时,消息队列 不会⽴刻将消息丢弃,⽽是将其发 送到该消费者对应的特殊队列中。

RocketMQ将这种正常情况下⽆法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。在 RocketMQ中,可以通过使⽤console控制台对死信队列中的消息进⾏重发来使得消 费者实例再次进⾏消费。

死信队列具备以下特点:

  • RocketMQ会⾃动为需要死信队列的ConsumerGroup创建死信队列。
  • 死信队列与ConsumerGroup对应,死信队列中包含该ConsumerGroup所有相关 topic的死信消息。
  • 死信队列中消息的有效期与正常消息相同,默认48⼩时。
  • 若要消费死信队列中的消息,需在控制台将死信队列的权限设置为6,即可读可 写。

幂等消息

幂等性:多次操作造成的结果是⼀致的。对于⾮幂等的操作,幂等性如何保证?

  1. 在请求⽅式中的幂等性的体现

    1. get:多次get 结果是⼀致的
    2. post:添加,⾮幂等
    3. put:修改:幂等,根据id修改
    4. delete:根据id删除,幂等

    对于⾮幂等的请求,我们在业务⾥要做幂等性保证。

  2. 在消息队列中的幂等性体现

    消息队列中,很可能⼀条消息被冗余部署的多个消费者收到,对于⾮幂等的操作, ⽐如⽤户的注册,就需要做幂等性保证,否则消息将会被重复消费。可以将情况概 括为以下⼏种:

    ⽣产者重复发送:由于⽹络抖动,导致⽣产者没有收到broker的ack⽽再次重发 消息,实际上broker收到了多条重复的消息,造成消息重复

    消费者重复消费:由于⽹络抖动,消费者没有返回ack给broker,导致消费者重 试消费。

    rebalance时的重复消费:由于⽹络抖动,在rebalance重分配时也可能出现消费 者重复消费某条消息。

  3. 如何保证幂等性消费

    1. mysql 插⼊业务id作为主键,主键是唯⼀的,所以⼀次只能插⼊⼀条
    2. 使⽤redis或zk的分布式锁(主流的⽅案)

RocketMQ最佳实践

保证消息顺序消费

为什么要保证消息有序

⽐如有这么⼀个物联⽹的应⽤场景,IOT中的设备在初始化时需要按顺序接收这样 的消息:

  • 设置设备名称
  • 设置设备的⽹络
  • 重启设备使配置⽣效

如果这个顺序颠倒了,可能就没有办法让设备的配置⽣效,因为只有重启设备才能 让配置⽣效,但重启的消息却在设置设备消息之前被消费。

如何保证消息顺序消费

  • 全局有序:消费的所有消息都严格按照发送消息的顺序进⾏消费
  • 局部有序:消费的部分消息按照发送消息的顺序进⾏消费

Snipaste_2023-11-08_15-54-08.png

快速处理积压消息

在rocketmq中,如果消费者消费速度过慢,⽽⽣产者⽣产消息的速度⼜远超于消费 者消费消息的速度,那么就会造成⼤量消息积压在mq中。

如何查看消息积压的情况

  • 在console控制台中可以查看 Snipaste_2023-11-08_15-55-02.png

如何解决消息积压

  • 在这个消费者中,使⽤多线程,充分利⽤机器的性能进⾏消费消息。
  • 通过业务的架构设计,提升业务层⾯消费的性能。
  • 创建⼀个消费者,该消费者在RocketMQ上另建⼀个主题,该消费者将poll下来 的消息,不进⾏消费,直接转发到新建的主题上。新建的主题配上多个 MessageQueue,多个MessageQueue再配上多个消费者。此时,新的主题的多个 分区的多个消费者就开始⼀起消费了。

Snipaste_2023-11-08_15-56-23.png

保证消息可靠性投递

Snipaste_2023-11-08_15-56-59.png

保证消息可靠性投递,⽬的是消息不丢失,可以顺利抵达消费者并被消费。要想实 现可靠性投递,需要完成以下⼏个部分。

  1. ⽣产者发送事务消息

  2. broker集群使⽤Dledger⾼可⽤集群

    1. 第⼀阶段:同步消息到follower,消息状态是uncommitted。follower在收到消息 以后,返回⼀个ack给leader,leader⾃⼰也会返回ack给⾃⼰。leader在收到集 群中的半数以上的ack后开始进⼊到第⼆阶段。
    2. 第⼆阶段:leader发送committed命令,集群中的所有的broker把消息写⼊到⽇ 志⽂件中,此时该消息才表示接收完毕。
  3. 保证消费者的同步消费

    1. 消费者使⽤同步的⽅式,在消费完后返回ack。
  4. 使⽤基于缓存中间件的MQ降级⽅案

    1. 当MQ整个服务不可⽤时,为了防⽌服务雪崩,消息可以暂存于缓存中间件中,⽐ 如redis。待MQ恢复后,将redis中的数据重新刷进MQ中。