消息发送流程分析
- consumer启动以及拉取消息
- 消息消费
继前文顺藤摸瓜RocketMQ之消息发送debug解析分析完消息发送的流程之后,我们接着分析rocketmq的消息消费流程,环境搭建见前面的文章消息发送。
消息发送流程分析
consumer启动以及拉取消息
来到我们的代码org.apache.rocketmq.example.quickstart.Consumer: new了一个consumer的默认实现,然后设置了一些必要信息之后,就执行start方法,我们跟进:
public class Consumer {
public static void main(String[] args) throws InterruptedException, MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.subscribe("TopicTest", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
来到org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#start方法,代码有点多,但是看到CREATE_JUST似曾相识,和producter里面的一样的,我们先分析this.copySubscription()方法:
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;
this.checkConfig();
this.copySubscription();
if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
this.defaultMQPushConsumer.changeInstanceNameToPID();
}
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);
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:
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:
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;
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();
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();
this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
this.mQClientFactory.rebalanceImmediately();
}
在这个方法里面第一次进去sub订阅信息是空的,走到下面默认是CLUSTERING模式,将%RETRY%consumer_group 这个topic放到了RebalanceImpl的subscriptionInner集合里面,这个subscriptionInner是干么用的呢,从类名可以猜测是将主题信息都放到这个集合里面然后用来负载均衡用的,
回到我们的主线org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#start:
这段是设置了实例名称(根据时间定的id),
接下来执行了getOrCreateMQClientInstance方法构建了MQClientInstance实例:
接着将rebalanceImpl设置了一些必要的值用来负载均衡:
接着根据消费的模式将offsetStore和consumeMessageService设置一下,其中offsetStore的默认实现是RemoteBrokerOffsetStore这个类,consumeMessageService这个类的默认实现是ConsumeMessageConcurrentlyService这个类,接下来,this.consumeMessageService.start();这个方法清除过期消息,接下来是将consumer注册到mQClientFactory对象里面,所以mQClientFactory才是最主要的调度类,我们最后进入这个mQClientFactory.start();启动方法:
来到这个方法里面:
我们发现这个和之前producter启动的是一个类,这个在顺藤摸瓜RocketMQ之消息发送debug解析已经分析过了
这边解答上一个章节的一个问题: 为什么producer和consumer都要启动这个类呢? 这边producer如果启动了这个类,那么ServiceState就会被设置成START_FAILED,因为是synchronized修饰的,所以consumer来到这里一定是走START_FAILED分支,提示已经被创建。也就是说在一个JVM里面MQClientInstance这个类只会启动一次。
我们这边主要看this.pullMessageService.start();这个方法,跟进去:
消息消费
来到这个类PullMessageService,是个线程类,我们直接来到他的run方法:
从一个集合里面拿到pullRequest,
这里解释一下pullrequest里面的内容:
分别是消费者组consumerGroup
待拉取消费队列messageQueue
消息处理队列processQueue
待拉取消费队列的偏移量nextOffset
是否被锁定lockedFirst
然后执行this.pullMessage(pullRequest);方法,我们这个时候就明白了this.pullMessage(pullRequest);这个方法就是去broker去拉取消息,就像producter一样,我们猜测他也是够构建了拉取请求然后通过启动的netty的客户端去发送到broker:
这边留个问题:pullRequestQueue这个请求集合里面是什么时候放进去的? 这个下面在分析,这边主要先分析pullMessage方法:继续跟进:
这个地方从fectory拿到了consumer,还记得上面我们将consumer注册到了fectory里面的吗,
接下来就来到了org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage方法:
这里我就不贴整段代码了,大家打开源码跟进:
设置一下时间
makeSureStateOK()检查状态,确定客户端启动了,
这里拿到cachedMessageCount缓存的消息数量,以及cachedMessageSizeInMiB缓存的消息总量的大小,如果缓存的消息长度达到1000或者大小总和达到100M,那就回限流,this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);这个方法,让这个消息延迟50ms之后再次放入请求集合里面,也就是说只要消息数量和大小达到了阈值,就会停止拉消息,进行限流,
而如果消息的首尾相隔2000个消息的话,也会进行限流,因为相隔过长,说明很久的消息都没有进行消费,说消息积压了,所以要进行限流,接下来的代码,定义了一个回调callback,用于拉到消息中后进行消费,我们后面分析,
接下来拿到queue的offset
接下来来到最核心的方法:
参数在下面读者可以一目了然:
我们接着跟进:
接下来是构建请求头requestHeader,然后调用this.mQClientFactory.getMQClientAPIImpl().pullMessage方法去broker上拉取消息,
拉取到消息之后,我们先看一下处理reponse,然后接下来再分析callback,
进行了转换消息的code,以及封装了一下结果:
最后
拉取到消息之后就会来到我们之前定义的callback,来到方法org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage里面找到定义的PullCallback,这边我们打个debug,然后发送一些消息 然后启动消费者,进入到里面,
拉取到消息pullrequest不为空,然后更新pullrequest的下一次拉取偏移量:
如果没拉到消息,就吧pullrequest再次放到集合里面,等下次拉取,那么问题来了,pullrequest的status是found的拿为什么还能为空呢?因为rocketmq根据TAG进行过滤的,在服务端只是验证了tag的hashcode,在客户端会再次对tag信息过滤,所以会出现为空的情况。
接着往下: 将拉取到的消息放到processqueue里面,然后将消息提交到consumeMessageService里面供消费者进行消费,consumeMessageService具体怎么消费的我们接下来再分析,这边先分析提交消费任务之后接下来的流程,submitConsumeRequest是异步的流程,不会再这里阻塞,所以接着往下走:
然后将pullrequest再次放到集合里面供消息拉取:
接下来我们分析一下消息的消费:我们来到ConsumeMessageConcurrentlyService类,刚刚是调用的submitConsumeRequest方法,我们直接看这个方法,我们刚刚的msgs的长度是12所以走下面的分支,我们可以看到他的逻辑:
这里还是根据consumeMessageBatchMaxSize=1这个参数进行批量消费,但是默认是1,所以我们在客户端拿到的消息都是一条一条的,而我们客户端接受的却是个list(见下图),
然后将消费的request提交到消费者的线程中,如果拒绝了就等5s在提交submitConsumeRequestLater:
接着来到org.apache.rocketmq.client.impl.consumer.ConsumeMessageConcurrentlyService.ConsumeRequest这个线程类,执行他的run方法,这里拿到我们一开始放进去的listener(如下图),然后下面进行调用,
然后根据返回的状态,设置一下returntype,接着根据returntype进行一些处理:
最后来到 ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this);
这个方法,
根据消息监听返回的结果计算ackindex,如果返回的是consumer_success则将ackindex设置为msgs.size()-1如果reconsume_later 则ackindex=-1,为下文发送ack准备:
接着来到最后,将该消息从processqueue里面删除这条消息,这里返回的offset是删除之后queue里面最小的偏移量,然后使用该偏移量作为消费进度,
这边是org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore这个实现类里面的:
将消费进度存储在RemoteBrokerOffsetStore这个类里面,还记得在前面文章顺藤摸瓜RocketMQ之消息发送debug解析client启动的时候执行了一些定时任务吗?org.apache.rocketmq.client.impl.factory.MQClientInstance#startScheduledTask在这个方法里面:如下图,有一个定时任务,每10s就会执行持久化消费进度,
在这个定时任务中最终调用到了this.offsetStore.persistAll(mqs);而offsetStore的集群模式实现类是我们刚刚分析的RemoteBrokerOffsetStore,我们跟进这个方法persistAll:
执行了updateConsumeOffsetToBroker方法,
最终我们会走到org.apache.rocketmq.client.consumer.store.RemoteBrokerOffsetStore#updateConsumeOffsetToBroker(org.apache.rocketmq.common.message.MessageQueue, long, boolean)这个方法中:
这里将offset放到请求头里面,然后发送了一个Oneway的请求给broker,接下来就是调用remoting模块进行发送了。消息的消费进度也就能更新到broker端了
至此,rocketmq的消息的消费流程集群模式消费的大致正常运行的流程就已经分析完了,希望读者耐下心来一步一步跟进源码用debug看看各个参数都是什么内容,比如消息的格式是什么呀的,请求头是怎么构建的,里面都有哪些东西,processqueue里面各个集合都是存放哪些东西的,等等。相信跟完流程,这些问题也就都有了答案。
- 顺藤摸瓜RocketMQ之整体架构以及执行流程
- 顺藤摸瓜RocketMQ之注册中心debug解析
- 顺藤摸瓜RocketMQ之消息发送debug解析
- 顺藤摸瓜RocketMQ之消息消费debug解析
- 顺藤摸瓜RocketMQ之刷盘机制debug解析
- 顺藤摸瓜RocketMQ之主从同步(HA)解析