源码分析RocketMQ消息轨迹,【秋招面试专题解析】

|| !context.getSendResult().isTraceOn()) {

// if switch is false,skip it

return;

}

TraceContext tuxeContext = (TraceContext) context.getMqTraceContext();

TraceBean traceBean = tuxeContext.getTraceBeans().get(0); // @2

int costTime = (int) ((System.currentTimeMillis() - tuxeContext.getTimeStamp()) / tuxeContext.getTraceBeans().size()); // @3

tuxeContext.setCostTime(costTime); // @4

if (context.getSendResult().getSendStatus().equals(SendStatus.SEND_OK)) {

tuxeContext.setSuccess(true);

} else {

tuxeContext.setSuccess(false);

}

tuxeContext.setRegionId(context.getSendResult().getRegionId());

traceBean.setMsgId(context.getSendResult().getMsgId());

traceBean.setOffsetMsgId(context.getSendResult().getOffsetMsgId());

traceBean.setStoreTime(tuxeContext.getTimeStamp() + costTime / 2);

localDispatcher.append(tuxeContext); // @5

}

代码@1:如果topic主题为消息轨迹的Topic,直接返回。

代码@2:从MqTraceContext中获取跟踪的TraceBean,虽然设计成List结构体,但在消息发送场景,这里的数据永远只有一条,及时是批量发送也不例外。

代码@3:获取消息发送到收到响应结果的耗时。

代码@4:设置costTime(耗时)、success(是否发送成功)、regionId(发送到broker所在的分区)、msgId(消息ID,全局唯一)、offsetMsgId(消息物理偏移量,如果是批量消息,则是最后一条消息的物理偏移量)、storeTime,这里使用的是(客户端发送时间 + 二分之一的耗时)来表示消息的存储时间,这里是一个估值。

代码@5:将需要跟踪的信息通过TraceDispatcher转发到Broker服务器。其代码如下:

public boolean append(final Object ctx) {

boolean result = traceContextQueue.offer((TraceContext) ctx);

if (!result) {

log.info(“buffer full” + discardCount.incrementAndGet() + " ,context is " + ctx);

}

return result;

}

这里一个非常关键的点是offer方法的使用,当队列无法容纳新的元素时会立即返回false,并不会阻塞。

接下来将目光转向TraceDispatcher的实现。

1.3 TraceDispatcher实现原理

TraceDispatcher,用于客户端消息轨迹数据转发到Broker,其默认实现类:AsyncTraceDispatcher。

1.3.1 TraceDispatcher构造函数

public AsyncTraceDispatcher(String traceTopicName, RPCHook rpcHook) throws MQClientException {

// queueSize is greater than or equal to the n power of 2 of value

this.queueSize = 2048;

this.batchSize = 100;

this.maxMsgSize = 128000;

this.discardCount = new AtomicLong(0L);

this.traceContextQueue = new ArrayBlockingQueue(1024);

this.appenderQueue = new ArrayBlockingQueue(queueSize);

if (!UtilAll.isBlank(traceTopicName)) {

this.traceTopicName = traceTopicName;

} else {

this.traceTopicName = MixAll.RMQ_SYS_TRACE_TOPIC;

} // @1

this.traceExecuter = new ThreadPoolExecutor(// :

10, //

20, //

1000 * 60, //

TimeUnit.MILLISECONDS, //

this.appenderQueue, //

new ThreadFactoryImpl(“MQTraceSendThread_”));

traceProducer = getAndCreateTraceProducer(rpcHook); // @2

}

代码@1:初始化核心属性,该版本这些值都是“固化”的,用户无法修改。

  • queueSize

队列长度,默认为2048,异步线程池能够积压的消息轨迹数量。

  • batchSize

一次向Broker批量发送的消息条数,默认为100.

  • maxMsgSize

向Broker汇报消息轨迹时,消息体的总大小不能超过该值,默认为128k。

  • discardCount

整个运行过程中,丢弃的消息轨迹数据,这里要说明一点的是,如果消息TPS发送过大,异步转发线程处理不过来时,会主动丢弃消息轨迹数据。

  • traceContextQueue

traceContext积压队列,客户端(消息发送、消息消费者)在收到处理结果后,将消息轨迹提交到噶队列中,则会立即返回。

  • appenderQueue

提交到Broker线程池中队列。

  • traceTopicName

用于接收消息轨迹的Topic,默认为RMQ_SYS_TRANS_HALF_TOPIC。

  • traceExecuter

用于发送到Broker服务的异步线程池,核心线程数默认为10,最大线程池为20,队列堆积长度2048,线程名称:MQTraceSendThread_。、

  • traceProducer

发送消息轨迹的Producer。

代码@2:调用getAndCreateTraceProducer方法创建用于发送消息轨迹的Producer(消息发送者),下面详细介绍一下其实现。

1.3.2 getAndCreateTraceProducer详解

private DefaultMQProducer getAndCreateTraceProducer(RPCHook rpcHook) {

DefaultMQProducer traceProducerInstance = this.traceProducer;

if (traceProducerInstance == null) { //@1

traceProducerInstance = new DefaultMQProducer(rpcHook);

traceProducerInstance.setProducerGroup(TraceConstants.GROUP_NAME);

traceProducerInstance.setSendMsgTimeout(5000);

traceProducerInstance.setVipChannelEnabled(false);

// The max size of message is 128K

traceProducerInstance.setMaxMessageSize(maxMsgSize - 10 * 1000);

}

return traceProducerInstance;

}

代码@1:如果还未建立发送者,则创建用于发送消息轨迹的消息发送者,其GroupName为:_INNER_TRACE_PRODUCER,消息发送超时时间5s,最大允许发送消息大小118K。

1.3.3 start

public void start(String nameSrvAddr) throws MQClientException {

if (isStarted.compareAndSet(false, true)) { // @1

traceProducer.setNamesrvAddr(nameSrvAddr);

traceProducer.setInstanceName(TRACE_INSTANCE_NAME + “_” + nameSrvAddr);

traceProducer.start();

}

this.worker = new Thread(new AsyncRunnable(), “MQ-AsyncTraceDispatcher-Thread-” + dispatcherId); // @2

this.worker.setDaemon(true);

this.worker.start();

this.registerShutDownHook();

}

开始启动,其调用的时机为启动DefaultMQProducer时,如果启用跟踪消息轨迹,则调用之。

代码@1:如果用于发送消息轨迹的发送者没有启动,则设置nameserver地址,并启动着。

代码@2:启动一个线程,用于执行AsyncRunnable任务,接下来将重点介绍。

1.3.4 AsyncRunnable

class AsyncRunnable implements Runnable {

private boolean stopped;

public void run() {

while (!stopped) {

List contexts = new ArrayList(batchSize); // @1

for (int i = 0; i < batchSize; i++) {

TraceContext context = null;

try {

//get trace data element from blocking Queue — traceContextQueue

context = traceContextQueue.poll(5, TimeUnit.MILLISECONDS); // @2

} catch (InterruptedException e) {

}

if (context != null) {

contexts.add(context);

} else {

break;

}

}

if (contexts.size() > 0) { :

AsyncAppenderRequest request = new AsyncAppenderRequest(contexts); // @3

traceExecuter.submit(request);

} else if (AsyncTraceDispatcher.this.stopped) {

this.stopped = true;

}

}

}

}

代码@1:构建待提交消息跟踪Bean,每次最多发送batchSize,默认为100条。

代码@2:从traceContextQueue中取出一个待提交的TraceContext,设置超时时间为5s,即如何该队列中没有待提交的TraceContext,则最多等待5s。

代码@3:向线程池中提交任务AsyncAppenderRequest。

1.3.5 AsyncAppenderRequest#sendTraceData

public void sendTraceData(List contextList) {

Map<String, List> transBeanMap = new HashMap<String, List>();

for (TraceContext context : contextList) { //@1

if (context.getTraceBeans().isEmpty()) {

continue;

}

// Topic value corresponding to original message entity content

String topic = context.getTraceBeans().get(0).getTopic(); // @2

// Use original message entity’s topic as key

String key = topic;

List transBeanList = transBeanMap.get(key);

if (transBeanList == null) {

transBeanList = new ArrayList();

transBeanMap.put(key, transBeanList);

}

TraceTransferBean traceData = TraceDataEncoder.encoderFromContextBean(context); // @3

transBeanList.add(traceData);

}

for (Map.Entry<String, List> entry : transBeanMap.entrySet()) { // @4

flushData(entry.getValue());

}

}

代码@1:遍历收集的消息轨迹数据。

代码@2:获取存储消息轨迹的Topic。

代码@3:对TraceContext进行编码,这里是消息轨迹的传输数据,稍后对其详细看一下,了解其上传的格式。

代码@4:将编码后的数据发送到Broker服务器。

1.3.6 TraceDataEncoder#encoderFromContextBean

根据消息轨迹跟踪类型,其格式会有一些不一样,下面分别来介绍其合适。

1.3.6.1 PUB(消息发送)

case Pub: {

TraceBean bean = ctx.getTraceBeans().get(0);

//append the content of context and traceBean to transferBean’s TransData

sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.getRegionId()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.getGroupName()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getTopic()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getTags()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getKeys()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getStoreHost()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getBodyLength()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.getCostTime()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getMsgType().ordinal()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getOffsetMsgId()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.isSuccess()).append(TraceConstants.FIELD_SPLITOR);

}

消息轨迹数据的协议使用字符串拼接,字段的分隔符号为1,整个数据以2结尾,感觉这个设计还是有点“不可思议”,为什么不直接使用json协议呢?

1.3.6.2 SubBefore(消息消费之前)

for (TraceBean bean : ctx.getTraceBeans()) {

sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.getRegionId()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.getGroupName()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.getRequestId()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getRetryTimes()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getKeys()).append(TraceConstants.FIELD_SPLITOR);//

}

}

轨迹就是按照上述顺序拼接而成,各个字段使用1分隔,每一条记录使用2结尾。

1.3.2.3 SubAfter(消息消费后)

case SubAfter: {

for (TraceBean bean : ctx.getTraceBeans()) {

sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.getRequestId()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.getCostTime()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.isSuccess()).append(TraceConstants.CONTENT_SPLITOR)//

.append(bean.getKeys()).append(TraceConstants.CONTENT_SPLITOR)//

.append(ctx.getContextCode()).append(TraceConstants.FIELD_SPLITOR);

}

}

}

格式编码一样,就不重复多说。

经过上面的源码跟踪,消息发送端的消息轨迹跟踪流程、消息轨迹数据编码协议就清晰了,接下来我们使用一张序列图来结束本部分的讲解。

在这里插入图片描述

其实行文至此,只关注了消息发送的消息轨迹跟踪,消息消费的轨迹跟踪又是如何呢?其实现原理其实是一样的,就是在消息消费前后执行特定的钩子函数,其实现类为ConsumeMessageTraceHookImpl,由于其实现与消息发送的思路类似,故就不详细介绍了。

2、 消息轨迹数据如何存储


其实从上面的分析,我们已经得知,RocketMQ的消息轨迹数据存储在到Broker上,那消息轨迹的主题名如何指定?其路由信息又怎么分配才好呢?是每台Broker上都创建还是只在其中某台上创建呢?RocketMQ支持系统默认与自定义消息轨迹的主题。

2.1 使用系统默认的主题名称

RocketMQ默认的消息轨迹主题为:RMQ_SYS_TRACE_TOPIC,那该Topic需要手工创建吗?其路由信息呢?

{

if (this.brokerController.getBrokerConfig().isTraceTopicEnable()) { // @1

String topic = this.brokerController.getBrokerConfig().getMsgTraceTopicName();

TopicConfig topicConfig = new TopicConfig(topic);

this.systemTopicList.add(topic);

topicConfig.setReadQueueNums(1); // @2

topicConfig.setWriteQueueNums(1);

this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig);

}

}

上述代码出自TopicConfigManager的构造函数,在Broker启动的时候会创建topicConfigManager对象,用来管理topic的路由信息。

代码@1:如果Broker开启了消息轨迹跟踪(traceTopicEnable=true)时,会自动创建默认消息轨迹的topic路由信息,注意其读写队列数为1。

2.2 用户自定义消息轨迹主题

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后总结

搞定算法,面试字节再不怕,有需要文章中分享的这些二叉树、链表、字符串、栈和队列等等各大面试高频知识点及解析

最后再分享一份终极手撕架构的大礼包(学习笔记):分布式+微服务+开源框架+性能优化

image

,同时减轻大家的负担。**
[外链图片转存中…(img-XK9uSDFH-1712036785430)]
[外链图片转存中…(img-Yas60wr9-1712036785431)]
[外链图片转存中…(img-hfF1c7SY-1712036785432)]
[外链图片转存中…(img-pyNZppqN-1712036785432)]
[外链图片转存中…(img-qDurVnOc-1712036785433)]
[外链图片转存中…(img-BhNPs3X5-1712036785434)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-g0jLzUEj-1712036785434)]

最后总结

搞定算法,面试字节再不怕,有需要文章中分享的这些二叉树、链表、字符串、栈和队列等等各大面试高频知识点及解析

最后再分享一份终极手撕架构的大礼包(学习笔记):分布式+微服务+开源框架+性能优化

[外链图片转存中…(img-68yC1EBk-1712036785435)]

  • 30
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值