rocketmq源码分析3-consumer消息获取
使用rocketmq的大体消息发送过程如下:
在前面已经分析过MQ的broker接收生产者客户端发过来的消息的过程,此文主要讲述订阅者获取消息的过程,或者说broker是怎样将消息传递给消费者客户端的,即上面时序图中拉取消息(pull message)
动作。。
1. 如何找到入口(MQ-broker端)
分析一个机制或者功能时,我们首先希望的是找到入口,前一篇我们是通过端口号方式顺藤摸瓜的方式找到了入口。但是此篇略微不同,涉及到consumer客户端与broker的两边分析,最终发现逻辑还是比较绕的,主要有很多异步动作,还有循环调用(当然不是一个线程上,而且中间有阻塞队列缓冲),这对调试式分析代码造成了一些不方便。
回到正题,怎么找到这里入口?在具备上篇分析的基础上,我直接分析broker的代码,broker接收消息的时候是靠SendMessageProcessor,那么在消息传递给消费端的时候是不是也是靠某个processor完成的?据这些processor的命名观察,猜测PullMessageProcessor比较像。
为了验证这一想法,注释掉BrokerController中使用这个processor的地方,再重新测试,发现consumer就收不到producer发过来的消息了。想法初步正确。
2. 调试PullMessageProcessor(MQ-broker端)
RemotingServer在注册processor的时候,是根据RequestCode进行注册的。
PullMessageProcessor 对应的RequestCode的PULL_MESSAGE,即11。猜测:consumer客户端不断(或定时轮询,或循环调用,或其他方式)发起pull message请求给broker,broker会处理这些请求,后面会验证这个猜测。
PullMessageProcessor在注册的时候对应的线程池是pullMessageExecutor,线程池的corePoolSize以及maxPoolSize都可以在broker中进行config,字段名是pullMessageThreadPoolNums。默认值16+处理器个数*2。
3. 哪里发送了RequestCode为PULL_MESSAGE的请求(consumer客户端)
通过全局搜索,很容易发现是MQClientAPIImpl.pullMessage422行,发送了PULL_MESSAGE类型的请求。
加断点(consumer客户端需要debug方式启动),看调用堆栈。
很容易发现 是 PullMessageService.run()发出了PULL_MESSAGE的request。 run的代码如下:
while (!this.isStoped()) {
try {
PullRequest pullRequest = this.pullRequestQueue.take();
if (pullRequest != null) {
this.pullMessage(pullRequest);
}
}
catch (InterruptedException e) {
}
catch (Exception e) {
log.error("Pull Message Service Run Method exception", e);
}
}
在线程没有停止的情况下,一直循环发拉取消息的请求,过程中被pullRequestQueue阻塞队列阻塞。
分析谁向pullRequestQueue put了元素?是PullMessageService.executePullRequestImmediately(PullRequest)方法。
谁调了上面的方法,同样断点分析,调用堆栈如下图:
划蓝色线的ResponseFuture地方,是阿里对这种通过发送网络请求调用后还能回调回来的一个特性封装,值得学习。 划红色线的 MQClientAPIImpl$2地方是在处理业务逻辑,位于方法pullMessageAsync(String, RemotingCommand, long, PullCallback)内。此处又是一个异步。
private void pullMessageAsync(//
final String addr,// 1
final RemotingCommand request,//
final long timeoutMillis,//
final PullCallback pullCallback//
) throws RemotingException, InterruptedException {
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);
assert pullResult != null;
pullCallback.onSuccess(pullResult);// 457行对应此处
}
catch (Exception e) {
pullCallback.onException(e);
}
}
else {
//... 省略
}
}
});
}
4. 调试MQClientAPIImpl.pullMessageAsync(consumer客户端)
谁调用了MQClientAPIImpl.pullMessageAsync?
449行打断点,堆栈如下:
发现又回到上面的PullMessageService的run中。:-(
回头去看看3段落中pullCallback.onSuccess(pullResult);// 457行对应此处
这一行代码,跟进去就会发现玄机,在这里面又调用了PullMessageService.executePullRequestImmediately(PullRequest)方法。 是一个匿名内部类,位于DefaultMQPushConsumerImpl.pullMessage(PullRequest)中。这个方法太长,贴一些简略的,
final long beginTimestamp = System.currentTimeMillis(); PullCallback pullCallback = new PullCallback() {
@Override
public void onSuccess(PullResult pullResult) {
if (pullResult != null) {
pullResult =
DefaultMQPushConsumerImpl.this.pullAPIWrapper.processPullResult(
pullRequest.getMessageQueue(), pullResult, subscriptionData); switch (pullResult.getPullStatus()) {
case FOUND:
//...省略 long firstMsgOffset = Long.MAX_VALUE;
if (pullResult.getMsgFoundList() == null || pullResult.getMsgFoundList().isEmpty()) {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
else {
//...省略 if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
}
else {
DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
} //...省略
break;
case NO_NEW_MSG:
//...省略 DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
break;
case NO_MATCHED_MSG:
//...省略 DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
break;
case OFFSET_ILLEGAL:
//...省略
上述代码基本上很清楚地看出来拉取代码的发起逻辑了。在case FOUND分支打个断点,当你从producer的客户端发一条消息过来的时候,能看到断点被命中。当没有producer没有发消息的时候,一直走的就是case NO_NEW_MSG分支。
5. 上面分析的对吗?
不全对。为什么?回去看段落3的[1]标注出,关于是谁调用了PullMessageService.executePullRequestImmediately(PullRequest)方法。
其实不止是段落3分析的那样,还有RebalanceImpl.updateProcessQueueTableInRebalance(String, Set)417行发起的调用。
为什么会想到这个问题?因为按段落3 的分析,调用executePullRequestImmediately方法的入参是 PullRequest,但是上面分析是"循环"调用,那么最初始的这个PullRequest是哪里构造的?
带着这个疑问就不难发现肯定还有其他调用了executePullRequestImmediately方法。于是搜索,加断点,发现RebalanceImpl.updateProcessQueueTableInRebalance会调用。
其实通过搜索 new PullRequest 关键字也是很容易找到上述调用的地方。
谁对RebalanceImpl.updateProcessQueueTableInRebalance发起了调用?
观察调用栈:
Thread [RebalanceService] (Suspended (breakpoint at line 417 in RebalanceImpl))
RebalancePushImpl(RebalanceImpl).updateProcessQueueTableInRebalance(String, Set) line: 417
RebalancePushImpl(RebalanceImpl).rebalanceByTopic(String) line: 321 RebalancePushImpl(RebalanceImpl).doRebalance() line: 248
DefaultMQPushConsumerImpl.doRebalance() line: 250
MQClientInstance.doRebalance() line: 925
RebalanceService.run() line: 49 Thread.run() line: 695
RebalancePushImpl(RebalanceImpl).updateProcessQueueTableInRebalance(String, Set) line: 417
是
this.dispatchPullRequest(pullRequestList); 该方法对push形式会调用PullMessageService.executePullRequestImmediately。至此,疑问基本解决。
这个堆栈要在consumer启动时会发现,整个堆栈发生在线程 Thread [RebalanceService]
// --RebalanceService run --
@Override
public void run() {
log.info(this.getServiceName() + " service started"); while (!this.isStoped()) {
this.waitForRunning(WaitInterval);// WaitInterval = 10s
this.mqClientFactory.doRebalance();// MQClientInstance.doRebalance()
} log.info(this.getServiceName() + " service end");
} //-- MQClientInstance.doRebalance() --
public void doRebalance() {
for (String group : this.consumerTable.keySet()) {
MQConsumerInner impl = this.consumerTable.get(group);
if (impl != null) {
try {
impl.doRebalance();// DefaultMQPushConsumerImpl.doRebalance()
} catch (Exception e) {
log.error("doRebalance exception", e);
}
}
}
} // -- DefaultMQPushConsumerImpl.doRebalance --
@Override
public void doRebalance() {
if (this.rebalanceImpl != null) {
this.rebalanceImpl.doRebalance();
}
}
// ......
此处RebalanceImpl的实现是RebalancePushImpl
根据上述线程名可以发现是RebalanceService这个类拉起了上面的RebalanceImpl.updateProcessQueueTableInRebalance
谁拉起了RebalanceService
看下面调用堆栈一目了然
Thread [main] (Suspended (breakpoint at line 185 in com.alibaba.rocketmq.client.impl.factory.MQClientInstance))
owns: com.alibaba.rocketmq.client.impl.factory.MQClientInstance (id=40)
com.alibaba.rocketmq.client.impl.factory.MQClientInstance.start() line: 185
com.alibaba.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl.start() line: 720
com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer.start() line: 365
org.simonme.rocketmq.demo.ConsumerDemo.main(java.lang.String[]) line: 55 // DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("testgroup");
因为我们针对该group设置的consumer就是Push的,即DefaultMQPushConsumer。官方提供的example工程中com.alibaba.rocketmq.example.quickstart.Consumer也是用的push。
引发问题:
1. 应该是可以针对不同的group设置不同形式的(pull or push)的consumer? 2. 不同的client实例订阅同一个group的同一个tag/不同tag会出现怎样的情况?是否还可以设置不同的获取消息方式(push or pull)
那么疑问又来了
如果此处我们设置的不是DefaultMQPushConsumer,而是DefaultMQPullConsumer,那么刚本节(第5节)开头那个问题怎么解决
尝试一下
先看Pull形式的Consumer的例子
在PullMessageService.executePullRequestImmediately(PullRequest)方法处加上断点并调试该demo,就会发现该方法根本不会被调用。因为dispatchPullRequest方法中针对pull模式的实现为空方法体,根本没有发起PullMessageService.executePullRequestImmediately调用。那么上面的疑问就解决了。
6. 分析PullMessageProcessor(MQ-broker端)
入口方法是processRequest
先做一系列的检查,然后获取消息,检查涉及的要点如下图:
获取消息分析
1. 根据offset查询到SelectMapedBufferResult实例。
final GetMessageResult getMessageResult =
this.brokerController.getMessageStore().getMessage(requestHeader.getConsumerGroup(),
requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getQueueOffset(),
requestHeader.getMaxMsgNums(), subscriptionData);
这个GetMessageResult中有SelectMapedBufferResult的List实例。
这个地方体现了零拷贝。消息落地磁盘的时候,是从file map出来的内存buffer,此时消费消息的时候无需再读取文件,而是直接读取map出来的buffer!当然如果堆积消息过多,内存中已经放不下的时候,就需要从磁盘上读取了。 这是rocketmq性能比较好的原因之一。 通常说的零拷贝是指系统态无需拷贝到用户态,即针对大文件拷贝常用的sendfile操作。此处不同于这个概念。
根据GetMessageResult取消息,没太多复杂的,如果发现有消息则走如下分支
switch (getMessageResult.getStatus()) {
case FOUND:
response.setCode(ResponseCode.SUCCESS); // 消息轨迹:记录客户端拉取的消息记录(不表示消费成功)
if (this.hasConsumeMessageHook()) {
// 执行hook
ConsumeMessageContext context = new ConsumeMessageContext();
context.setConsumerGroup(requestHeader.getConsumerGroup());
context.setTopic(requestHeader.getTopic());
rocketmq源码分析3-consumer消息获取的更多相关文章
- rocketmq源码分析2-broker的消息接收
broker消息接收,假设接收的是一个普通消息(即没有事务),此处分析也只分析master上动作逻辑,不涉及ha. 1. 如何找到消息接收处理入口 可以通过broker的监听端口10911顺藤摸瓜式的 ...
- rocketmq源码分析4-事务消息实现原理
为什么消息要具备事务能力 参见还是比较清晰的.简单的说 就是在你业务逻辑过程中,需要发送一条消息给订阅消息的人,但是期望是 此逻辑过程完全成功完成之后才能使订阅者收到消息.业务逻辑过程 假设是这样的: ...
- RocketMQ源码分析之从官方示例窥探:RocketMQ事务消息实现基本思想
摘要: RocketMQ源码分析之从官方示例窥探RocketMQ事务消息实现基本思想. 在阅读本文前,若您对RocketMQ技术感兴趣,请加入RocketMQ技术交流群 RocketMQ4.3.0版本 ...
- RocketMQ 源码分析 —— Message 发送与接收
1.概述 Producer 发送消息.主要是同步发送消息源码,涉及到 异步/Oneway发送消息,事务消息会跳过. Broker 接收消息.(存储消息在<RocketMQ 源码分析 —— Mes ...
- 【RocketMQ源码分析】深入消息存储(2)
前文回顾 CommitLog篇 --[RocketMQ源码分析]深入消息存储(1) MappedFile篇 --[RocketMQ源码分析]深入消息存储(3) 前文说完了一条消息如何被持久化到本地磁盘 ...
- 【RocketMQ源码分析】深入消息存储(3)
前文回顾 CommitLog篇 --[RocketMQ源码分析]深入消息存储(1) ConsumeQueue篇 --[RocketMQ源码分析]深入消息存储(2) 前面两篇已经说过了消息如何存储到Co ...
- RocketMQ源码详解 | Consumer篇 · 其一:消息的 Pull 和 Push
概述 当消息被存储后,消费者就会将其消费. 这句话简要的概述了一条消息的最总去向,也引出了本文将讨论的问题: 消息什么时候才对被消费者可见? 是在 page cache 中吗?还是在落盘后?还是像 K ...
- RocketMQ源码分析之RocketMQ事务消息实现原理上篇(二阶段提交)
在阅读本文前,若您对RocketMQ技术感兴趣,请加入 RocketMQ技术交流群 根据上文的描述,发送事务消息的入口为: TransactionMQProducer#sendMessageInTra ...
- 【RocketMQ源码分析】深入消息存储(1)
最近在学习RocketMQ相关的东西,在学习之余沉淀几篇笔记. RocketMQ有很多值得关注的设计点,消息发送.消息消费.路由中心NameServer.消息过滤.消息存储.主从同步.事务消息等等. ...
- Spring AMQP 源码分析 06 - 手动消息确认
### 准备 ## 目标 了解 Spring AMQP 如何手动确认消息已成功消费 ## 前置知识 <Spring AMQP 源码分析 04 - MessageListener> ## 相 ...
随机推荐
- Bootstrap设置按钮禁用
在Bootstrap中,按钮可以使用button标签或者a标签.设置按钮禁用可以通过两种方式,一种是通用CSS样式,一种是用过JS脚本动态设置,下面举例说明! <!DOCTYPE html> ...
- .Net平台互操作技术:02. 技术介绍
上一篇文章简单介绍了.Net平台互操作技术的面临的主要问题,以及主要的解决方案.本文将重点介绍使用相对较多的P/Invoke技术的实现:C#通过P/Invoke调用Native C++ Dll技术.C ...
- 本号讯 | 微软和百度携手推进全球自动驾驶技术; 微软发布新一代可垂直可水平滚动的Arc鼠标
7 月 13 日,微软宣布了与宝马的最新合作进展,继语音助手 Cortana .云服务 Azure.Office 365 和微软 Exchange 安装在部分宝马车型后——Skype for Busi ...
- C# 使用解析json 嵌套方法
C#从网页不传参数 接收json数据 public String GetHtmlFromUrl(String url) { //Response.Write(url); //Response.End( ...
- 洛谷 P2383 狗哥玩木棒
题目背景 狗哥又趁着语文课干些无聊的事了... 题目描述 现给出一些木棒长度,那么狗哥能否用给出的木棒(木棒全用完)组成一个正方形呢? 输入输出格式 输入格式: 输入文件中的第一行是一个整数n表示测试 ...
- 如何处理错误消息Please install the gcc make perl packages
如何处理这行错误消息? Please install the gcc make perl packages from your distribution. 执行命令行:yum install gcc ...
- Linux中根据访问日志统计访问量最高的前N个IP
前段时间面试中被问到如上问题,日常不怎么注意积累,以此谨记. 访问IP 页面[nxuser@im440-zh test]$ vi log 135.252.172.181 page1 136.252.1 ...
- MySQL安装未响应解决方法
安装MySQL出示未响应,一般显示在安装MySQL程序最后2步的3,4项就不动了. 这种情况一般是你以前安装过MySQL数据库服务项被占用了. 1.卸载MySQL 2.删除安装目录及数据存放目录 3. ...
- [学习总结] python语言学习总结 (三)
函数闭包 定义 延伸了作用域的函数(能访问定义体之外定义的非全局变量 作用 共享变量的时候避免使用了不安全的全局变量 允许将函数与某些数据关联起来,类似于简化版面向对象编程 相同代码每次生成的闭包,其 ...
- Web前端 优化方案
1.减少Http请求 在一个页面中图片,CSS,JS可能N个,如果每个资源都去请求一次服务器的话,那么服务器就会为每个资源开一个线程来完成,这样的话对服务器的压力就很大了.所以解决的方法就是合并资源 ...