Consumer端实现负载均衡的核心类—RebalanceImpl

81 阅读3分钟

消息重试策略

RocketMQ会为每个消费者组都设置一个Topic名称为“%RETRY%+consumerGroup”的重试队列(这里需要注意的是,这个Topic的重试队列是针对消费组,而不是针对每个Topic设置的),用于暂时保存因为各种异常而导致Consumer端无法消费的消息。

考虑到异常恢复需要一些时间,RocketMQ会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ对于重试消息的处理是先保存至Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至“%RETRY%+consumerGroup”的重试队列中。

DefaultMQPushConsumerImpl

public synchronized void start() throws MQClientException {
    //初始化
    this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
    ...
    if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
        this.consumeOrderly = true;
        this.consumeMessageService =
            new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
    } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
        this.consumeOrderly = false;
        this.consumeMessageService =
            new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
    }
    
    this.consumeMessageService.start();
    
}
  • 初始化MQClientInstance。
  • 初始化consumeMessageService【有序消费、并发消费】。

MQClientInstance

该实例的启动是通过自定义的生产者、消费者调用#start方法触发。

public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) {
    ...
    this.clientId = clientId;
    this.mQAdminImpl = new MQAdminImpl(this);
    this.pullMessageService = new PullMessageService(this);
    this.rebalanceService = new RebalanceService(this);
    ...
}
  • 初始化PullMessageService。
  • 初始化RebalanceService。
public void start() throws MQClientException {

    synchronized (this) {
        switch (this.serviceState) {
            case CREATE_JUST:
                this.serviceState = ServiceState.START_FAILED;
                // If not specified,looking address from name server
                if (null == this.clientConfig.getNamesrvAddr()) {
                    this.mQClientAPIImpl.fetchNameServerAddr();
                }
                // Start request-response channel
                this.mQClientAPIImpl.start();
                // Start various schedule tasks
                this.startScheduledTask();
                // Start pull service
                this.pullMessageService.start();//启动拉取消息任务
                // Start rebalance service
                this.rebalanceService.start();// 启动消费端负载均衡任务
                // Start push service
                this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
                this.serviceState = ServiceState.RUNNING;
                break;
            case START_FAILED:
                throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
            default:
                break;
        }
    }
}

PullMessageService

PullMessageService 从服务端拉取到消息后,会根据消息对应的消费组,转给该组对应的 ProcessQueue,而 ProcessQueue 是 MessageQueue 在消费端的重现、快照。 PullMessageService 从消息服务器默认每次拉取 32 条消息,按消息的队列偏移量顺序存放在 ProcessQueue 中,PullMessageService 然后将消息提交到消费者消费线程池,消息成功消费后从 ProcessQueue 中移除。

PullMessageService是消息拉取服务线程【ServiceThread】。MQClientInstance在启动时(调用start方法)会调用PullMessageService#start方法来启动消费者消息拉取线程。

public void run() {
    while (!this.isStopped()) {//不断轮询拉取broker端的消息
        PullRequest pullRequest = this.pullRequestQueue.take();//主要是轮询获取当前消费端实例消费的所有队列信息
        this.pullMessage(pullRequest);
    }
}

队列PullRequestQueue中的元素是线程RebalanceService执行的任务赋值完成的。

private void pullMessage(final PullRequest pullRequest) {
    final MQConsumerInner consumer = 
        this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
    DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
        impl.pullMessage(pullRequest);
}

RebalanceService

RebalanceService【ServiceThread】,start方法会启动一个后台线程,确保每隔一段时间(默认20秒)会调用一次MQClientInstance的doRebalance方法。

public void run() {
    while (!this.isStopped()) {
        this.waitForRunning(waitInterval);
        this.mqClientFactory.doRebalance();//MQClientInstance.doRebalance
    }
}

开始处理消费端的负载均衡。

public void doRebalance() {
    for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
        MQConsumerInner impl = entry.getValue();//DefaultMQPushConsumerImpl
        impl.doRebalance();
    }
}

DefaultMQPushConsumerImpl

public void doRebalance() {
    if (!this.pause) {
        // RebalancePushImpl
        this.rebalanceImpl.doRebalance(this.isConsumeOrderly());
    }
}

RebalancePushImpl

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();
            this.rebalanceByTopic(topic, isOrder);//负载均衡分配算法
        }
    }
    this.truncateMessageQueueNotMyTopic();
}

消费端实例与队列分配算法:

  • 平均分配策略(默认)(AllocateMessageQueueAveragely)
  • 环形分配策略(AllocateMessageQueueAveragelyByCircle)
  • 手动配置分配策略(AllocateMessageQueueByConfig)
  • 机房分配策略(AllocateMessageQueueByMachineRoom)
  • 一致性哈希分配策略(AllocateMessageQueueConsistentHash)

Mq中平均分配策略的实现方式:

//consumerGroup:表示当前GroupId 
//currentCID:当前消费端实例ID,表达方式为 IP@PORT@consumer,
//mqAll:当前topic下所有的队列集合
//cidAll:订阅当前GroupId的所有消费端实例,即currentCID集合
public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,List<String> cidAll) {//所有集合都是经过排序处理的
    List<MessageQueue> result = new ArrayList<MessageQueue>();
    int index = cidAll.indexOf(currentCID);//获取当前currentCID在集合列表cidAll中下标
    int mod = mqAll.size() % cidAll.size();//判断所有消费端实例是否可以平均分配所有队列,mod为0表示平均分配,否则无法平均分配
    int averageSize =
        mqAll.size() <= cidAll.size() 
            ? 1 //表示消费端实例个数多余队列个数,消费端实例无法平均获取监听队列
            : (
                mod > 0 && index < mod 
                    ? mqAll.size() / cidAll.size() + 1 // topic队列总数为15,消费端实例个数为4,mod 为3
                    : mqAll.size() / cidAll.size() // 每个消费端平均监听的队列数
              );
    int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
    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;
}

目的是获取当前消费端实例被分配到的队列。

参考官方文章