广播模式下TrackType为NOT_ONLINE
好了梳理完成 来个事后总结吧!
1、在控制台看到的TrackType 为NOT_ONLINE 在不出意外的情况下 是已经消费掉了
2、原因是因为广播模式下 offsetTable 存储在消费者本地,而集群模式下 存储在broker服务器上。
导致console在获取broker中消费情况时 mq 无法获取到offsetTabel
3、为什么这样区分? 个人觉得是因为
3.1集群模式下 大家的消费 是需要负载的,每个消费组 只允许一个机器消费,一旦消费完成后 ,我们集中存储消费进度防止重复消费
3.2广播模式下 任何消费者都需要去消费每条消息,broker 可能是为了 节省某些方面,这些消费的点位 就由消费者自身存储了
问题阐述以及解决思路
在广播模式下 我们看到消息的TarckType 为 NOT_ONLINE,这是什么意思呢?
按照翻译来理解 是 当前消息的结果 未在线
首先我们应该知道有哪几种类型:(这个类型百度即可)
public enum TrackType {
CONSUMED, //消息被消费
CONSUMED_BUT_FILTERED, //已使用但是被过滤了,基本都是tag 被过滤
PULL, // 这个我没见到过 于本文无关 先过掉
NOT_CONSUME_YET, //没有被消费
NOT_ONLINE, //消费不在线,这就是我们下文需要去研究的类型
UNKNOWN,//报错了,可以看日志,一般报错内容会紧跟其后,具体很容易排查出来
}
解决思路很笨(但是理解的也最深,我用了一天的时间),下载源码 从头跟到尾
代码如何跑起来?看另一篇文章吧,当前文章 只做问题解读
运行rocketMQ代码
问题入口确认
现在,我们按照1、NameSver 2、Broker 3、console 控制台 启动项目
既然出现了 NOT_ONLINE类型 ,我们直接F12 查看这个数据是怎么出来的呗!
我这边的请求地址 如下:
http://127.0.0.1:8080/message/viewMessage.query?msgId=C0A8323F2B8A18B4AAC265AC73CA0000&topic=test_03
返回数据结构(根据返回结构 可以确定当前接口的 trackType 就是本次需要查找的主要方向 )
{
"status": 0,
"data": {
"messageTrackList": [
{
"consumerGroup": "consumer_01",
"trackType": "NOT_ONLINE",
"exceptionDesc": "CODE:206 DESC:Not found the consumer group consume stats, because return offset table is empty, maybe the consumer not consume any message"
}
],
"messageView": { ... }
},
"errMsg": null
}
请求地址【message/viewMessage.query】(搜索的关键字[/viewMessage.query] )
好了,我们找到了【MessageController】类中第一个请求,好戏准备
代码跟进 (流程比较多,捡注释看)
直接跟进代码
Pair<MessageView, List<MessageTrack>> messageViewListPair = messageService.viewMessage(topic, msgId);
进入
【MessageServiceImpl.java】
public Pair<MessageView, List<MessageTrack>> viewMessage(String subject, final String msgId) {
try {
MessageExt messageExt = mqAdminExt.viewMessage(subject, msgId);
//数据是这里返回的
List<MessageTrack> messageTrackList = messageTrackDetail(messageExt);
return new Pair<>(MessageView.fromMessageExt(messageExt), messageTrackList);
} catch (Exception e) {
throw Throwables.propagate(e);
}
继续跟进
【MessageServiceImpl.java】
@Override
public List<MessageTrack> messageTrackDetail(MessageExt msg) {
try {
//当前mqAdminExt 是一个interface,查看实现类有三个,但是咱们当前项目 只有一个,可以确认方法的实现在此项目中
return mqAdminExt.messageTrackDetail(msg);
}catch (Exception e) {
logger.error("op=messageTrackDetailError", e);
return Collections.emptyList();
}
}
继续跟进
【MQAdminExtImpl.java】
@Override
public List<MessageTrack> messageTrackDetail(MessageExt msg)
throws RemotingException, MQClientException, InterruptedException, MQBrokerException {
//当前 threadLocalMQAdminExt()方法,获取 DefaultMQAdminExt 扩展类
//即代表 messageTrackDetail实现 在 扩展类中 (扩展类是RocketMQ tool的源码)
return MQAdminInstance.threadLocalMQAdminExt().messageTrackDetail(msg);
}
继续跟进
【DefaultMQAdminExt.java】
public List<MessageTrack> messageTrackDetail(MessageExt msg) throws RemotingException, MQClientException, InterruptedException, MQBrokerException {
return this.defaultMQAdminExtImpl.messageTrackDetail(msg);
}
console代码重点
继续跟进
【DefaultMQAdminExtImpl.java】
当前代码 需要把注释仔细看看
里面包含了 TrackType 四个类型的由来
初始化 UNKNOWN 、 获取消息是否被消费 CONSUMED、 NOT_CONSUME_YET / 消费成功后 但被过滤 CONSUMED_BUT_FILTERED
获取消息是否被消费发生 MQClientException 时NOT_ONLINE
/**
* 当前代码 需要把注释仔细看看
* 里面包含了 TrackType 四个类型的由来
* 初始化 UNKNOWN 、 获取消息是否被消费 CONSUMED、 NOT_CONSUME_YET / 消费成功后 但被过滤 CONSUMED_BUT_FILTERED
* 获取消息是否被消费发生 MQClientException 时NOT_ONLINE
* @param msg
* @return
* @throws RemotingException
* @throws MQClientException
* @throws InterruptedException
* @throws MQBrokerException
*/
@Override
public List<MessageTrack> messageTrackDetail( MessageExt msg) throws RemotingException, MQClientException, InterruptedException, MQBrokerException {
List<MessageTrack> result = new ArrayList<MessageTrack>();
//获取消费者groupName列表
GroupList groupList = this.queryTopicConsumeByWho(msg.getTopic());
for (String group : groupList.getGroupList()) {
//默认trackType 类型 UNKNOWN
MessageTrack mt = new MessageTrack();
mt.setConsumerGroup(group);
mt.setTrackType(TrackType.UNKNOWN);
ConsumerConnection cc = null;
try {
//获取当前消费者消费类型
// [主动消费 pull CONSUME_ACTIVELY]
// [被动消费 push CONSUME_PASSIVELY】
//这里我们用的是被动消费类型
cc = this.examineConsumerConnectionInfo(group);
} catch (MQBrokerException e) {
if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) {
mt.setTrackType(TrackType.NOT_ONLINE);
}
mt.setExceptionDesc("CODE:" + e.getResponseCode() + " DESC:" + e.getErrorMessage());
result.add(mt);
continue; } catch (Exception e) {
mt.setExceptionDesc(RemotingHelper.exceptionSimpleDesc(e));
result.add(mt);
continue; }
switch (cc.getConsumeType()) {
case CONSUME_ACTIVELY:
mt.setTrackType(TrackType.PULL);
break; case CONSUME_PASSIVELY:
//被动消费
boolean ifConsumed = false;
try {
//注意⚠️:console代码的主要点位在此
//返回的参数:是否消息被消费 下一步跟进这里
ifConsumed = this.consumed(msg, group);
} catch (MQClientException e) {
if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) {
mt.setTrackType(TrackType.NOT_ONLINE);
}
mt.setExceptionDesc("CODE:" + e.getResponseCode() + " DESC:" + e.getErrorMessage());
result.add(mt);
continue; } catch (MQBrokerException e) {
if (ResponseCode.CONSUMER_NOT_ONLINE == e.getResponseCode()) {
mt.setTrackType(TrackType.NOT_ONLINE);
}
mt.setExceptionDesc("CODE:" + e.getResponseCode() + " DESC:" + e.getErrorMessage());
result.add(mt);
continue; } catch (Exception e) {
mt.setExceptionDesc(RemotingHelper.exceptionSimpleDesc(e));
result.add(mt);
continue; }
//如果是被消费 trackType = CONSUMED
if (ifConsumed) {
mt.setTrackType(TrackType.CONSUMED);
Iterator<Entry<String, SubscriptionData>> it = cc.getSubscriptionTable().entrySet().iterator();
while (it.hasNext()) {
Entry<String, SubscriptionData> next = it.next();
if (next.getKey().equals(msg.getTopic())) {
if (next.getValue().getTagsSet().contains(msg.getTags())
|| next.getValue().getTagsSet().contains("\*")
|| next.getValue().getTagsSet().isEmpty()) {
} else {
//消费成功后 但被过滤
mt.setTrackType(TrackType.CONSUMED_BUT_FILTERED);
}
}
}
} else {
//如果未消费 trackType = NOT_CONSUME_YET
mt.setTrackType(TrackType.NOT_CONSUME_YET);
}
break;
default:
break;
}
result.add(mt);
}
return result;
}
跟进当前类方法[获取消息 是否被当前消费者消费成功]
public boolean consumed(final MessageExt msg,
final String group) throws RemotingException, MQClientException, InterruptedException,
MQBrokerException {
//检查消费统计 ,因为MQClientException 是当前方法里面抛出来的,所以导致 NOT_ONLINE
ConsumeStats cstats = this.examineConsumeStats(group);
....
return false;
}
跟进当前类方法[检查消费统计]
@Override
public ConsumeStats examineConsumeStats(
String consumerGroup)
throws RemotingException, MQClientException,InterruptedException, MQBrokerException {
//跟进
return examineConsumeStats(consumerGroup, null);
}
继续跟进
public ConsumeStats examineConsumeStats(String consumerGroup,
String topic) throws RemotingException, MQClientException,
InterruptedException, MQBrokerException {
//获取重试主题 retryTopic = "%RETRY%" + consumerGroup
String retryTopic = MixAll.getRetryTopic(consumerGroup);
//当前方法继续跟进 到了其他源码包中,流程就不贴出来了
//步骤说一下,通过neety 调用了nameServer 获取到了 broker 的通信地址 10911
//BrokerData [brokerName=broker-a, brokerAddrs={0=127.0.0.1:10911}]
TopicRouteData topicRouteData = this.examineTopicRouteInfo(retryTopic);
ConsumeStats result = new ConsumeStats();
for (BrokerData bd : topicRouteData.getBrokerDatas()) {
String addr = bd.selectBrokerAddr();
if (addr != null) {
//获取消费统计 其实主要是获取 offsetTable ,这里代码 下面继续跟进
ConsumeStats consumeStats = this.mqClientInstance.getMQClientAPIImpl().getConsumeStats(addr, consumerGroup, topic, timeoutMillis * 3);
result.getOffsetTable().putAll(consumeStats.getOffsetTable());
double value = result.getConsumeTps() + consumeStats.getConsumeTps();
result.setConsumeTps(value);
}
}
//注意⚠️:如果offsetTable 列表是空的,此时就会抛出 MQClientException,导致console 中显示 消费者的消费trackType 是 NOT_ONLINE
if (result.getOffsetTable().isEmpty()) {
throw new MQClientException(ResponseCode.CONSUMER_NOT_ONLINE,
"Not found the consumer group consume stats, because return offset table is empty, maybe the consumer not consume any message");
}
return result;
}
[========]
其实上方代码流程就足以说明 为什么console 显示消息trackType 为NOT_ONLINE的来源
就是因为在获取消费者的offsetTable 时,未获取到,因为广播模式下 offset Table是存储在消费者本地的,broker 无法获取到
但是我们知其然要知其所以然 ,所以我们继续跟进代码 ,看看offsetTable 到底是什么,从那里来的
当前文章东西太多 ,我们下个文章介绍 如何获取offsetTable