【RocketMQ】源码详解:消费者启动、消费者负载均衡Rebalance

消费者启动

入口 : org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#start

消费者要监听并消费消息 , 需要先 1.构造消费者对象 2.添加订阅关系 3.注册消息监听器 4. 消费者启动

构造函数没什么好说的,new一个对象, 设置消费者组名\nameserver地址等

添加订阅关系就是订阅一个topic的哪些tag的消息, 通过subscribe方法进行,主要是将订阅关系保存到 rebalanceImpl中

消费者需要调用start方法进行启动, 才可以监听并消费消息.启动中会拷贝订阅关系,因为旧版本是通过set方法到consumer中进行订阅, 新版是直接订阅到rebalance服务对象中, 这一步就是将订阅关系复制过去. 还会启动MQClientInstance, 这个实例是针对一个项目的全部生产者消费者, 而不是单个的生产者或消费者, 其内部针对消费者会启动消息拉取服务、rebalance服务、更新偏移量定时任务等

public synchronized void start() throws MQClientException {
    switch (this.serviceState) {
        case CREATE_JUST:
            log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(),
                this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
            this.serviceState = ServiceState.START_FAILED;
            /**
             * 1 检查消费者的配置信息
             */
            this.checkConfig();
            /**
             * 2 拷贝订阅关系
             *
             * 将订阅defaultMQPushConsumer中的订阅关系复制到RebalanceImpl中
             * (旧版本是通过defaultMQPushConsumer的set方法设置订阅关系的,现在已经废弃.目前新版推荐通过subscribe方法添加订阅关系)
             * 对于集群模式的消费者,自动添加当前topic的重试主题到订阅列表,以实现消费重试
             */
            this.copySubscription();
            // 如果是集群消费模式,如果instanceName为默认值 "DEFAULT",那么改成 UtilAll.getPid() + "#" + System.nanoTime()
            if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
                this.defaultMQPushConsumer.changeInstanceNameToPID();
            }
            /**
             * 3 获取 MQClientInstance
             *
             * MQClientInstance是针对客户端(一个应用的 全部消费者/生产者)的类
             * 其内部提供了netty服务,针对消费者提供了 消息拉取、Rebalance服务等,还有多个针对客户端的定时任务,如更新offset任务。
             */
            this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);

            /**
             * 4 设置rebalance服务的相关属性
             */
            this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
            this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
            this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
            this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);

            /**
             * 5 创建消息拉取核心对象PullAPIWrapper,封装了消息拉取及结果解析逻辑的API
             */
            this.pullAPIWrapper = new PullAPIWrapper(
                mQClientFactory,
                this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
            // 为PullAPIWrapper注册过滤消息的钩子函数
            this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);

            /**
             * 6 根据消息模式设置不同的OffsetStore,用于实现消费者的消息消费偏移量offset的管理
             */
            if (this.defaultMQPushConsumer.getOffsetStore() != null) {
                this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
            } else {
                switch (this.defaultMQPushConsumer.getMessageModel()) {
                    // 广播消费模式 消息消费进度即offset存储在本地磁盘中。
                    case BROADCASTING:
                        this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                        break;
                    // 集群消费模式 消息消费进度即offset存储在远程broker中。+
                    case CLUSTERING:
                        this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                        break;
                    default:
                        break;
                }
                this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
            }
            /**
             * 7 加载消费偏移量,LocalFileOffsetStore会加载本地磁盘中的数据,RemoteBrokerOffsetStore则是一个空实现。
             */
            this.offsetStore.load();

            /**
             * 8 根据消息监听器的类型创建不同的 消息消费服务
             */
            if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
                // 顺序消费,创建ConsumeMessageOrderlyService
                this.consumeOrderly = true;
                this.consumeMessageService =
                    new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
            } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
                // 并发消费, 创建ConsumeMessageConcurrentlyService
                this.consumeOrderly = false;
                this.consumeMessageService =
                    new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
            }
            //启动消息消费服务
            this.consumeMessageService.start();
            /**
             * 9 注册消费者组和消费者到MQClientInstance中的consumerTable中
             */
            boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
            if (!registerOK) {
                // 如果没注册成功,那么可能是因为同一个程序中存在 同名消费者组 的 不同消费者
                this.serviceState = ServiceState.CREATE_JUST;
                this.consumeMessageService.shutdown(defaultMQPushConsumer.getAwaitTerminationMillisWhenShutdown());
                throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
                    + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                    null);
            }
            /**
             * 10 启动 CreateMQClientInstance 客户端通信实例
             * netty服务、各种定时任务、拉取消息服务、rebalanceService服务
             */
            mQClientFactory.start();
            log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
            this.serviceState = ServiceState.RUNNING;
            break;
        /**
         * 服务状态是其他的,那么抛出异常,即start方法仅能调用一次
         */
        case RUNNING:
        case START_FAILED:
        case SHUTDOWN_ALREADY:
            throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
                + this.serviceState
                + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                null);
        default:
            break;
    }

    /*
     * 11 后续处理
     */
    /*
     * 向NameServer拉取并更新当前消费者订阅的topic路由信息
     */
    this.updateTopicSubscribeInfoWhenSubscriptionChanged();
    /*
     * 随机选择一个Broker,发送检查客户端tag配置的请求,主要是检测Broker是否支持SQL92类型的tag过滤以及SQL92的tag语法是否正确
     */
    this.mQClientFactory.checkClientInBroker();
    /*
     * 发送心跳信息给所有broker
     */
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
    /*
     * 唤醒负载均衡服务rebalanceService,进行重平衡
     */
    this.mQClientFactory.rebalanceImmediately();
}

拷贝订阅关系: org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#copySubscription

private void copySubscription() throws MQClientException {
    try {
        Map<String, String> sub = this.defaultMQPushConsumer.getSubscription();
        if (sub != null) {
            for (final Map.Entry<String, String> entry : sub.entrySet()) {
                final String topic = entry.getKey();
                final String subString = entry.getValue();
                SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subString);
                // 将defaultMQPushConsumer中的订阅关系拷贝到rebalanceImpl中
                this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData);
            }
        }

        // 如果messageListenerInner为null,那么将defaultMQPushConsumer的messageListener赋给它
        // MessageListener就是实际对消息做处理、我们自己开发的消费者,消息会传入MessageListener进行消费
        if (null == this.messageListenerInner) {
            this.messageListenerInner = this.defaultMQPushConsumer.getMessageListener();
        }

        // 为集群消费模式设置消息重试的队列
        switch (this.defaultMQPushConsumer.getMessageModel()) {
            // 广播消费, 消费失败消息会丢弃
            case BROADCASTING:
                break;
            // 集群消费
            case CLUSTERING:
                // retry主题 为 %RETRY% + 消费组名
                final String retryTopic = MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup());
                // 当前消费者自动订阅该消费者组对应的重试topic,用于实现消费重试
                SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(retryTopic, SubscriptionData.SUB_ALL);
                this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData);
                break;
            default:
                break;
        }
    } catch (Exception e) {
        throw new MQClientException("subscription exception", e);
    }
}

Rebalance

入口: org.apache.rocketmq.client.impl.consumer.RebalanceService

在rocketmq中, 一个queue只能被一个消费者消费, 但是 一个消费者可以消费多个queue . 而且在程序运行过程中,可能会启动新的消费者以增加消费能力, 这就需要给新的消费者分配queue, 或者有消费者关闭, 其负责的queue就要交给其他消费者消费, 这就涉及到多个queue与消费者之间的负载均衡问题. 在rocketmq中由RebalanceService提供此服务. 其提供了多种负载均衡策略: 平均分配、环形平均策略、一致性哈希策略等

consumer启动重平衡

consumer调用start方法启动的时候,会在 mQClientFactory.start(); 时启动rebalance,并在启动的最后唤醒服务,执行一次rebalance

启动重平衡服务:org.apache.rocketmq.client.impl.factory.MQClientInstance#start

// 重平衡服务,针对消费者
this.rebalanceService.start();

唤醒重平衡: org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#start

/*
 * 唤醒负载均衡服务rebalanceService,进行重平衡
 */
this.mQClientFactory.rebalanceImmediately();

RebalanceService自动重平衡

入口:org.apache.rocketmq.client.impl.consumer.RebalanceService#run

RebalanceService本身是一个服务线程,其run方法中不断每20s循环,以执行一次Rebalance

@Override
public void run() {
    log.info(this.getServiceName() + " service started");

    while (!this.isStopped()) {
        // 等待20s
        this.waitForRunning(waitInterval);
        // 执行rebalance
        this.mqClientFactory.doRebalance();
    }

    log.info(this.getServiceName() + " service end");
}

broker发起重平衡

入口:org.apache.rocketmq.broker.processor.ClientManageProcessor#heartBeat

在consumer启动的时候会发送一个心跳包到broker,同时也会启动一个每30s发送一次心跳的定时任务

在broker处理心跳包的时候,如果是consumer的心跳,则会判断是否是新的channel连接(新的consumer)或者是否consumer的订阅信息发生了变化

若该心跳包的consumer传过来的是新的channel连接,则说明是新的consumer,则会将其缓存在broker中

若该心跳包的consumer传过来的订阅信息有变化,则说明订阅了新的topic或取消订阅了某个topic,broker将会更新其订阅信息

而更新channel连接、订阅信息,是以消费者组来调用更新的,这也是为何同一个消费者组只能订阅相同的topic的原因,不然同一个消费者组的两个消费者发送心跳包会导致订阅信息相互覆盖。

若是新的channel或者订阅信息有变化,并且允许通知,那么通知该consumergroup中的所有consumer进行重平衡

// 循环consumer的心跳
for (ConsumerData data : heartbeatData.getConsumerDataSet()) {
    // 查询broker端缓存的当前consumer的订阅组配置
    SubscriptionGroupConfig subscriptionGroupConfig =
        this.brokerController.getSubscriptionGroupManager().findSubscriptionGroupConfig(
            data.getGroupName());
    boolean isNotifyConsumerIdsChangedEnable = true;
    if (null != subscriptionGroupConfig) {
        // 当consumer发生改变的时候是否支持通知同组的所有consumer进行重平衡,默认true,即支持
        isNotifyConsumerIdsChangedEnable = subscriptionGroupConfig.isNotifyConsumerIdsChangedEnable();
        int topicSysFlag = 0;
        if (data.isUnitMode()) {
            topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
        }
        // 创建重试topic
        String newTopic = MixAll.getRetryTopic(data.getGroupName());
        this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
            newTopic,
            subscriptionGroupConfig.getRetryQueueNums(),
            PermName.PERM_WRITE | PermName.PERM_READ, topicSysFlag);
    }
    /**
     * 注册消费者,返回consumer信息是否已发生改变
     * 如果发生了改变,Broker会发送NOTIFY_CONSUMER_IDS_CHANGED请求给同组的所有consumer客户端,要求进行重平衡操作
     */
    boolean changed = this.brokerController.getConsumerManager().registerConsumer(
        data.getGroupName(),
        clientChannelInfo,
        data.getConsumeType(),
        data.getMessageModel(),
        data.getConsumeFromWhere(),
        data.getSubscriptionDataSet(),
        isNotifyConsumerIdsChangedEnable
    );

    if (changed) {
        log.info("registerConsumer info changed {} {}",
            data.toString(),
            RemotingHelper.parseChannelRemoteAddr(ctx.channel())
        );
    }
}

注册消费者,判断心跳包中传过来的信息是否有改变:org.apache.rocketmq.broker.client.ConsumerManager#registerConsumer

public boolean registerConsumer(final String group, final ClientChannelInfo clientChannelInfo,
    ConsumeType consumeType, MessageModel messageModel, ConsumeFromWhere consumeFromWhere,
    final Set<SubscriptionData> subList, boolean isNotifyConsumerIdsChangedEnable) {

    ConsumerGroupInfo consumerGroupInfo = this.consumerTable.get(group);
    if (null == consumerGroupInfo) {
        ConsumerGroupInfo tmp = new ConsumerGroupInfo(group, consumeType, messageModel, consumeFromWhere);
        ConsumerGroupInfo prev = this.consumerTable.putIfAbsent(group, tmp);
        consumerGroupInfo = prev != null ? prev : tmp;
    }
    // 更新channel连接,返回值表示是否是新连接,如果是新连接则需要重平衡
    boolean r1 =
        consumerGroupInfo.updateChannel(clientChannelInfo, consumeType, messageModel,
            consumeFromWhere);
    // 更新订阅信息,返回值表示是否订阅信息有变化,如果有变化则需要重平衡
    boolean r2 = consumerGroupInfo.updateSubscription(subList);

    // 若channel或订阅信息有变化,并且允许通知,那么通知该consumergroup中的所有consumer进行重平衡
    if (r1 || r2) {
        if (isNotifyConsumerIdsChangedEnable) {
            this.consumerIdsChangeListener.handle(ConsumerGroupEvent.CHANGE, group, consumerGroupInfo.getAllChannel());
        }
    }

    this.consumerIdsChangeListener.handle(ConsumerGroupEvent.REGISTER, group, subList);

    return r1 || r2;
}

更新channel连接:org.apache.rocketmq.broker.client.ConsumerGroupInfo#updateChannel

public boolean updateChannel(final ClientChannelInfo infoNew, ConsumeType consumeType,
    MessageModel messageModel, ConsumeFromWhere consumeFromWhere) {
    boolean updated = false;
    this.consumeType = consumeType;
    this.messageModel = messageModel;
    this.consumeFromWhere = consumeFromWhere;

    // 根据当前连接获取 channelInfoTable 缓存中的连接信息
    ClientChannelInfo infoOld = this.channelInfoTable.get(infoNew.getChannel());
    // 为空, 表示是新连接
    if (null == infoOld) {
        // 存入缓存
        ClientChannelInfo prev = this.channelInfoTable.put(infoNew.getChannel(), infoNew);
        if (null == prev) {
            log.info("new consumer connected, group: {} {} {} channel: {}", this.groupName, consumeType,
                messageModel, infoNew.toString());
            // 表示需要重平衡
            updated = true;
        }

        infoOld = infoNew;
    } else {
        if (!infoOld.getClientId().equals(infoNew.getClientId())) {
            log.error("[BUG] consumer channel exist in broker, but clientId not equal. GROUP: {} OLD: {} NEW: {} ",
                this.groupName,
                infoOld.toString(),
                infoNew.toString());
            this.channelInfoTable.put(infoNew.getChannel(), infoNew);
        }
    }

    this.lastUpdateTimestamp = System.currentTimeMillis();
    infoOld.setLastUpdateTimestamp(this.lastUpdateTimestamp);

    return updated;
}

更新订阅信息:org.apache.rocketmq.broker.client.ConsumerGroupInfo#updateSubscription

/**
 * 一个消费者组中的所有消费者必须订阅相同的topic,否则可能会相互覆盖
 * 假设一个消费者组groupX里面有两个消费者A、B
 * 若A消费者先订阅topicA,A消费者发送心跳,那么subscriptionTable中消费者组groupX里面仅有topicA。
 * 如果之后B消费者订阅topicB,B消费者也发送心跳,subscriptionTable中消费者组groupX里面的topicA将会被移除,topicB的订阅信息会被存入。
 * @param subList
 * @return
 */
public boolean updateSubscription(final Set<SubscriptionData> subList) {
    boolean updated = false;

    for (SubscriptionData sub : subList) {
        SubscriptionData old = this.subscriptionTable.get(sub.getTopic());
        // 根据topic查询订阅信息为空
        if (old == null) {
            // 说明该topic是新订阅的,则将其保存
            SubscriptionData prev = this.subscriptionTable.putIfAbsent(sub.getTopic(), sub);
            if (null == prev) {
                updated = true;
                log.info("subscription changed, add new topic, group: {} {}",
                    this.groupName,
                    sub.toString());
            }
        } else if (sub.getSubVersion() > old.getSubVersion()) {
            // 更新订阅信息
            if (this.consumeType == ConsumeType.CONSUME_PASSIVELY) {
                log.info("subscription changed, group: {} OLD: {} NEW: {}",
                    this.groupName,
                    old.toString(),
                    sub.toString()
                );
            }

            this.subscriptionTable.put(sub.getTopic(), sub);
        }
    }

    // 获取之前的订阅信息集合进行遍历
    Iterator<Entry<String, SubscriptionData>> it = this.subscriptionTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<String, SubscriptionData> next = it.next();
        // 获取此次循环的之前topic
        String oldTopic = next.getKey();

        boolean exist = false;
        for (SubscriptionData sub : subList) {
            // 如果这次请求来的订阅信息中存在之前的topic
            if (sub.getTopic().equals(oldTopic)) {
                exist = true;
                break;
            }
        }
        // 当前的subList不存在该topic的订阅信息,说明consumer移除了对于该topic的订阅
        if (!exist) {
            log.warn("subscription changed, group: {} remove topic {} {}",
                this.groupName,
                oldTopic,
                next.getValue().toString()
            );
            // 移除数据
            it.remove();
            // 需要进行重平衡
            updated = true;
        }
    }

    this.lastUpdateTimestamp = System.currentTimeMillis();

    return updated;
}

客户端处理broker发起的重平衡:org.apache.rocketmq.client.impl.ClientRemotingProcessor#processRequest

@Override
public RemotingCommand processRequest(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    switch (request.getCode()) {
        // 处理NOTIFY_CONSUMER_IDS_CHANGED(通知消费者变更,即重平衡)请求
        case RequestCode.NOTIFY_CONSUMER_IDS_CHANGED:
            return this.notifyConsumerIdsChanged(ctx, request);
		// 。。。。省略
    }
    return null;
}
/**
 * broker通知进行重平衡的处理入口
 *
 * @param ctx
 * @param request
 * @return
 * @throws RemotingCommandException
 */
public RemotingCommand notifyConsumerIdsChanged(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    try {
        final NotifyConsumerIdsChangedRequestHeader requestHeader =
            (NotifyConsumerIdsChangedRequestHeader) request.decodeCommandCustomHeader(NotifyConsumerIdsChangedRequestHeader.class);
        log.info("receive broker's notification[{}], the consumer group: {} changed, rebalance immediately",
            RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
            requestHeader.getConsumerGroup());
        // 进行重平衡
        this.mqClientFactory.rebalanceImmediately();
    } catch (Exception e) {
        log.error("notifyConsumerIdsChanged exception", RemotingHelper.exceptionSimpleDesc(e));
    }
    return null;
}

rebalanceImmediately就是唤醒重平衡服务线程的方法

重平衡过程分析

入口: org.apache.rocketmq.client.impl.factory.MQClientInstance#doRebalance

遍历当前客户端的每一个消费者,调用他们的rebalance方法,因为每个消费者都需要执行一次重平衡来获取本次分配到的queue

对于一个消费者,遍历其订阅的topic,针对每个topic进行rebalance

对于一个topic,会获取其下全部的queue信息以及全部的消费者的clientId 并排序, 用于rebalance计算自己分配到的queue, 并保证所有的消费者分配的结果相同。

然后根据配置获取对应的负载均衡策略类,对当前clientId的消费者分配queue,并得到分配的queueList

然后更新该消费者 对应消费的queue。(本质上就是维护一个消费者本地的队列map,删除本次未分配的queue,添加新分配的queue)

更新完queue之后,再对新分配的queue发起第一次消息拉取请求,之后则由消息拉取服务自动循环拉取

public void doRebalance() {
    // 遍历每个消费者
    for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
        MQConsumerInner impl = entry.getValue();
        if (impl != null) {
            try {
                // 每个消费者都要执行重平衡,以更新自己要消费的queue
                impl.doRebalance();
            } catch (Throwable e) {
                log.error("doRebalance exception", e);
            }
        }
    }
}

每个消费者执行其重平衡方法:org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#doRebalance

/**
 * 执行重平衡
 * @param isOrder
 */
public void doRebalance(final boolean isOrder) {
    // 获取当前消费者的订阅信息集合
    Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
    if (subTable != null) {
        for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
            // 获取topic
            final String topic = entry.getKey();
            try {
                /*
                 * 对该topic进行重平衡
                 */
                this.rebalanceByTopic(topic, isOrder);
            } catch (Throwable e) {
                if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
                    log.warn("rebalanceByTopic Exception", e);
                }
            }
        }
    }
    /*
     * 丢弃不属于当前消费者订阅的topic的队列快照ProcessQueue
     */
    this.truncateMessageQueueNotMyTopic();
}

根据该消费者的每个订阅topic执行重平衡:org.apache.rocketmq.client.impl.consumer.RebalanceImpl#rebalanceByTopic

private void rebalanceByTopic(final String topic, final boolean isOrder) {
    switch (messageModel) {
        // 广播模式没有负载均衡的概念,每个consumer都会消费所有队列中的全部消息
        case BROADCASTING: {
            // 。。。
        }
        // 集群模式消费
        case CLUSTERING: {
            // 获取该topic下的queue信息
            Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
            // 向broker获取该topic下所有客户端id, 一个clientId代表一个消费者
            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 (null == cidAll) {
                log.warn("doRebalance, {} {}, get consumer id list failed", consumerGroup, topic);
            }

            if (mqSet != null && cidAll != null) {
                List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
                mqAll.addAll(mqSet);
                // 对所有的queue和消费者Id进行排序,以保证不同的消费者在进行负载均衡时,其mqAll和cidAll中的元素顺序是一致的
                Collections.sort(mqAll);
                Collections.sort(cidAll);
                // 获取负载均衡策略类
                AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;

                List<MessageQueue> allocateResult = null;
                try {
                    /*
                     * 为当前clientId也就是当前消费者,分配消息队列
                     * 这一步就是执行负载均衡或者说重平衡的算法
                     *
                     * AllocateMessageQueueAveragely:平均分配策略,这是默认策略。尽量将消息队列平均分配给所有消费者,多余的队列分配至排在前面的消费者。分配的时候,前一个消费者分配完了,才会给下一个消费者分配。
                     * AllocateMessageQueueAveragelyByCircle:环形平均分配策略。尽量将消息队列平均分配给所有消费者,多余的队列分配至排在前面的消费者。与平均分配策略差不多,区别就是分配的时候,按照消费者的顺序进行一轮一轮的分配,直到分配完所有消息队列。
                     * AllocateMessageQueueByConfig:根据用户配置的消息队列分配。将会直接返回用户配置的消息队列集合。
                     * AllocateMessageQueueByMachineRoom:机房平均分配策略。消费者只消费绑定的机房中的broker,并对绑定机房中的MessageQueue进行负载均衡。
                     * AllocateMachineRoomNearby:机房就近分配策略。消费者对绑定机房中的MessageQueue进行负载均衡。除此之外,对于某些拥有消息队列但却没有消费者的机房,其消息队列会被所欲消费者分配,具体的分配策略是,另外传入的一个AllocateMessageQueueStrategy的实现。
                     * AllocateMessageQueueConsistentHash:一致性哈希分配策略。基于一致性哈希算法分配。
                     */
                    // 执行重平衡,返回当前消费者被分配的queueList
                    allocateResult = strategy.allocate(
                        this.consumerGroup,
                        this.mQClientFactory.getClientId(),
                        mqAll,
                        cidAll);
                } catch (Throwable e) {
                    log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),
                        e);
                    return;
                }

                Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
                if (allocateResult != null) {
                    allocateResultSet.addAll(allocateResult);
                }
                // 更新当前消费者分配到的queue (processQueueTable信息)
                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);
                    /*
                     * 设置新的本地订阅关系版本,重设流控参数,立即给所有broker发送心跳,让Broker更新当前订阅关系
                     */
                    this.messageQueueChanged(topic, mqSet, allocateResultSet);
                }
            }
            break;
        }
        default:
            break;
    }
}

负载均衡获取当前消费者分配的queue,以平均分配为例:org.apache.rocketmq.client.consumer.rebalance.AllocateMessageQueueAveragely

@Override
public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,
    List<String> cidAll) {
    // 。。。省略一些校验代码
    
    int index = cidAll.indexOf(currentCID);
    int mod = mqAll.size() % cidAll.size();
    /**
     * queue的数量小于等于consumer的数量  则为 1 即每个consumer分配一个queue
     * 否则
     * 若:mq的数量 未整除 consumer的数量 且 当前consumer的下标小于余数 则 要多分配一个
     * 否则 则是平均不加上余数的数量
     *
     * 举例: 当前consumer 为 第 0 个 , 且余数为 1 则说明需要帮忙多分配一个, 若除去余数每个consumer分配 1个,则当前consumer分配2个
     * 举例: 5个queue,3个consumer,则前两个分配 5/3+1 = 2个, 最后一个分配 5/3 = 1个
     */
    // 当前消费者要分配的queue数量
    int averageSize =
        mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()
            + 1 : mqAll.size() / cidAll.size());
    /**
     * 若不是需要多分配一个余数中的, 则下标需要加上余数
     */
    // 当前消费者分配的queue的起始下标
    int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
    // 取 要分配的数量 与 剩余mq数量 中的较小值
    int range = Math.min(averageSize, mqAll.size() - startIndex);
    for (int i = 0; i < range; i++) {
        // 从下标开始 循环添加(取模以防止下标越界)
        result.add(mqAll.get((startIndex + i) % mqAll.size()));
    }
    return result;
}

将分配的queue进行更新:org.apache.rocketmq.client.impl.consumer.RebalanceImpl#updateProcessQueueTableInRebalance

/**
 * 更新当前消费者重平衡后 分配的queue
 *
 * @param topic
 * @param mqSet
 * @param isOrder
 * @return
 */
private boolean updateProcessQueueTableInRebalance(final String topic, final Set<MessageQueue> mqSet,
    final boolean isOrder) {
    boolean changed = false;
    /*
     * 遍历当前消费者的所有处理队列,当消费者启动并且第一次执行该方法时,processQueueTable是一个空集合,
     * ProcessQueue是messageQueue在consumer端的快照体现
     */
    Iterator<Entry<MessageQueue, ProcessQueue>> it = this.processQueueTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<MessageQueue, ProcessQueue> next = it.next();
        MessageQueue mq = next.getKey();
        ProcessQueue pq = next.getValue();

        // 如果topic相等
        if (mq.getTopic().equals(topic)) {
            // 如果新分配的消息队列集合不包含当前遍历到的消息队列,说明这个队列被移除了
            if (!mqSet.contains(mq)) {
                /**
                 * 设置对应的处理队列dropped = true
                 * dropped会在消息消费时被用到,为true时:1.不会拉取消息 2.消息消费完成则不会处理消费结果,可能会出现重复消费
                 */
                pq.setDropped(true);
                /*
                 * 删除不必要的消息队列
                 */
                if (this.removeUnnecessaryMessageQueue(mq, pq)) {
                    it.remove();
                    changed = true;
                    log.info("doRebalance, {}, remove unnecessary mq, {}", consumerGroup, mq);
                }
            // 当前队列,拉取消息超过120s,超时了
            } else if (pq.isPullExpired()) {
                switch (this.consumeType()) {
                    case CONSUME_ACTIVELY:
                        break;
                    case CONSUME_PASSIVELY:
                        pq.setDropped(true);
                        if (this.removeUnnecessaryMessageQueue(mq, pq)) {
                            it.remove();
                            changed = true;
                            log.error("[BUG]doRebalance, {}, remove unnecessary mq, {}, because pull is pause, so try to fixed it",
                                consumerGroup, mq);
                        }
                        break;
                    default:
                        break;
                }
            }
        }
    }

    List<PullRequest> pullRequestList = new ArrayList<PullRequest>();
    for (MessageQueue mq : mqSet) {
        // 如果当前消费者的处理队列集合中不包含该消息队列,那么表示这个消息队列是新分配的
        if (!this.processQueueTable.containsKey(mq)) {
            if (isOrder && !this.lock(mq)) {
                log.warn("doRebalance, {}, add a new mq failed, {}, because lock failed", consumerGroup, mq);
                continue;
            }
            // 从offsetTable中移除该消息队列的消费点位offset记录信息
            this.removeDirtyOffset(mq);
            /*
             * 为该消息队列创建一个处理队列
             */
            ProcessQueue pq = new ProcessQueue();

            long nextOffset = -1L;
            try {
                /*
                 * 获取该MessageQueue的下一个消息的消费偏移量offset
                 */
                nextOffset = this.computePullFromWhereWithException(mq);

            } catch (Exception e) {
                log.info("doRebalance, {}, compute offset failed, {}", consumerGroup, mq);
                continue;
            }
            // >=0说明获取成功
            if (nextOffset >= 0) {
                // 将该mq放入集合
                ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);
                if (pre != null) {
                    log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
                } else {
                    log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);
                    PullRequest pullRequest = new PullRequest();
                    pullRequest.setConsumerGroup(consumerGroup);
                    pullRequest.setNextOffset(nextOffset);
                    pullRequest.setMessageQueue(mq);
                    pullRequest.setProcessQueue(pq);
                    pullRequestList.add(pullRequest);
                    changed = true;
                }
            } else {
                log.warn("doRebalance, {}, add new mq failed, {}", consumerGroup, mq);
            }
        }
    }
    /*
     * 分发本次创建的PullRequest请求。
     * pull模式需要手动拉取消息,这些请求会作废,因此该方法是一个空实现
     * push模式下自动拉取消息,而这里的PullRequest就是对应的消息队列的第一个拉取请求,因此这些请求会被PullMessageService依次处理,后续实现自动拉取消息
     */
    this.dispatchPullRequest(pullRequestList);

    return changed;
}

你可能感兴趣的:(Rocket,MQ,java-rocketmq,rocketmq,java)