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

相关推荐
苏三的开发日记17 分钟前
windows系统搭建kafka环境
后端
爬山算法27 分钟前
Netty(19)Netty的性能优化手段有哪些?
java·后端
Tony Bai28 分钟前
Cloudflare 2025 年度报告发布——Go 语言再次“屠榜”API 领域,AI 流量激增!
开发语言·人工智能·后端·golang
想用offer打牌41 分钟前
虚拟内存与寻址方式解析(面试版)
java·后端·面试·系统架构
無量1 小时前
AQS抽象队列同步器原理与应用
后端
努力学算法的蒟蒻1 小时前
day38(12.19)——leetcode面试经典150
算法·leetcode·面试
9号达人1 小时前
支付成功订单却没了?MyBatis连接池的坑我踩了
java·后端·面试
用户497357337982 小时前
【轻松掌握通信协议】C#的通信过程与协议实操 | 2024全新
后端
草莓熊Lotso2 小时前
C++11 核心精髓:类新功能、lambda与包装器实战
开发语言·c++·人工智能·经验分享·后端·nginx·asp.net
C雨后彩虹2 小时前
斗地主之顺子
java·数据结构·算法·华为·面试