我理解的RocketMQ—消费者负载均衡的实现

本文详细介绍了RocketMQ消费者负载均衡的实现,包括负载均衡的整体概述、一次负载均衡的流程,以及消费者组如何使用负载均衡组件。重点讨论了RebalanceService的作用、执行流程和负载均衡函数的调用分析。

1 负载均衡实现组件分析

1.1 负载均衡整体概述

  • 概述
    所谓的负载均衡指的就是,在集群消费模式下,一个消费组里面有多个消费者订阅了一个主题,此主题有多个消息队列(MessageQueue),负载均衡组件就将这些消息队列平均分给消费组里面的消费者。

  • 使用注意点
    同一个消费组内的消费者要保持一样主题订阅,否则会导致某些消费队列没有消费者进行消费。

  • 组件分析(核心类和策略类)
    负载均衡只对集群消费模式的消费者有效,对广播模式的消费者无效的。RocketMQ的负载均衡是在消费者客户端完成的。
    首先抛开其他复杂的关系调用,逻辑,单纯的从负载均衡实现组件上来分析。负载均衡的组件中是RebalanceImpl这个抽象类,它的实现者有RebalancePushImplRebalanceLitePullImplRebalancePullImpl分别供几个消费者客户端使用。下图是类结构图。

在这里插入图片描述

抽象类中有一个重要的方法doRebalance(),这个方法是对外使用的。即使用的方式就是创建一个负载均衡实现类的实例,然后调用它的doRebalance()方法即可进行。

1.2 一次负载均衡的流程分析

干的核心事情:

  • 将主题的消息队列应用分配策略分配到消费组里面的消费者

  • 丢弃删减的消息队列对应的ProcessQueue,创建新增的消息队列的PullRequest和ProcessQueue。这些动作与消费者拉取消息消费相关。

主要流程说明:

  • 1 从ConcurrentMap<String/*主题*/, Set<MessageQueue> /*主题的消息队列*/> topicSubscribeInfoTable这个map中获取主题对应的MessageQueue集合,这个map会被一个定时任务(定时的从NameServer获取已订阅主题的路由信息)进行更新;主题的消息队列集合

  • 2 获取该消费者所属消费者组下的所有消费者集合;消费者集合

  • 3 将消息队列集合和消费者集合按照某个分配策略进行分配;

  • 4 分配策略完成分配,当前消费者获得新的消费队列集合或者删减了某些消息队列,接下来将新的消费队列和旧的进行对比

  • 5 如果消费者分配到了一个之前没有的mq,那么将会创建一个PullRequest给PullMessageService实现对消息的拉取,如果之前有但是现在没有的那么将会调用里面的ProcessQueue的droped方法来销毁,即不再拉取消息。其中创建新的PullRequest将会计算一次消息拉取偏移量this.computePullFromWhere(mq);

public void doRebalance(final boolean isOrder) {
    // 获取订阅数据
    Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
    // 遍历订阅数据
        for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
            // 获取主题
            final String topic = entry.getKey();
            // 根据主题进行负载均衡
            this.rebalanceByTopic(topic, isOrder);

        }
    this.truncateMessageQueueNotMyTopic();
}
private void rebalanceByTopic(final String topic, final boolean isOrder) {
    switch (messageModel) {
        case BROADCASTING: {
            ...
        }
        case CLUSTERING: {
            // 同样是先获取主题对应的消息队列列表
            // 这个map是会被一个从NameServer拉取路由信息来更新的
            Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
            // 找到主题对应的该消费组内所有的消费者ID
            List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
            if (mqSet != null && cidAll != null) {
                List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
                mqAll.addAll(mqSet);
                AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
                List<MessageQueue> allocateResult = null;
                        // 调用具体的负载均衡策略进行负载
                    // 负载的含义:将该主题的mq在该消费组的所有的消费者进行分配
                    allocateResult = strategy.allocate(
                            this.consumerGroup,
                            this.mQClientFactory.getClientId(),
                            mqAll,
                            cidAll);

            Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
            if (allocateResult != null) {
                allocateResultSet.addAll(allocateResult);
            }
            // 看当前主题当前消费者的分配到的mq是否变化了
            // 下面这个函数很重要,如果消费者分配到了一个之前没有的mq,那么将会创建一个PullRequest给PullMessageService实现对消息的拉取,如果之前有但是现在没有的那么将会调用里面的ProcessQueue的droped方法来销毁,即不再拉取消息。
            boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
            if (changed) {
                log.info(
                        "rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}",
                        strategy.getName(), consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(),
                        allocateResultSet.size(), allocateResultSet);
                this.messageQueueChanged(topic, mqSet, allocateResultSet);
            }
        }
        break;
        
}
}

2 消费者组使用负载均衡组件

关系说明:在MQClientInstance“客户端实例工厂”里面维护了一个负载均衡后台服务线程RebalanceService。它里面还维护了一个消费者组–>消费者内部客户端实例这样的一个map。每个消费者内部客户端里面维护了一个负载均衡实现类。如下图所示

在这里插入图片描述

调用流程:rebalanceService服务线程的run()方法调用MQClientInstancedoRebalance(),在此方法或遍历消费者组–>消费者内部客户端这个map,并调用每个消费者内部客户端的doRebalance(),此方法会调用负载均衡组件的doRebalance(...)方法。

在这里插入图片描述

负载均衡的后台服务线程在MQ的客户端MQClientInstance启动的,它有一个成员变量

public class MQClientInstance {
    ...
    private final RebalanceService rebalanceService;
    ...
}

赋初值是在它的构造函数中

public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) {
    this.rebalanceService = new RebalanceService(this);
}

负载均衡的启动是在它的start()方法中,调用了RebalanceServicestart()方法

    public void start() throws MQClientException {
                    this.rebalanceService.start();
    }

定时的负载均衡就在RebalanceService中实现了。
下面从几个问题出发来分析其中的机制

2.1 RebalanceService是什么?

它的类结构图如下,可以看到它继承自ServiceThread抽象类,属于RocketMQ中典型的创建后台服务线程运行方式,可参考RocketMQ中服务线程的分析。只要启动RebalanceService,它就会一直在后台运行,不会停,相当于一个定时任务。

在这里插入图片描述

2.2 RebalanceService的运行时,执行的代码体是什么?

下面是RebalanceService中run()方法体

    public void run() {
        log.info(this.getServiceName() + " service started");
        // 典型的线程设计方式,用一个volatile变量来控制线程的运行
        while (!this.isStopped()) {
            // 在这儿进行了等待,等待一定的时间,相当于定时
            this.waitForRunning(waitInterval);
            // 被唤醒了执行真正的负载均衡【重要,下面这个调用即是执行负载均衡】
            this.mqClientFactory.doRebalance();
        }
        log.info(this.getServiceName() + " service end");
    }

2.3 负载均衡函数调用分析

首先调用的是MQClientInstance的doRebalance()方法,在该方法中,首先获取的是消费者组内部客户端map,然后遍历这个map中的元素,并调用内部客户端(MQConsumerInner)doRebalance()进行负载均衡。在一个JVM中只存在一个MQClientInstance,而一个消费者组又只存在一个消费者内部客户端,所以进行负载均衡就是对每个消费者组内部客户端执行负载均衡,。

MQClientInstance#
    public void doRebalance() {
        // 遍历消费的存储表 <group,consumerImpl>
        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);
                }
            }
        }
    }
DefaultMQPushConsumerImpl#
public void doRebalance() {
    if (!this.pause) {
        // 调用里面的负载均衡实现类做负载均衡
        this.rebalanceImpl.doRebalance(this.isConsumeOrderly());
    }
}

2.4 更新主题消息队列

在这里插入图片描述

参考资料
在rocketmq中,部署多台机器上的消费者负载均衡是如何解决的?

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值