带着问题去研究中间件,想想自己实现如何实现
前提
通过架构可以知道下面角色之间的对应关系
- 主题:消息队列(MessageQueue)= 1:n
- 主题:消息生产者 = 1:n (n>=1)
- 主题:消息消费者 = 1:n(n>=1)
问题
围绕着上面的关系那么就会存在三类问题
消费者
- 消费组角度:一个消费组中多个消费者是如何对消息队列(1个主题多个消息队列)进行负载消费的。
- 消费者角度:一个消费者中多个线程又是如何协作(并发)的消费分配给该消费者的消息队列中的消息呢?
- 增量消费方面:消息消费进度如何保存,包括MQ是如何知道消息是否正常被消费了。
重要的类
不熟悉的话,可以边看变回来查看具体的功能
- DefaultMQPushConsumerImpl :消息消息者默认实现类,应用程序中直接用该类的实例完成消息的消费,并回调业务方法。
- RebalanceImpl 字面上的意思(重新平衡)也就是消费端消费者与消息队列的重新分布,与消息应该分配给哪个消费者消费息息相关。
- MQClientInstance 消息客户端实例,负载与MQ服务器(Broker,Nameserver)交互的网络实现
- PullAPIWrapper Pull与Push在RocketMQ中,其实就只有Pull模式,所以Push其实就是用pull封装一下
- MessageListenerInner 消费消费回调类,当消息分配给消费者消费时,执行的业务代码入口
- OffsetStore 消息消费进度保存
- ConsumeMessageService 消息消费逻辑
源码
Consumer 启动
入口:DefaultMQPushConsumer 的 start 方法
RebalanceImpl启动
依赖的 MQClientFactory 的初始化
kotlin
//IMP 决定了后面的 MQInstance 也就是与其他组件的交换逻辑
//如果是集群消费模式,如果instanceName为默认值 "DEFAULT",那么改成 UtilAll.getPid() + "#" + System.nanoTime()
if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
this.defaultMQPushConsumer.changeInstanceNameToPID();
}
/*
* K2 3 获取MQClientManager实例,然后根据clientId获取或者创建CreateMQClientInstance实例,并赋给mQClientFactory变量
*
* MQClientInstance封装了RocketMQ底层网络处理API,Producer、Consumer都会使用到这个类,是Producer、Consumer与NameServer、Broker 打交道的网络通道。
* 因此,同一个clientId对应同一个MQClientInstance实例就可以了,即同一个应用中的多个producer和consumer使用同一个MQClientInstance实例即可。
*/
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
rebalanceImpl初始化,主要其中注入了 MQClientFactory,那岂不是一个 PushConsuemr 实例,必然有一个次对象,这个对象的功能,后续说明
kotlin
/*
* K2 4 设置负载均衡服务的相关属性
* RebalanceImpl 要解决的问题:对 MessageQueue 资源的重平衡
*/
this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
this.rebalanceImpl.setmQClientFactory(this.mQClientFactory);
PullAPIWrapper 对象构建
此对象的构建中,也存在一个 mQClientFactory
kotlin
/*
* IMP 核心组件,无论是推还是拉都是使用此组件来执行的
* K2 5 创建消息拉取核心对象PullAPIWrapper,封装了消息拉取及结果解析逻辑的API0º
*/
this.pullAPIWrapper = new PullAPIWrapper(
mQClientFactory,
this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
//为PullAPIWrapper注册过滤消息的钩子函数
this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList);
OffsetStore 对象构建
同样的也 MQClientFactory 注入到对象中,同时此处,针对不同的消费方式,消息存储在不同的地方本地和远端 Broker( 如果 Broker 不进行通信的话,岂不是会丢失进度呢??? )
kotlin
/* IMP OffsetStore 是用于记录当前消费者消费进度的一个组件
* LocalFileOffsetStore:顾名思义,就是将消费进度存储在 Consumer 本地,Consumer 会在磁盘上生成文件以保存进度。
* RemoteBrokerOffsetStore:将消费进度保存在远端的 Broker。
*
* K2 6 根据消息模式设置不同的OffsetStore,用于实现消费者的消息消费偏移量offset的管理
*/
if (this.defaultMQPushConsumer.getOffsetStore() != null) {
this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
} else {
//根据不用的消费模式选择不同的OffsetStore实现
switch (this.defaultMQPushConsumer.getMessageModel()) {
case BROADCASTING:
//如果是广播消费模式,则是LocalFileOffsetStore,消息消费进度即offset存储在本地磁盘中。
this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
case CLUSTERING:
//如果是集群消费模式,则是RemoteBrokerOffsetStore,消息消费进度即offset存储在远程broker中。
this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
break;
default:
break;
}
this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
}
ConsumeMessageService 消费管理构建
kotlin
/*
* K2 8 根据消息监听器的类型创建不同的消息消费服务
*/
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
//如果是MessageListenerOrderly类型,则表示顺序消费,创建ConsumeMessageOrderlyService
this.consumeOrderly = true;
this.consumeMessageService =
new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
//如果是MessageListenerConcurrently类型,则表示并发消费,创建ConsumeMessageOrderlyService
this.consumeOrderly = false;
this.consumeMessageService =
new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}
//启动消息消费服务
this.consumeMessageService.start();
MQClientInstance 启动
- 里面干的事情,各种定时任务之类的,同时这个对象里面包含了很多重要对象,包含了,各种消费者对象,路由信息,broker 信息,以及拉取服务等等,
定时任务
- 具体任务如下
PullMessageService 启动
- 是一个现成,那么肯定是一个自旋的任务去执行
kotlin
@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");
}
java
private void pullMessage(final PullRequest pullRequest) {
//获取消费者
final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
if (consumer != null) {
DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
impl.pullMessage(pullRequest);
} else {
log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
}
}
csharp
public MQConsumerInner selectConsumer(final String group) {
return this.consumerTable.get(group);
}
- 上面说明了啥呢,cnsumerTable 是一个 Map结构,如果一个应用创建了同一个消费者组的多个消费者,此处会怎样呢,必然只会选择一个,所以,同一个应用增加多个消费者并不会 提高消费效率。但是实际上看了,对象而言,这个 consumerTable 是私有变量,根本不会进行重用,应该两个都会消费的,这个地方应该有什么问题,请教一下 大佬问问
-
看到这里,终于知道了,其实内部还是启动的其他线程(PullMessageService)来执行拉取消息。那么 Consumer 和 PullMessageService 的关系是什么样子的呢?
- 一个应用程序(消费端),一个消费组 一个 DefaultMQPushConsumerImpl ,同一个IP:端口,会有一个MQClientInstance ,而每一个MQClientInstance中持有一个PullMessageServive实例,故可以得出如下结论:同一个应用程序中,如果存在多个消费组,那么多个DefaultMQPushConsumerImpl 的消息拉取,都需要依靠一个PullMessageServive
- 简言之就是,一个 Consumer 对应一个 MQClientIntstance,也对应一个 PullMessageService,不同的 Consumer 对应的 PullMessageService 不一样。和大佬沟通之后,发现,之前的版本都是Producer 和 Consumer 都是底层使用同一个 MQClientInstance,但是呢,现在都是一对一的,即一个 Consumer 使用一个 MQClientInstance,也就底层一个 PullMessageService 了。终于解惑了。。。
PullMessageService 是依赖内存队列的请求进行拉取消息的,那么这个请求是什么时候加入到这个对象里面的呢???
PullMessageService 拉取消息
-
上面的循环不断的从队列中获取 PullRequest
javaprivate void pullMessage(final PullRequest pullRequest) { final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup()); if (consumer != null) { DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer; impl.pullMessage(pullRequest); } else { log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest); } }
-
上面的拉取消息,最终回到 DefaultMQPushConsumerImpl的pullMessage
DefualtMQPushConsumerImpl 的 pullMessage 方法
- 首先获取PullRequest的 处理队列ProcessQueue,然后更新该消息队列最后一次拉取的时间。
scss
// IMP
// ProcessQueue 内部会通过 TreeMap 来存放这些暂时还没有被消费的 Message,TreeMap 是一个用红黑树实现的有序 Map。
// Key 是消息在当前 ProcessQueue 所对应的 MessageQueue 中的偏移量,Value 就是 Message 自身。
final ProcessQueue processQueue = pullRequest.getProcessQueue();
if (processQueue.isDropped()) {
log.info("the pull request[{}] is dropped.", pullRequest.toString());
return;
}
pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());
- 如果消费者 服务状态不为ServiceState.RUNNING,或当前处于暂停状态,默认延迟3秒再执行(PullMessageService.executePullRequestLater)。 如何实现延迟执行呢,简单就使用 Sleep 方法。等会看看里面具体如何实现延迟的操作。
kotlin
try {
this.makeSureStateOK();
} catch (MQClientException e) {
log.warn("pullMessage exception, consumer state not ok", e);
//延迟执行
this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException);
return;
}
//暂停
if (this.isPause()) {
log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
return;
}
延迟消息实现
-
使用定时任务线程池,然后过了多久之后,重新放回到 PullRequest 中,可见这个延迟也是个大概的时间,具体需要等消费者也就是 PullMessageService 拉取到才去执行
javapublic void executePullRequestLater(final PullRequest pullRequest, final long timeDelay) { if (!isStopped()) { this.scheduledExecutorService.schedule(new Runnable() { @Override public void run() { PullMessageService.this.executePullRequestImmediately(pullRequest); } }, timeDelay, TimeUnit.MILLISECONDS); } else { log.warn("PullMessageServiceScheduledThread has shutdown"); } } //PullMessageService.executePullRequestImmediately /** * PullMessageService的方法 * 下一次消息拉取 * * @param pullRequest 拉取请求 */ public void executePullRequestImmediately(final PullRequest pullRequest) { try { //存入pullRequestQueue集合,等待下次拉取 this.pullRequestQueue.put(pullRequest); } catch (InterruptedException e) { log.error("executePullRequestImmediately pullRequestQueue.put", e); } }
-
拉取消息进行限流限速
- 消息数量达到阔值(默认1000个)
- 消息体总大小(默认100m)
scss
long cachedMessageCount = processQueue.getMsgCount().get();
long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
// 数量
if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
if ((queueFlowControlTimes++ % 1000) == 0) {
log.warn(
"the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
}
return;
}
//消息体大小
if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
if ((queueFlowControlTimes++ % 1000) == 0) {
log.warn(
"the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
}
return;
}
非顺序消息拉取
- 如果我们自己实现的话,肯定是,获取订阅的 broker 地址信息(通过版本进行增量拉取,同时设置最大拉取次数,防止,过多拉取失败,导致消费太慢),然后去 broker 中拉取消息,如果拉取失败了,进行重试处理。看看 MQ 是如何实现的呢。
-
获取主题订阅信息
kotlinfinal SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic()); if (null == subscriptionData) { this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); log.warn("find the consumer's subscription failed, {}", pullRequest); return; }
-
主题的订购信息,实体类
vbnetpublic class SubscriptionData implements Comparable<SubscriptionData> { public final static String SUB_ALL = "*"; private boolean classFilterMode = false; private String topic; private String subString; private Set<String> tagsSet = new HashSet<String>(); private Set<Integer> codeSet = new HashSet<Integer>(); private long subVersion = System.currentTimeMillis(); private String expressionType = ExpressionType.TAG; }
-
-
构造回调方法,涉及到重试操作
-
获取偏移量,好吧,那上面的当前拉取时间是干嘛用的呢???如果不记得可以返回去看看 setLastPullTimestamp
-
如果是集群模式,就去内存中获取偏移量
inilong commitOffsetValue = 0L; if (MessageModel.CLUSTERING == this.defaultMQPushConsumer.getMessageModel()) { //此处会涉及本地获取还是集群获取 commitOffsetValue = this.offsetStore.readOffset(pullRequest.getMessageQueue(), ReadOffsetType.READ_FROM_MEMORY); if (commitOffsetValue > 0) { commitOffsetEnable = true; } }
-
-
拉取消息,好嘛,最后还是交给了别人来进行拉取,自己就是构造了请求参数等等信息,不过也是,专业的事儿找专业的人来干,后续可以很好的扩展
less//IMP 拉取消息 this.pullAPIWrapper.pullKernelImpl( pullRequest.getMessageQueue(), subExpression, subscriptionData.getExpressionType(), subscriptionData.getSubVersion(), pullRequest.getNextOffset(), this.defaultMQPushConsumer.getPullBatchSize(), sysFlag, commitOffsetValue, BROKER_SUSPEND_MAX_TIME_MILLIS, CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND, CommunicationMode.ASYNC, pullCallback );
-
就是进一步获取 topic 和 broker 的地址信息,然后交由 MQClientAPIImpl 进行拉取消息
java//MQClientAPIImpl.pullMessage /** * IMP 拉取消息 * @param addr * @param requestHeader * @param timeoutMillis * @param communicationMode * @param pullCallback * @return * @throws RemotingException * @throws MQBrokerException * @throws InterruptedException */ public PullResult pullMessage( final String addr, final PullMessageRequestHeader requestHeader, final long timeoutMillis, final CommunicationMode communicationMode, final PullCallback pullCallback ) throws RemotingException, MQBrokerException, InterruptedException { RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader); switch (communicationMode) { case ONEWAY: assert false; return null; case ASYNC: this.pullMessageAsync(addr, request, timeoutMillis, pullCallback); return null; case SYNC: return this.pullMessageSync(addr, request, timeoutMillis); default: assert false; break; } return null; }
-
对于异步和同步而言,同步是等待返回结果,而异步,就是在 DefaultConsumerPushImpl 拉取消息的时候,创建的回调函数进行处理,其实这也是异步的常用套路。使用回调来进行结果处理
javaprivate void pullMessageAsync( final String addr, final RemotingCommand request, final long timeoutMillis, final PullCallback pullCallback ) throws RemotingException, InterruptedException { /* * 基于netty给broker发送异步消息,设置一个InvokeCallback回调对象 * * InvokeCallback#operationComplete方法将会在得到结果之后进行回调,内部调用pullCallback的回调方法 */ this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() { /** * 异步执行的回调方法 */ @Override public void operationComplete(ResponseFuture responseFuture) { //返回命令对象 RemotingCommand response = responseFuture.getResponseCommand(); if (response != null) { try { //解析响应获取结果 PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response, addr); assert pullResult != null; //如果解析到了结果,那么调用pullCallback#onSuccess方法处理 pullCallback.onSuccess(pullResult); } catch (Exception e) { //出现异常则调用pullCallback#onException方法处理异常 pullCallback.onException(e); } } else { //没有结果,都调用onException方法处理异常 if (!responseFuture.isSendRequestOK()) { //发送失败 pullCallback.onException(new MQClientException("send request failed to " + addr + ". Request: " + request, responseFuture.getCause())); } else if (responseFuture.isTimeout()) { //超时 pullCallback.onException(new MQClientException("wait response from " + addr + " timeout :" + responseFuture.getTimeoutMillis() + "ms" + ". Request: " + request, responseFuture.getCause())); } else { pullCallback.onException(new MQClientException("unknown reason. addr: " + addr + ", timeoutMillis: " + timeoutMillis + ". Request: " + request, responseFuture.getCause())); } } } }); }
-
-
回调方法
-
这个时候是不是想到几个点,如果你自己写的时候,
- 不同响应如何处理,成功,失败,异常
- 内存队列进行消费,如果一个拉取请求太久怎么办,或者 broker 超时了,怎么去重试,要求肯定是不能阻塞后续的消费
kotlinPullCallback pullCallback = new PullCallback() { @Override public void onSuccess(PullResult pullResult) { if (pullResult != null) { /* * K2 1 处理pullResult,进行消息解码、过滤以及设置其他属性的操作 */ pullResult = DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(pullRequest.getMessageQueue(), pullResult, subscriptionData); switch (pullResult.getPullStatus()) { case FOUND: //拉取的起始offset long prevRequestOffset = pullRequest.getNextOffset(); //设置下一次拉取的起始offset到PullRequest中 pullRequest.setNextOffset(pullResult.getNextBeginOffset()); //增加拉取耗时 long pullRT = System.currentTimeMillis() - beginTimestamp; DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullRT(pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), pullRT); long firstMsgOffset = Long.MAX_VALUE; //如果没有消息 if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) { /* * 立即将拉取请求再次放入PullMessageService的pullRequestQueue中,PullMessageService是一个线程服务 * PullMessageService将会循环的获取pullRequestQueue中的pullRequest然后向broker发起新的拉取消息请求 * 进行下次消息的拉取 */ DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); } else { //获取第一个消息的offset firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset(); //增加拉取tps DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size()); /* * K2 2 将拉取到的所有消息,存入对应的processQueue处理队列内部的msgTreeMap中 */ boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList()); /* * K2 3 通过consumeMessageService将拉取到的消息构建为ConsumeRequest,然后通过内部的consumeExecutor线程池消费消息 * consumeMessageService有ConsumeMessageConcurrentlyService并发消费和ConsumeMessageOrderlyService顺序消费两种实现 */ DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest( pullResult.getMsgFoundList(), processQueue, pullRequest.getMessageQueue(), dispatchToConsume); /* * K2 4 获取配置的消息拉取间隔,默认为0,则等待间隔时间后将拉取请求再次放入pullRequestQueue中,否则立即放入pullRequestQueue中 * 进行下次消息的拉取 */ if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) { /* * 将executePullRequestImmediately的执行放入一个PullMessageService的scheduledExecutorService延迟任务线程池中 * 等待给定的延迟时间到了之后再执行executePullRequestImmediately方法 */ DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval()); } else { /* * 立即将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取 */ DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); } } if (pullResult.getNextBeginOffset() < prevRequestOffset || firstMsgOffset < prevRequestOffset) { log.warn( "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}", pullResult.getNextBeginOffset(), firstMsgOffset, prevRequestOffset); } break; case NO_NEW_MSG: //没有匹配到消息 case NO_MATCHED_MSG: //更新下一次拉取偏移量 pullRequest.setNextOffset(pullResult.getNextBeginOffset()); DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest); //立即将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取 DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); break; //请求offset不合法,过大或者过小 case OFFSET_ILLEGAL: log.warn("the pull request offset illegal, {} {}", pullRequest.toString(), pullResult.toString()); //更新下一次拉取偏移量,这个下一次的开始偏移是broker那边进行返回的 pullRequest.setNextOffset(pullResult.getNextBeginOffset()); //丢弃拉取请求 pullRequest.getProcessQueue().setDropped(true); DefaultMQPushConsumerImpl.this.executeTaskLater(new Runnable() { @Override public void run() { try { //更新下次拉取偏移量 DefaultMQPushConsumerImpl.this.offsetStore.updateOffset(pullRequest.getMessageQueue(), pullRequest.getNextOffset(), false); //持久化offset DefaultMQPushConsumerImpl.this.offsetStore.persist(pullRequest.getMessageQueue()); //移除对应的消费队列,同时将消息队列从负载均衡服务中移除 DefaultMQPushConsumerImpl.this.rebalanceImpl.removeProcessQueue(pullRequest.getMessageQueue()); log.warn("fix the pull request offset, {}", pullRequest); } catch (Throwable e) { log.error("executeTaskLater Exception", e); } } }, 10000); break; default: break; } } } /* * 出现异常,延迟3s将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取 */ @Override public void onException(Throwable e) { if (!pullRequest.getMessageQueue().getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) { log.warn("execute the pull request exception", e); } DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, pullTimeDelayMillsWhenException); } };
- 上面无非就是针对不同的情况,来进行相应的处理,下面主要针对成功和异常的情况看看,成功之后如何处理,异常之后如何重试
-
-
拉取成功之后的处理
kotlin//获取第一个消息的offset firstMsgOffset = pullResult.getMsgFoundList().get(0).getQueueOffset(); //增加拉取tps DefaultMQPushConsumerImpl.this.getConsumerStatsManager().incPullTPS(pullRequest.getConsumerGroup(), pullRequest.getMessageQueue().getTopic(), pullResult.getMsgFoundList().size()); /* * K2 2 将拉取到的所有消息,存入对应的processQueue处理队列内部的msgTreeMap中 */ boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList()); /* * K2 3 通过consumeMessageService将拉取到的消息构建为ConsumeRequest,然后通过内部的consumeExecutor线程池消费消息 * consumeMessageService有ConsumeMessageConcurrentlyService并发消费和ConsumeMessageOrderlyService顺序消费两种实现 */ DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest( pullResult.getMsgFoundList(), processQueue, pullRequest.getMessageQueue(), dispatchToConsume); /* * K2 4 获取配置的消息拉取间隔,默认为0,则等待间隔时间后将拉取请求再次放入pullRequestQueue中,否则立即放入pullRequestQueue中 * 进行下次消息的拉取 */ if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) { /* * 将executePullRequestImmediately的执行放入一个PullMessageService的scheduledExecutorService延迟任务线程池中 * 等待给定的延迟时间到了之后再执行executePullRequestImmediately方法 */ DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest, DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval()); } else { /* * 立即将拉取请求再次放入PullMessageService的pullRequestQueue中,等待下次拉取 */ DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest); } } if (pullResult.getNextBeginOffset() < prevRequestOffset || firstMsgOffset < prevRequestOffset) { log.warn( "[BUG] pull message result maybe data wrong, nextBeginOffset: {} firstMsgOffset: {} prevRequestOffset: {}", pullResult.getNextBeginOffset(), firstMsgOffset, prevRequestOffset); } break;
-
拉取到消息之后,放到消费队列 processQueue,这还这么操作,拉取到难道不消费吗,各种内存队列,生产-消费者模式呀,生产者是当前的消费者 PushConsumer,内存队列就是加入的地方,那么消费者是谁呢,如何通知消费者来进行消费消息呢,后续可以看到,此方法中底层代码可以看到将消息加入一个 msgTreeMap 中,同时增加了一些统计数据
ini/** * IMP putMessage * * 该方法将拉取到的所有消息,存入对应的processQueue处理队列内部的msgTreeMap中。 * * 返回是否需要分发消费dispatchToConsume,当当前processQueue的内部的msgTreeMap中 * 有消息并且consuming=false,即还没有开始消费时,将会返回true。 * * dispatchToConsume对并发消费无影响,只对顺序消费有影响。 * @param msgs 一批消息 * @return 是否需要分发消费,当当前processQueue的内部的msgTreeMap中有消息并且consuming=false,即还没有开始消费时,将会返回true */ public boolean putMessage(final List<MessageExt> msgs) { boolean dispatchToConsume = false; try { //尝试加写锁防止并发 this.treeMapLock.writeLock().lockInterruptibly(); try { int validMsgCnt = 0; for (MessageExt msg : msgs) { //当该消息的偏移量以及该消息存入msgTreeMap // 此处肯定是两个数据结构的关联点 MessageExt old = msgTreeMap.put(msg.getQueueOffset(), msg); if (null == old) { //如果集合没有这个offset的消息,那么增加统计数据 validMsgCnt++; this.queueOffsetMax = msg.getQueueOffset(); msgSize.addAndGet(msg.getBody().length); } } //消息计数 msgCount.addAndGet(validMsgCnt); //当前processQueue的内部的msgTreeMap中有消息并且consuming=false,即还没有开始消费时,dispatchToConsume = true,consuming = true if (!msgTreeMap.isEmpty() && !this.consuming) { dispatchToConsume = true; this.consuming = true; } //计算broker累计消息数量 if (!msgs.isEmpty()) { MessageExt messageExt = msgs.get(msgs.size() - 1); String property = messageExt.getProperty(MessageConst.PROPERTY_MAX_OFFSET); if (property != null) { long accTotal = Long.parseLong(property) - messageExt.getQueueOffset(); if (accTotal > 0) { this.msgAccCnt = accTotal; } } } } finally { this.treeMapLock.writeLock().unlock(); } } catch (InterruptedException e) { log.error("putMessage exception", e); } return dispatchToConsume; }
-
消费消息服务提交(只看非顺序的)
- 上面是降拉取到 的消息存储到了 treeMap 中,其实如果这个接口是阻塞队列的话,是可以接受一个线程不断自旋获取,或者阻塞等待获取,如果有消息那么就去消费,但是通过源码分析,是利用 treeMap 的有序性,以及查询速度快的情况来作为存储消息的结构,那么就需要一个阻塞队列,来通知,消费者来进行消费,所以,此处就会有消息服务提交的一个步骤。
- 通过下面源码发现,是直接将 返回的消息,构造成一个 runnable 然后交给了线程池执行,底层也是一种生产-消费模式。有点奇怪了,那上面的 msgTreeMap 存储消息干嘛呢???
java@Override public void submitConsumeRequest( final List<MessageExt> msgs, final ProcessQueue processQueue, final MessageQueue messageQueue, final boolean dispatchToConsume) { //单次批量消费的数量,默认1 // consumeMessageBatchMaxSize是什么意思呢?他的字面意思就是单次批量消费的数量,实际上它代表着每次发送给 // 消息监听器MessageListenerOrderly或者MessageListenerConcurrently的consumeMessage方法中的参数List msgs中的最多的消息数量。 // // consumeMessageBatchMaxSize默认值为1,所以说,无论是并发消费还是顺序消费,每次的consumeMessage方法的执行, // msgs集合默认都只有一条消息。同理,如果把它设置为其他值n,无论是并发消费还是顺序消费,每次的consumeMessage的执行,msgs集合默认都最多只有n条消息。 // final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize(); /* * 如果消息数量 <= 单次批量消费的数量,那么直接全量消费 */ if (msgs.size() <= consumeBatchSize) { //构建消费请求,将消息全部放进去 ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue); try { // 重点 //将请求提交到consumeExecutor线程池中进行消费 this.consumeExecutor.submit(consumeRequest); } catch (RejectedExecutionException e) { //提交的任务被线程池拒绝,那么延迟5s进行提交,而不是丢弃 this.submitConsumeRequestLater(consumeRequest); } } /* * 如果消息数量 > 单次批量消费的数量,那么需要分割消息进行分批提交 */ else { //遍历 for (int total = 0; total < msgs.size(); ) { //一批消息集合,每批消息最多consumeBatchSize条,默认1 List<MessageExt> msgThis = new ArrayList<MessageExt>(consumeBatchSize); //将消息按顺序加入集合 for (int i = 0; i < consumeBatchSize; i++, total++) { if (total < msgs.size()) { msgThis.add(msgs.get(total)); } else { break; } } //将本批次消息构建为ConsumeRequest ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue); try { //将请求提交到consumeExecutor线程池中进行消费 this.consumeExecutor.submit(consumeRequest); } catch (RejectedExecutionException e) { //被拒绝之后,把所有的消息都加入到的msgThis这个集合中,整体延迟5s进行执行 for (; total < msgs.size(); total++) { msgThis.add(msgs.get(total)); } //提交的任务被线程池拒绝,那么所有后面的任务都延迟5s进行提交,而不是丢弃 this.submitConsumeRequestLater(consumeRequest); } } } }
- 每次超了每次消费的消息个数,那么就对消息进行分批处理,然后依次处理,不过这个地方写的,和我们写的也没啥区别
-
ConsumeRequest
单独拎出来是比较重要,也是每一批消息,消费的任务模型,直接上源码,又臭又长,还是慢慢分析吧,因为是 Runnable,懂得都懂
-
既然消费一批消息, 即 processQueue,那么一开始的一些校验肯定必不可少了,如果是重试消息的话,还原会真正的主题,咦??,所以,重试消息的主题在拉取到之后,broker 端不进行修改主题信息吗,为啥此处还有进行调整呢???后续重试消息的地方讲解
kotlin//如果处理队列被丢弃,那么直接返回,不再消费,例如负载均衡时该队列被分配给了其他新上线的消费者,尽量避免重复消费 if (this.processQueue.isDropped()) { log.info("the message queue not be able to consume, because it's dropped. group={} {}", ConsumeMessageConcurrentlyService.this.consumerGroup, this.messageQueue); return; } //重置重试topic defaultMQPushConsumerImpl.resetRetryAndNamespace(msgs, defaultMQPushConsumer.getConsumerGroup());
arduinopublic void resetRetryAndNamespace(final List<MessageExt> msgs, String consumerGroup) { //获取重试topic final String groupTopic = MixAll.getRetryTopic(consumerGroup); for (MessageExt msg : msgs) { //尝试通过PROPERTY_RETRY_TOPIC属性获取每个消息的真实topic String retryTopic = msg.getProperty(MessageConst.PROPERTY_RETRY_TOPIC); //如果该属性不为null,并且重试topic和消息的topic相等,则表示当前消息是重试消息 if (retryTopic != null && groupTopic.equals(msg.getTopic())) { //那么设置消息的topic为真实topic,即还原回来 msg.setTopic(retryTopic); } if (StringUtils.isNotEmpty(this.defaultMQPushConsumer.getNamespace())) { msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), this.defaultMQPushConsumer.getNamespace())); } } }
-
消费消息前的扩展点,钩子函数
ini/* * K2 2 如果有消费钩子,那么执行钩子函数的前置方法consumeMessageBefore * 我们可以注册钩子ConsumeMessageHook,再消费消息的前后调用 */ ConsumeMessageContext consumeMessageContext = null; if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) { consumeMessageContext = new ConsumeMessageContext(); consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace()); consumeMessageContext.setConsumerGroup(defaultMQPushConsumer.getConsumerGroup()); consumeMessageContext.setProps(new HashMap<String, String>()); consumeMessageContext.setMq(messageQueue); consumeMessageContext.setMsgList(msgs); consumeMessageContext.setSuccess(false); ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext); }
-
调用业务的监听器方法来处理消息
lesstry { if (msgs != null && !msgs.isEmpty()) { //循环设置每个消息的起始消费时间 for (MessageExt msg : msgs) { MessageAccessor.setConsumeStartTimeStamp(msg, String.valueOf(System.currentTimeMillis())); } } /* * K2 3 调用listener#consumeMessage方法,进行消息消费,调用实际的业务逻辑,返回执行状态结果 * 有两种状态ConsumeConcurrentlyStatus.CONSUME_SUCCESS 和 ConsumeConcurrentlyStatus.RECONSUME_LATER */ status = listener.consumeMessage(Collections.unmodifiableList(msgs), context); } catch (Throwable e) { log.warn(String.format("consumeMessage exception: %s Group: %s Msgs: %s MQ: %s", RemotingHelper.exceptionSimpleDesc(e), ConsumeMessageConcurrentlyService.this.consumerGroup, msgs, messageQueue), e); //抛出异常之后,设置异常标志位 hasException = true; }
-
根据不同的消费状态 status 进行 处理,主要还是去判断,超时了,异常的情况,如何去处理呢
ini/* * K2 4 对返回的执行状态结果进行判断处理 */ //计算消费时间 long consumeRT = System.currentTimeMillis() - beginTimestamp; //如status为null if (null == status) { //如果业务的执行抛出了异常 if (hasException) { //设置returnType为EXCEPTION returnType = ConsumeReturnType.EXCEPTION; } else { //设置returnType为RETURNNULL returnType = ConsumeReturnType.RETURNNULL; } //如消费时间consumeRT大于等于consumeTimeout,默认15min } else if (consumeRT >= defaultMQPushConsumer.getConsumeTimeout() * 60 * 1000) { //设置returnType为TIME_OUT returnType = ConsumeReturnType.TIME_OUT; //如status为RECONSUME_LATER,即消费失败 } else if (ConsumeConcurrentlyStatus.RECONSUME_LATER == status) { //设置returnType为FAILED returnType = ConsumeReturnType.FAILED; //如status为CONSUME_SUCCESS,即消费成功 } else if (ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status) { //设置returnType为SUCCESS,即消费成功 returnType = ConsumeReturnType.SUCCESS; } //兜底策略,默认返回,没有返回的话,就认为是消费失败,等会在消费 //如果status为null if (null == status) { log.warn("consumeMessage return null, Group: {} Msgs: {} MQ: {}", ConsumeMessageConcurrentlyService.this.consumerGroup, msgs, messageQueue); //将status设置为RECONSUME_LATER,即消费失败 status = ConsumeConcurrentlyStatus.RECONSUME_LATER; }
-
如果有钩子,后续执行钩子方法
ini/* * K2 5 如果有消费钩子,那么执行钩子函数的后置方法consumeMessageAfter * 我们可以注册钩子ConsumeMessageHook,在消费消息的前后调用 */ if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) { consumeMessageContext.setStatus(status.toString()); consumeMessageContext.setSuccess(ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status); ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext); }
-
上面记录的消息消费之后的,状态, 但是针对不同的结果之后,没有具体的处理策略,显然不合适,只是执行了,消费前后的钩子方法,那么真正的处理方法如下,也是针对,成功,超时,异常,重试的后续一些操作,为啥要写一个方法呢,因为后续的特别多的逻辑需要处理,这样分开来说,每个方法指责不同,而且方便单侧
kotlin/* * K2 6 如果处理队列没有被丢弃,那么调用ConsumeMessageConcurrentlyService#processConsumeResult方法处理消费结果,包含重试等逻辑 */ if (!processQueue.isDropped()) { ConsumeMessageConcurrentlyService.this.processConsumeResult(status, context, this); } else { log.warn("processQueue is dropped without process consume result. messageQueue={}, msgs={}", messageQueue, msgs); }
总结
-
上面通过源码看到了,Consumer 启动的时候,启动了哪些数据
-
Consumer 中各种生产-消费模式进行获取消息,消费消息,涉及组件
- Consumer->PullMessageService(Thread,自旋获取PullRequest)->offStore->CosumerMessageService( 内部含有ThreadPool 进行异步执行)
遗留问题
对于消费端
- PullRequest是哪里产生的呢???
- 消费端消息负载均衡机制与重新分布
- MsgTreeMap 干嘛用的,为何要保存依次数据
- 消息消费进度保持机制