RocketMQ源码分析8:Consumer启动流程

773 阅读8分钟

本文已参与[新人创作礼]活动,一路开启掘金创作之路。

基于rocketmq-4.9.0 版本分析rocketmq

消费者代码举例:

public class Consumer {

   public static void main(String[] args) throws InterruptedException, MQClientException {

       // 实例化消费者
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("c1-group");

       // 设置NameServer的地址
        consumer.setNamesrvAddr("127.0.0.1:9876");

       //TODO:订阅一个或者多个Topic,以及Tag来过滤需要消费的消息
        consumer.subscribe("test_topic", "*");
       // TODO: 注册消息监听器,用来消费消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                try {

                    System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);

                    boolean consume = new Random().nextBoolean();
                    if(!consume) {
                        //TODO:消费失败,等待重试
                        return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }

                //TODO: 消费成功
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        //TODO: 启动消费者实例
        consumer.start();

        System.out.printf("Consumer Started.%n");
   }
}

2.客户端start()方法都干了什么?

首先,暴露给开发者的 DefaultMQPushConsumer是一个外观类,真正工作的是其内部的DefaultMQPushConsumerImpl,所以我们看下DefaultMQPushConsumerImpl#start()的逻辑

内容非常的多,我会在代码上添加一些注释

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;
            //TODO:检查配置,比如消费者组是否为空,消费模式是否为空,订阅信息是否为空等等
            this.checkConfig();

            //TODO: 拷贝订阅关系,大概就是将订阅关系设置到重平衡服务类中
            this.copySubscription();

            if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
                this.defaultMQPushConsumer.changeInstanceNameToPID();
            }

            //TODO:创建客户端实例对象
            this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);

            this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
            this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
            this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
            this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);

            //TODO: 创建拉取对象的核心类,后面去broker拉取消息时会看到
            this.pullAPIWrapper = new PullAPIWrapper(
                mQClientFactory,
                this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
            this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);

            if (this.defaultMQPushConsumer.getOffsetStore() != null) {
                this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
            } else {
                switch (this.defaultMQPushConsumer.getMessageModel()) {
                    case BROADCASTING:
                        //TODO: 广播模式创建LocalFileOffsetStore,保存offset到本地文件中
                        this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                        break;
                    case CLUSTERING:
                        //TODO:集群模式创建 RemoteBrokerOffsetStore,保存offset到broker文件中
                        this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
                        break;
                    default:
                        break;
                }
                this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
            }
            this.offsetStore.load();

            if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
                this.consumeOrderly = true;
                //TODO:创建顺序消费消息服务类
                this.consumeMessageService =
                    new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
            } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
                this.consumeOrderly = false;
                //TODO:创建其他消费消息服务类,其内部维护了一个线程池,后面会用到
                this.consumeMessageService =
                    new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
            }

            this.consumeMessageService.start();
            //TODO:注册consumer到本地
            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);
            }

            //启动客户端实例,这个方法非常重要,内部做了很多事情,后面会说
            mQClientFactory.start();
            log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
            this.serviceState = ServiceState.RUNNING;
            break;
        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;
    }
    
    this.updateTopicSubscribeInfoWhenSubscriptionChanged();
    this.mQClientFactory.checkClientInBroker();
    //TODO:发送消息到broker
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
    //TODO:立即重平衡
    this.mQClientFactory.rebalanceImmediately();
}

2.1 检查配置

  1. 校验GroupName是否为空;校验GroupName是否等于DEFAULT_CONSUMER(等于的话直接抛出异常)
  2. 校验消费模式:集群/广播                            
  3. 校验ConsumeFromWhere                         
  4. 校验开始消费的指定时间                            
  5. 校验AllocateMessageQueueStrategy         
  6. 校验订阅关系                                          
  7. 校验是否注册消息监听                               
  8. 校验消费线程数,consumeThreadMinconsumeThreadMax 默认值都是20,取值区间都是 [1, 1000]   
  9. 校验本地队列缓存消息的最大数,默认是1000,取值范围是[1, 1024], 主要是做流控用的
  10. 校验拉取消息的时间间隔,pullInterval参数,默认是不存在间隔,取值范围是[0, 65535]。当消费速度比生产速度快,可以设置这个参数,避免花费大概率从broker拉取空消息
  11. 校验单次拉取的最大消息数,consumeMessageBatchMaxSize 参数,默认是1,取值范围是[1, 1024] 
  12. 校验单次消费的最大消息数, pullBatchSize 参数,默认是32,取值范围是[1, 1024]。

2.2 拷贝订阅关系

将订阅关系设置到重平衡服务类RebalanceImpl

//TODO:key=topic, value=订阅数据(就是tag信息)
protected final ConcurrentMap<String /* topic */, SubscriptionData> subscriptionInner =
    new ConcurrentHashMap<String, SubscriptionData>();

2.3 创建客户端实例 MQClientInstance

//TODO: 当后面启动的时候会做很多事情,请继续往后看
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);

这个对象在前面分析Producer启动时也看到了,因为Prodcuer/Consumer 都是客户端,所以都会根据这个实例来创建对象。 我们看下这个类的结构:

public class MQClientInstance {
    
        //TODO:生产者表,producer启动时创建一个新的MQClientInstance实例对象,将生产者信息注册到这里。生产者实例对象中消费者信息是空
        private final ConcurrentMap<String/* group */, MQProducerInner> producerTable = new ConcurrentHashMap<String, MQProducerInner>();

        //TODO:消费者表,consumer启动时创建一个新的MQClientInstance实例对象,将消费者信息注册到这里。消费者实例对象中生产者信息是空
        private final ConcurrentMap<String/* group */, MQConsumerInner> consumerTable = new ConcurrentHashMap<String, MQConsumerInner>();

        //TODO:topic路由信息,producer和consumer都会使用
        private final ConcurrentMap<String/* Topic */, TopicRouteData> topicRouteTable = new ConcurrentHashMap<String, TopicRouteData>();

        //TODO:broker信息,producer和consumer都会用到
        private final ConcurrentMap<String/* Broker Name */, HashMap<Long/* brokerId */, String/* address */>> brokerAddrTable =
            new ConcurrentHashMap<String, HashMap<Long, String>>();
        
        //TODO......
        
        
       /**
        * TODO:构造器
        */
       public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) {
        //TODO:客户端处理器,比如在集群消费模式下,有新的消费者加入,则通知消费者客户端重平衡,是给消费者用的(分析生产者时,我们直接忽略了它)
        this.clientRemotingProcessor = new ClientRemotingProcessor(this);
        
        //TODO:它的内部会创建netty客户端对象(NettyRemotingClient),用于和broker通信
        this.mQClientAPIImpl = new MQClientAPIImpl(this.nettyClientConfig, this.clientRemotingProcessor, rpcHook, clientConfig);

        //TODO.......

        //TODO:拉取消息的服务,和消费者相关
        this.pullMessageService = new PullMessageService(this);

        //TODO:重平衡服务,和消费者相关
        this.rebalanceService = new RebalanceService(this);
        
        //TODO:other......
    }
}        

所谓的"客户端",实际上是在MQClientAPIImpl对象的内部的NettyRemotingClient

public class MQClientAPIImpl {
    //TODO:.....
    
    private final RemotingClient remotingClient;
    private final TopAddressing topAddressing;
    private final ClientRemotingProcessor clientRemotingProcessor;
    private String nameSrvAddr = null;
    private ClientConfig clientConfig;

    public MQClientAPIImpl(final NettyClientConfig nettyClientConfig,
        final ClientRemotingProcessor clientRemotingProcessor,
        RPCHook rpcHook, final ClientConfig clientConfig) {
        this.clientConfig = clientConfig;
        topAddressing = new TopAddressing(MixAll.getWSAddr(), clientConfig.getUnitName());
         
        //TODO:所谓的客户端实际上就是这个,netty客户端 
        this.remotingClient = new NettyRemotingClient(nettyClientConfig, null);
        this.clientRemotingProcessor = clientRemotingProcessor;

        this.remotingClient.registerRPCHook(rpcHook);
       
        //TODO:注册处理器
        this.remotingClient.registerProcessor(RequestCode.CHECK_TRANSACTION_STATE, this.clientRemotingProcessor, null);
        this.remotingClient.registerProcessor(RequestCode.NOTIFY_CONSUMER_IDS_CHANGED, this.clientRemotingProcessor, null);

     //TODO:....注册其他处理器......
    }

这里我们关注下 NettyRemotingClient,其实前面分析Broker,Producer时,我们经常看到这个,因为RocketMQ是使用netty作为通信的。

NettyRemotingClient:netty的客户端对象
NettyRemotingServer:netty的服务端对象

2.4 创建拉取消息的核心类 PullAPIWrapper

this.pullAPIWrapper = new PullAPIWrapper(
    mQClientFactory,
    this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());

2.5 创建 offset存储服务 OffsetStore

  • 如果是广播模式(BROADCASTING), 则创建 LocalFileOffsetStore 对象,将消费者的offset存储到本地的,默认文件路径为当前用户主目录下的 .rocketmq_offsets/clientId/{clientId}/{group}/Offsets.json。其中clientId为当前消费者id,默认为ip@default,{clientId}为当前消费者id,默认为ip@default, {group}为消费者组名称
  • 如果是集群模式(CLUSTERING), 则创建 RemoteBrokerOffsetStore对象,将消费者的offset存储到broker中,文件路径为当前用户主目录下的store/config/consumerOffset.json

2.6 创建消费消息的服务类 ConsumeMessageService

  • 如果是顺序消费,则创建 ConsumeMessageOrderlyService 对象
  • 如果是其他消费,则创建 ConsumeMessageConcurrentlyService 对象,同时内部也会创建一个ThreadPoolExecutor线程池,这个线程池非常的重要,拉取到消息后会将消息提交到这个线程池中给消费者消费

2.7 将consumer注册到本地

boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);

将消费者组信息添加到本地客户端实例MQClientInstanceconsumerTable Map中,key=groupName; value=DefaultMQPushConsumerImpl,就是消费者对象

2.8 启动客户端实例(重要)

mQClientFactory.start();

其内部做了很多事情,主要如下:

2.8.1 启动远程netty客户端

// Start request-response channel
this.mQClientAPIImpl.start();

就是启动其内部的 NettyRemotingClient,用于和broker通信。

2.8.2 启动各种定时任务

// Start various schedule tasks
this.startScheduledTask();

那么有哪些定时任务呢?继续往里看,罗列几个特别关注的

2.8.2.1 发送心跳到Broker

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        try {
            MQClientInstance.this.cleanOfflineBroker();
            MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
        } catch (Exception e) {
            log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
        }
    }
}, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);

延迟1s执行,每隔30s发送一次心跳包

2.8.2.2 持久化消费者的 offset

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

    @Override
    public void run() {
        try {
            MQClientInstance.this.persistAllConsumerOffset();
        } catch (Exception e) {
            log.error("ScheduledTask persistAllConsumerOffset exception", e);
        }
    }
}, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

延迟10s执行,每隔5s持久化一次offset
这里的持久化是将本地Map中offset发送到broker中,然后broker中的定时任务写到文件中,完成真正的持久化,后面会看到。

2.8.3 启动拉取消息的服务PullMessageService(重要)

// Start pull service
this.pullMessageService.start();

他是一个异步线程,其核心逻辑是

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

    while (!this.isStopped()) {
        try {
            PullRequest pullRequest = this.pullRequestQueue.take();
            this.pullMessage(pullRequest);
        } catch (InterruptedException ignored) {
        } catch (Exception e) {
            log.error("Pull Message Service Run Method exception", e);
        }
    }

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

他会监听阻塞队列pullRequestQueue,当队列是空的时候,他会一直阻塞,如果不为空,则获取PullRequest对象,去拉取消息,这个逻辑后面再说。

刚开始肯定是阻塞的,我们要看什么时候往队列中放入值,以及放入值之后做什么

2.8.4 启动重平衡服务 RebalanceService

// Start rebalance service
this.rebalanceService.start();

它也是一个异步线程,其核心逻辑是

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

    while (!this.isStopped()) {
        //TODO:内部使用了juc的CountDownLatch, 使得这里启动后仍然是阻塞的
        this.waitForRunning(waitInterval);
        this.mqClientFactory.doRebalance();
    }

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

他是重平衡的核心逻辑,但是在启动时,由于使用了JUC的 CountDownLatch锁,使其不会立即重平衡,而是阻塞,什么时候出发重平衡呢?我们还是继续往后看

2.9 发送心跳到broker

this.updateTopicSubscribeInfoWhenSubscriptionChanged();
this.mQClientFactory.checkClientInBroker();
//TODO: 发送心跳到broker
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
this.mQClientFactory.rebalanceImmediately();

是不是有些疑惑?前面的定时任务中不是已经启动了心跳服务吗,为什么这里还要启动呢?我也不清楚,猜测是因为担心网络问题导致没有及时发送给broker吧

2.10 立即启动重平衡服务RebalanceService

this.updateTopicSubscribeInfoWhenSubscriptionChanged();
this.mQClientFactory.checkClientInBroker();
//TODO: 发送心跳到broker
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
//TODO:立即启动重平衡服务
this.mQClientFactory.rebalanceImmediately();

前面在 2.8.4的时候,启动了重平衡服务,但是因为 CountDownLatch 导致阻塞了,这里就是唤醒,可以执行重平衡的逻辑。

这里先不关注它的内部逻辑,后面在分析。

其实,到这里Consumer客户端启动流程就结束了,至于如何拉取消息,也是这些服务类的相互配合工作的,后面我们在分析Consumer消费流程中在仔细分析它是如何一步一步拉取消息并消费的。

3. 总结

消费者启动过程相比生产者启动过程要复杂一些,会启动很多对象(实际上生产者也启动了,因为他们都是基于客户端实例MQClientInstance去创建的对象,只不过生产者并不会使用某些服务类)

简单总结下:

  1. 创建客户端实例MQClientInstance(其内部要创建netty客户端对象NettyRemotingClient
  2. 启动客户端实例,其内部要启动netty客户端,消息拉取服务,以及重平衡服务,还有各种定时任务
  3. 发送心跳到broker

好了,Consumer启动过程就分析到这里,接下来就分析下消费过程。

限于作者个人水平,文中难免有错误之处,欢迎指正! 勿喷,感谢