广播模式下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

offsetTable由来

posted @ 2022-09-27 17:20  smile008  阅读(2279)  评论(0编辑  收藏  举报