我服了,RocketMQ消费者负载均衡内核是这样设计的

前言

上文分析了RocketMQ消费者消费消息核心原理,其中在消费者端,有一个重量级的组件:负载均衡组件, 他负责给消费者分配需要拉取的队列信息。一个Topic下可能会有很多逻辑队列,而消费者又有多个,这样不同的消费者到底消费哪个呢?如果消费者或者队列扩缩容,Topic下的队列又该分配给谁呢?这些时候负载均衡策略就有他的用武之地了。

本文将会从源码层面来剖析上面的问题。

RocketMQ负载均衡服务启动时机

RocketMQ的负载均衡服务,RebalanceService,他是一个线程任务。消费者客户端启动时候,会启动RebalanceService线程,从而触发其run方法运行。

doRebalance函数,对每个消费者组执行负载均衡操作。 也就是一个负载均衡服务是对一个消费者组负责的,那么我们可以想到对不同的消费者组使用不同负载均衡策略

java 复制代码
public void doRebalance() {
    //每个消费者组都有负载均衡
    for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
        MQConsumerInner impl = entry.getValue();
        if (impl != null) {
            try {
                impl.doRebalance();
            } catch (Throwable e) {
                log.error("doRebalance exception", e);
            }
        }
    }
}

最终是对每个topic维度进行负载均衡

java 复制代码
public void doRebalance(final boolean isOrder) {
    Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
    if (subTable != null) {
        for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
            final String topic = entry.getKey();
            try {
                this.rebalanceByTopic(topic, isOrder);
            } catch (Throwable e) {
                if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    log.warn("rebalanceByTopic Exception", e);
                }
            }
        }
    }

    this.truncateMessageQueueNotMyTopic();
}

负载均衡处理逻辑,分为广播消息和集群消息单独处理。由于广播消息每个消费者实例都需要消费到,因此逻辑会简单点,我们主要关注集群消息模式。

java 复制代码
private void rebalanceByTopic(final String topic, final boolean isOrder) {
        switch (messageModel) {
            case BROADCASTING: {
                Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
                if (mqSet != null) {
                    boolean changed = this.updateProcessQueueTableInRebalance(topic, mqSet, isOrder);
                    if (changed) {
                        this.messageQueueChanged(topic, mqSet, mqSet);
                        
                    }
                }
                break;
            }
            case CLUSTERING: {
                Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
                List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
                if (null == mqSet) {
                    if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                        log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic);
                    }
                }

                if (mqSet != null && cidAll != null) {
                    List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
                    mqAll.addAll(mqSet);
                    Collections.sort(mqAll);
                    Collections.sort(cidAll);
                    //负载均衡组件
                    AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
                    //负载均衡结果
                    List<MessageQueue> allocateResult = strategy.allocate(
                            this.consumerGroup,
                            this.mQClientFactory.getClientId(),
                            mqAll,
                            cidAll);
                    
                    Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
                    if (allocateResult != null) {
                        allocateResultSet.addAll(allocateResult);
                    }
                    //负载均衡执行结束后,判断是否有新的消费策略变化,更新拉取策略
                    boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
                    if (changed) {
                        //发送更新通知
                        this.messageQueueChanged(topic, mqSet, allocateResultSet);
                    }
                }
                break;
            }
            default:
                break;
        }
    }

代码逻辑可以看出负载均衡的主流程,主要做了4件事情

其中比较重要的是具体的负载均衡策略,他关系着哪些队列是当前消费者需要消费的。下面我们看下具体实现。

负载均衡策略原理

RocketMQ中共有6种负载均衡策略。

策略顶层接口,分配消息队列顶层定义

java 复制代码
List<MessageQueue> allocate(
    final String consumerGroup, //消费者组
    final String currentCID, //当前消费者id
    final List<MessageQueue> mqAll, //所有的队列
    final List<String> cidAll //所有的消费者
);

Push消息客户端使用的队列平均分配策略。

为了说明这种分配算法的分配规则,现在对 16 个队列,进行编号,用 q0~q15 表示, 消费者用 c0~c2 表示。 AllocateMessageQueueAveragely分配算法的队列负载机制如下:

c0:q0 q1 q2 q3 q4 q5

c1: q6 q7 q8 q9 q10

c2: q11 q12 q13 q14 q15

其算法的特点是用总数除以消费者个数,余数按消费者顺序分配给消费者,故 c0 会多 分配一个队列,而且队列分配是连续的

java 复制代码
        List<String> result = new ArrayList<>();
        // 消息队列%消费者 是否能够正好整数倍分配完整
        int mod = mqAll.size() % cidAll.size();
        //平均每个消费者消费的队列大小
        int averageSize = 0;

        //计算当前消费者需要消费的队列大小
        //如果需要消费的队列数 小于 消费者数量 则每个(编号小于队列编号的)消费者需要消费1个队列
        if (mqAll.size() <= cidAll.size()) {
            averageSize = 1;
        } else {
            //如果队列不能被正好整数被分配完,并且当前消费者需要比整数个消费多一个
            if (mod > 0 && index < mod) {
                averageSize = mqAll.size() / cidAll.size() + 1;
            } else {
                ////如果队列不能被正好整数被分配完,并且当前消费者不需要比整数个消费多一个(当前消费者消费队列数不加1),刚好消费整数个
                averageSize = mqAll.size() / cidAll.size();
            }
        }


        //计算消费者需要开始消费的队列下标。
        int startIndex;
        //消费者不能正好整数倍消费完成,并且需要多消费一个队列的情况下 比如是第3个消费者 平均大小是1 则开始位置是2*1=2
        if (mod > 0 && index < mod) {
          //计算当前消费者的 需要消费队列大小
            startIndex = index * averageSize;
        } else {
            // 总共3个队列 2个消费者 mod = 1    则第2个消费者的开始位置为 1*1 + 1 = 2
            // 总共5个队列 3个消费者 mod = 2    则第2个消费者的开始位置为 2*1 + 1 = 3
            startIndex = index * averageSize + mod;
        }
        //  startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
        //消费队列的范围  总共3个队列 2个消费者 mode = 1    则第2个消费者的开始位置为 1*1 + 1 = 2
        //如果消费者需要消费的数量不会加1 则消费范围为averageSize, 但是也可能存在一个消费者
        System.out.println(mqAll.size() - startIndex);
        System.out.println(averageSize);
        //范围比较 存在一种情况 消费者数量比队列数量多的情况 则存在部分消费者消费不到队列情况,
        // 则会使得 averageSize=1 但是 (mqAll.size() - startIndex) =0的情况 这样就范围就是0了。
        int range = Math.min(averageSize, mqAll.size() - startIndex);
        for (int i = 0; i < range; i++) {
            //按范围获取队列,保证连续性质
            result.add(mqAll.get((startIndex + i) % mqAll.size()));
        }

平均分配法,是按范围分配,一个范围分配给一个消费者。

总结

负载均衡策略在RocketMQ中比较重要,在很多中间件都需要使用,比如Dubbo,kafka等等。

相关推荐
喝醉的小喵2 分钟前
【mysql】并发 Insert 的死锁问题 第二弹
数据库·后端·mysql·死锁
kaixin_learn_qt_ing25 分钟前
Golang
开发语言·后端·golang
炒空心菜菜1 小时前
MapReduce 实现 WordCount
java·开发语言·ide·后端·spark·eclipse·mapreduce
独行soc3 小时前
2025年渗透测试面试题总结-阿里云[实习]阿里云安全-安全工程师(题目+回答)
linux·经验分享·安全·阿里云·面试·职场和发展·云计算
wowocpp4 小时前
spring boot Controller 和 RestController 的区别
java·spring boot·后端
后青春期的诗go4 小时前
基于Rust语言的Rocket框架和Sqlx库开发WebAPI项目记录(二)
开发语言·后端·rust·rocket框架
freellf4 小时前
go语言学习进阶
后端·学习·golang
全栈派森6 小时前
云存储最佳实践
后端·python·程序人生·flask
CircleMouse6 小时前
基于 RedisTemplate 的分页缓存设计
java·开发语言·后端·spring·缓存
獨枭7 小时前
使用 163 邮箱实现 Spring Boot 邮箱验证码登录
java·spring boot·后端