我服了,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等等。

相关推荐
艾伦~耶格尔1 小时前
Spring Boot 三层架构开发模式入门
java·spring boot·后端·架构·三层架构
man20172 小时前
基于spring boot的篮球论坛系统
java·spring boot·后端
攸攸太上2 小时前
Spring Gateway学习
java·后端·学习·spring·微服务·gateway
罗曼蒂克在消亡3 小时前
graphql--快速了解graphql特点
后端·graphql
潘多编程3 小时前
Spring Boot与GraphQL:现代化API设计
spring boot·后端·graphql
大神薯条老师3 小时前
Python从入门到高手4.3节-掌握跳转控制语句
后端·爬虫·python·深度学习·机器学习·数据分析
2401_857622664 小时前
Spring Boot新闻推荐系统:性能优化策略
java·spring boot·后端
Neituijunsir4 小时前
2024.09.22 校招 实习 内推 面经
大数据·人工智能·算法·面试·自动驾驶·汽车·求职招聘
知否技术4 小时前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
AskHarries4 小时前
如何优雅的处理NPE问题?
java·spring boot·后端