问题
对于消息而言,必须记录当前消息处理的轨迹,日常开发中,主要使用唯一ID,再不同业务,以及不同中间件中进行传递,然后通过日志的方式全部串联起来,那么MQ也肯定存在这样的消息轨迹机制,便于监控消息的运行情况等等。
那么消息的轨迹肯定需要解决几个问题
- 消息轨迹模型什么样子
- 消息轨迹什么时候记录
- 消息轨迹信息记录在哪里
模型
-
TraceContext:链路信息的上下文节点,每个执行,都需要一个上下文环境,里面包含了,当前节点的一些信息
- 包括region了,发送端还是消费端,状态情况
public class TraceContext implements Comparable<TraceContext> { //跟踪类型,可选值:Pub(消息发送)、SubBefore(消息拉取到客户端,执行业务定义的消费逻辑之前)、SubAfter(消费后)。 private TraceType traceType; //当前时间戳。 private long timeStamp = System.currentTimeMillis(); //broker所在的区域ID,取自BrokerConfig#regionId private String regionId = ""; private String regionName = ""; //组名称,traceType为Pub时为生产者组的名称;如果traceType为subBefore或subAfter时为消费组名称。 private String groupName = ""; //耗时 private int costTime = 0; //是发送成功。 private boolean isSuccess = true; //raceType为subBefore、subAfter时使用,消费端的请求Id private String requestId = MessageClientIDSetter.createUniqID(); //消费状态码,可选值:SUCCESS,TIME_OUT,EXCEPTION,RETURNNULL,FAILED。 private int contextCode = 0; //具体的轨迹信息 private List<TraceBean> traceBeans; } -
TraceBean:包含消息的一些具体信息,注意是LIst,那么必然是有序的,会不会每个节点,好比生产端,消费端,都会加入一个节点信息进去呢,这样整条链路就很清楚了,这和平时状态变化有点区别,直接一个状态不断去更新
public class TraceBean { private static final String LOCAL_ADDRESS = UtilAll.ipToIPv4Str(UtilAll.getIP()); //主题 private String topic = ""; //消息ID private String msgId = ""; //消息偏移量ID,该ID中包含了broker的ip以及偏移量。 private String offsetMsgId = ""; //消息tag。 private String tags = ""; //消息索引key,根据该key可快速检索消息。 private String keys = ""; //跟踪类型为PUB时为存储该消息的Broker服务器IP;跟踪类型为subBefore、subAfter时为消费者IP。 private String storeHost = LOCAL_ADDRESS; private String clientHost = LOCAL_ADDRESS; private long storeTime; //重试次数 private int retryTimes; //消息长度 private int bodyLength; //消息类型 private MessageType msgType; //------------事务的一些特性-------------- //本地事务状态 private LocalTransactionState transactionState; //事务ID private String transactionId; private boolean fromTransactionCheck; }
记录
不知道是否还记得,之前再发送和消费前后,都提供了钩子函数,那么很显然这些钩子函数可以进行消息轨迹的注入
public interface SendMessageHook {
String hookName();
//消息发送前
void sendMessageBefore(final SendMessageContext context);
//消息发送后
void sendMessageAfter(final SendMessageContext context);
}
public interface ConsumeMessageHook {
String hookName();
//消费消息前
void consumeMessageBefore(final ConsumeMessageContext context);
//消费消息后
void consumeMessageAfter(final ConsumeMessageContext context);
}
生产端
生产者,需不需要去记录信息呢,肯定是交给producer来决定的,所以直接看,DefaultMQProducer的构造方法。发现巨多,看两个和消息轨迹有关系的构造方法
DefaultMQProducer
-
其中重要的参数含义
- producerGroup:见名知意,生产者组
- enableMsgTrace:是否开启跟踪消息轨迹
- customizedTraceTopic:如果开启消息轨迹跟踪,用来存储消息轨迹数据所属的主题名称,如果没有写,默认为:RMQ_SYS_TRACE_TOPIC
public DefaultMQProducer(final String producerGroup, boolean enableMsgTrace) {
this(null, producerGroup, null, enableMsgTrace, null);
}
public DefaultMQProducer(final String producerGroup, boolean enableMsgTrace, final String customizedTraceTopic) {
this(null, producerGroup, null, enableMsgTrace, customizedTraceTopic);
}
-
底层构造方法
-
重要角色:AsyncTraceDispatcher,和生产者是一对一的关系,而且是相互依赖
-
重要过程:注册了两类钩子方法
- SendMessageTraceHookImpl:里面包含了上面的分配器
- EndTransactionTraceHookImpl:里面包含了上面的分配器
public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook, boolean enableMsgTrace, final String customizedTraceTopic) { this.namespace = namespace; this.producerGroup = producerGroup; defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook); //if client open the message trace feature //K1 开启日志跟踪 if (enableMsgTrace) { try { //IMP 异步的轨迹分发器 AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(producerGroup, TraceDispatcher.Type.PRODUCE, customizedTraceTopic, rpcHook); //IMP 分发器和生产者 一对一 dispatcher.setHostProducer(this.defaultMQProducerImpl); traceDispatcher = dispatcher; //K1 构建SendMessageTraceHookImpl对象,并使用AsyncTraceDispatcher用来异步转发 this.defaultMQProducerImpl.registerSendMessageHook( new SendMessageTraceHookImpl(traceDispatcher)); this.defaultMQProducerImpl.registerEndTransactionHook( new EndTransactionTraceHookImpl(traceDispatcher)); } catch (Throwable e) { log.error("system mqtrace hook init failed ,maybe can't send msg trace data"); } } } -
-
类图结构
SendMessageTraceHookImpl
-
构造方法
public class SendMessageTraceHookImpl implements SendMessageHook { //分配器 private TraceDispatcher localDispatcher; public SendMessageTraceHookImpl(TraceDispatcher localDispatcher) { this.localDispatcher = localDispatcher; } //钩子方法,下面介绍 }
sendMessageBefore
-
发送消息之前,记录轨迹信息,此处没有将轨迹信息加入到分配器中,只是构建了轨迹上下文,轨迹节点信息,此处的TraceType为 PUB
@Override public void sendMessageBefore(SendMessageContext context) { //if it is message trace data,then it doesn't recorded //K1 如果topic主题为消息轨迹的Topic,直接返回 if (context == null || context.getMessage().getTopic().startsWith(((AsyncTraceDispatcher) localDispatcher).getTraceTopicName())) { return; } //build the context content of TuxeTraceContext //K1 在消息发送上下文中,设置用来跟踪消息轨迹的上下环境,里面主要包含一个TraceBean集合、追踪类型(TraceType.Pub)与生产者所属的组。 TraceContext tuxeContext = new TraceContext(); tuxeContext.setTraceBeans(new ArrayList<TraceBean>(1)); context.setMqTraceContext(tuxeContext); tuxeContext.setTraceType(TraceType.Pub);//发布者 tuxeContext.setGroupName(NamespaceUtil.withoutNamespace(context.getProducerGroup())); //build the data bean object of message trace //K1 构建一条跟踪消息,用TraceBean来表示,记录原消息的topic、tags、keys、发送到broker地址、消息体长度等消息 TraceBean traceBean = new TraceBean(); traceBean.setTopic(NamespaceUtil.withoutNamespace(context.getMessage().getTopic())); traceBean.setTags(context.getMessage().getTags()); traceBean.setKeys(context.getMessage().getKeys()); traceBean.setStoreHost(context.getBrokerAddr()); traceBean.setBodyLength(context.getMessage().getBody().length); traceBean.setMsgType(context.getMsgType()); tuxeContext.getTraceBeans().add(traceBean); }
sendMessageEnd
-
主要的过程
- 如果topic主题为消息轨迹的Topic,直接返回。
- 从MqTraceContext中获取跟踪的TraceBean,虽然设计成List结构体,但在消息发送场景,这里的数据永远只有一条,及时是批量发送也不例外。
- 获取消息发送到收到响应结果的耗时。
- 设置costTime(耗时)、success(是否发送成功)、regionId(发送到broker所在的分区)、msgId(消息ID,全局唯一)、offsetMsgId(消息物理偏移量,如果是批量消息,则是最后一条消息的物理偏移量)、storeTime,这里使用的是(客户端发送时间 + 二分之一的耗时)来表示消息的存储时间,这里是一个估值。
- 将需要跟踪的信息通过TraceDispatcher转发到Broker服务器
@Override public void sendMessageAfter(SendMessageContext context) { //if it is message trace data,then it doesn't recorded //K1 如果topic主题为消息轨迹的Topic,直接返回 if (context == null || context.getMessage().getTopic().startsWith(((AsyncTraceDispatcher) localDispatcher).getTraceTopicName()) || context.getMqTraceContext() == null) { return; } //K1 存在响应结果,才执行下面的逻辑 if (context.getSendResult() == null) { return; } if (context.getSendResult().getRegionId() == null || !context.getSendResult().isTraceOn()) { // if switch is false,skip it return; } TraceContext tuxeContext = (TraceContext) context.getMqTraceContext(); //K1 从MqTraceContext中获取跟踪的TraceBean,虽然设计成List结构体,但在消息发送场景,这里的数据永远只有一条,及时是批量发送也不例外 TraceBean traceBean = tuxeContext.getTraceBeans().get(0); //K1 获取消息发送到收到响应结果的耗时。 int costTime = (int) ((System.currentTimeMillis() - tuxeContext.getTimeStamp()) / tuxeContext.getTraceBeans().size()); //IMP 设置costTime(耗时)、success(是否发送成功)、regionId(发送到broker所在的分区)、msgId(消息ID,全局唯一)、offsetMsgId(消息物理偏移量, // 如果是批量消息,则是最后一条消息的物理偏移量)、storeTime,这里使用的是(客户端发送时间 + 二分之一的耗时)来表示消息的存储时间,这里是一个估值 tuxeContext.setCostTime(costTime); 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); //K1 将需要跟踪的信息通过TraceDispatcher转发到Broker服务器 localDispatcher.append(tuxeContext); }- 此处存在一个问题,轨迹丢失的问题,如果SendResult,好比超时了,那么这个对象就不存在,那么就无法发给分配器,那么就不会将消息轨迹发到Broker中,那是不是就会导致轨迹丢失,或者说,轨迹丢失,可能存在这些原因的时候,就是没有轨迹信息,后面再看看,然后回来分析分析
TraceDispatcher
public interface TraceDispatcher {
enum Type {
PRODUCE,
CONSUME
}
/**
* Initialize asynchronous transfer data module
*/
void start(String nameSrvAddr, AccessChannel accessChannel) throws MQClientException;
/**
* Append the transfering data
* @param ctx data infomation
* @return
*/
boolean append(Object ctx);
/**
* Write flush action
*
* @throws IOException
*/
void flush() throws IOException;
/**
* Close the trace Hook
*/
void shutdown();
}
- 具体的实现类是:AsyncTraceDispatcher
AsyncTraceDispatcher
-
注意其中的重要参数
public AsyncTraceDispatcher(String group, Type type, String traceTopicName, RPCHook rpcHook) { //队列长度,默认为2048,异步线程池能够积压的消息轨迹数量。 this.queueSize = 2048; //一次向Broker批量发送的消息条数,默认为100. this.batchSize = 100; //向Broker汇报消息轨迹时,消息体的总大小不能超过该值,默认为128k。 //消息体的大小 this.maxMsgSize = 128000; //整个运行过程中,丢弃的消息轨迹数据,这里要说明一点的是,如果消息TPS发送过大,异步转发线程处理不过来时,会主动丢弃消息轨迹数据。 this.discardCount = new AtomicLong(0L); //traceContext积压队列,客户端(消息发送、消息消费者)在收到处理结果后,将消息轨迹提交到该队列中,则会立即返回 this.traceContextQueue = new ArrayBlockingQueue<TraceContext>(1024); this.group = group; this.type = type; //提交到Broker线程池中队列 this.appenderQueue = new ArrayBlockingQueue<Runnable>(queueSize); //用于接收消息轨迹的Topic,默认为RMQ_SYS_TRANS_HALF_TOPIC if (!UtilAll.isBlank(traceTopicName)) { this.traceTopicName = traceTopicName; } else { this.traceTopicName = TopicValidator.RMQ_SYS_TRACE_TOPIC; } //IMP 异步线程池 // 核心线程数默认为10,最大线程池为20,队列堆积长度2048,线程名称:MQTraceSendThread_。 this.traceExecutor = new ThreadPoolExecutor(// 10, // 20, // 1000 * 60, // TimeUnit.MILLISECONDS, // this.appenderQueue, // new ThreadFactoryImpl("MQTraceSendThread_")); //IMP 发送消息轨迹的Producer traceProducer = getAndCreateTraceProducer(rpcHook); }
getAndCreateTraceProducer
-
获取或者创建TraceProcuder,主要观察他的主题是什么,如果还未建立发送者,则创建用于发送消息轨迹的消息发送者,其GroupName为:_INNER_TRACE_PRODUCER,消息发送超时时间5s,最大允许发送消息大小118K
private DefaultMQProducer getAndCreateTraceProducer(RPCHook rpcHook) { DefaultMQProducer traceProducerInstance = this.traceProducer; if (traceProducerInstance == null) { traceProducerInstance = new DefaultMQProducer(rpcHook); traceProducerInstance.setProducerGroup(genGroupNameForTrace()); traceProducerInstance.setSendMsgTimeout(5000); traceProducerInstance.setVipChannelEnabled(false); // The max size of message is 128K traceProducerInstance.setMaxMessageSize(maxMsgSize - 10 * 1000); } return traceProducerInstance; }private String genGroupNameForTrace() { // public static final String GROUP_NAME_PREFIX = "_INNER_TRACE_PRODUCER"; return TraceConstants.GROUP_NAME_PREFIX + "-" + this.group + "-" + this.type + "-" + COUNTER.incrementAndGet(); }
- 之前最后是使用了分配器的offer方法,那么看看offer方法执行了什么
offer
- traceContextQueue:此对象存储用于发送给broker的轨迹消息
@Override
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;
}
- 基于内存队列的发布订阅模式,好吧,肯定有地方在不断地拉取此队列中的数据。进行消费。那么此对象肯定是被其他线程执行了,那么创建线程的方式,一个是Thread,一个是线程池。既然是Producer的服务,那么肯定在DefaultMQProducer启动也就是start的时候,这个定时任务也启动了,直接回去查看
-
DefaultMQProducer#start
- traceDispatcher.start,启动了此分配服务,继续查看里面的逻辑
@Override public void start() throws MQClientException { //根据namespace和producerGroup设置生产者组 // 基于命名空间对 ProducerGroup 再次封装, 一般用于在不同的业务场景下做隔离 this.setProducerGroup(withNamespace(this.producerGroup)); //IMP 默认生产者实现启动 this.defaultMQProducerImpl.start(); if (null != traceDispatcher) { try { traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); } catch (MQClientException e) { log.warn("trace dispatcher start failed ", e); } } }public void start(String nameSrvAddr, AccessChannel accessChannel) throws MQClientException { //如果用于发送消息轨迹的发送者没有启动,则设置nameserver地址,并启动着 if (isStarted.compareAndSet(false, true)) { traceProducer.setNamesrvAddr(nameSrvAddr); traceProducer.setInstanceName(TRACE_INSTANCE_NAME + "_" + nameSrvAddr); traceProducer.start(); } this.accessChannel = accessChannel; //启动一个线程,用于执行AsyncRunnable任务 this.worker = new Thread(new AsyncRunnable(), "MQ-AsyncTraceDispatcher-Thread-" + dispatcherId); this.worker.setDaemon(true); this.worker.start(); this.registerShutDownHook(); }
AsyncRunnable
-
直接查看run方法
class AsyncRunnable implements Runnable { private boolean stopped; @Override public void run() { while (!stopped) { //K1 构建待提交消息跟踪Bean,每次最多发送batchSize,默认为100条。 List<TraceContext> contexts = new ArrayList<TraceContext>(batchSize); synchronized (traceContextQueue) { for (int i = 0; i < batchSize; i++) { TraceContext context = null; try { //K1 从traceContextQueue中取出一个待提交的TraceContext,设置超时时间为5s,即如何该队列中没有待提交的TraceContext,则最多等待5s context = traceContextQueue.poll(5, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { } if (context != null) { contexts.add(context); } else { break; } } //K1 向线程池中提交任务AsyncAppenderRequest if (contexts.size() > 0) { AsyncAppenderRequest request = new AsyncAppenderRequest(contexts); traceExecutor.submit(request); } else if (AsyncTraceDispatcher.this.stopped) { this.stopped = true; } } } } }- 总得来说就是,从内存队列里面获取到了,需要提交的轨迹信息,然后呢,构成Request请求,然后讲请求提交给 异步线程池,然后发送给Broker,好吧,各种内存队列进行解耦。接下来看 AsyncAppenderRequest
AsyncAppenderRequest
run
- 遍历消息轨迹请求
- 获取轨迹主题
- 构造 TraceTransferBean 编码对象,这个对象会针对不同的时期进行编码设置
- 发送给Broker
@Override
public void run() {
sendTraceData(contextList);
}
public void sendTraceData(List<TraceContext> contextList) {
//IMP 主题 + regionId -> 轨迹消息
Map<String, List<TraceTransferBean>> transBeanMap = new HashMap<String, List<TraceTransferBean>>();
//K1 遍历收集的消息轨迹数据
for (TraceContext context : contextList) {
if (context.getTraceBeans().isEmpty()) {
continue;
}
// Topic value corresponding to original message entity content
//K1 获取存储消息轨迹的Topic
String topic = context.getTraceBeans().get(0).getTopic();
String regionId = context.getRegionId();
// Use original message entity's topic as key
String key = topic;
if (!StringUtils.isBlank(regionId)) {
key = key + TraceConstants.CONTENT_SPLITOR + regionId;
}
List<TraceTransferBean> transBeanList = transBeanMap.get(key);
if (transBeanList == null) {
transBeanList = new ArrayList<TraceTransferBean>();
transBeanMap.put(key, transBeanList);
}
//K1 对TraceContext进行编码,这里是消息轨迹的传输数据
TraceTransferBean traceData = TraceDataEncoder.encoderFromContextBean(context);
transBeanList.add(traceData);
}
for (Map.Entry<String, List<TraceTransferBean>> entry : transBeanMap.entrySet()) {
String[] key = entry.getKey().split(String.valueOf(TraceConstants.CONTENT_SPLITOR));
String dataTopic = entry.getKey();
String regionId = null;
if (key.length > 1) {
dataTopic = key[0];
regionId = key[1];
}
//K1 将编码后的数据发送到Broker服务器
flushData(entry.getValue(), dataTopic, regionId);
}
}
public static TraceTransferBean encoderFromContextBean(TraceContext ctx) {
if (ctx == null) {
return null;
}
//build message trace of the transfering entity content bean
TraceTransferBean transferBean = new TraceTransferBean();
StringBuilder sb = new StringBuilder(256);
switch (ctx.getTraceType()) {
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);//
}
break;
case 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);//
}
}
break;
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.CONTENT_SPLITOR)
.append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR)
.append(ctx.getGroupName()).append(TraceConstants.FIELD_SPLITOR);
}
}
break;
case EndTransaction: {
TraceBean bean = ctx.getTraceBeans().get(0);
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.getMsgType().ordinal()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getTransactionId()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.getTransactionState().name()).append(TraceConstants.CONTENT_SPLITOR)//
.append(bean.isFromTransactionCheck()).append(TraceConstants.FIELD_SPLITOR);
}
break;
default:
}
transferBean.setTransData(sb.toString());
for (TraceBean bean : ctx.getTraceBeans()) {
transferBean.getTransKey().add(bean.getMsgId());
if (bean.getKeys() != null && bean.getKeys().length() > 0) {
String[] keys = bean.getKeys().split(MessageConst.KEY_SEPARATOR);
transferBean.getTransKey().addAll(Arrays.asList(keys));
}
}
return transferBean;
}
- 对于发送给Broker的消息,重要的是RecommandCode,那么这个的Code是什么呢,深入代码,发现,就是一个正常消息的发送,Broker并没有专门的Processor进行处理,那么回头想想,如果还是按照数据的主题进行投递,那岂不是就出现问题了,肯定存在着主题的转换,这种太常见了。最终发现是在flushData里面,源码直接看一波
flushData
- 调用到了 sendTraceDataByMQ 的逻辑
private void flushData(List<TraceTransferBean> transBeanList, String dataTopic, String regionId) {
if (transBeanList.size() == 0) {
return;
}
// Temporary buffer
StringBuilder buffer = new StringBuilder(1024);
int count = 0;
Set<String> keySet = new HashSet<String>();
for (TraceTransferBean bean : transBeanList) {
// Keyset of message trace includes msgId of or original message
keySet.addAll(bean.getTransKey());
//包含了原始数据的一些信息
buffer.append(bean.getTransData());
count++;
// Ensure that the size of the package should not exceed the upper limit.
if (buffer.length() >= traceProducer.getMaxMessageSize()) {
sendTraceDataByMQ(keySet, buffer.toString(), dataTopic, regionId);
// Clear temporary buffer after finishing
buffer.delete(0, buffer.length());
keySet.clear();
count = 0;
}
}
if (count > 0) {
sendTraceDataByMQ(keySet, buffer.toString(), dataTopic, regionId);
}
transBeanList.clear();
}
sendTraceDataByMQ
- 发送之前,直接将主题改成了traceTopic,而且也没有保存数据主题,也是,这个消息,和原始数据没有关系,也不属于原始的主题,根本不用单独保留,数据主题已经在data中了。
private void sendTraceDataByMQ(Set<String> keySet, final String data, String dataTopic, String regionId) {
//IMP RMQ_SYS_TRACE_TOPIC
String traceTopic = traceTopicName;
//主题变化
if (AccessChannel.CLOUD == accessChannel) {
traceTopic = TraceConstants.TRACE_TOPIC_PREFIX + regionId;
}
final Message message = new Message(traceTopic, data.getBytes());
// Keyset of message trace includes msgId of or original message
message.setKeys(keySet);
try {
Set<String> traceBrokerSet = tryGetMessageQueueBrokerSet(traceProducer.getDefaultMQProducerImpl(), traceTopic);
SendCallback callback = new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
}
@Override
public void onException(Throwable e) {
log.error("send trace data failed, the traceData is {}", data, e);
}
};
if (traceBrokerSet.isEmpty()) {
// No cross set
traceProducer.send(message, callback, 5000);
} else {
traceProducer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Set<String> brokerSet = (Set<String>) arg;
List<MessageQueue> filterMqs = new ArrayList<MessageQueue>();
for (MessageQueue queue : mqs) {
if (brokerSet.contains(queue.getBrokerName())) {
filterMqs.add(queue);
}
}
int index = sendWhichQueue.incrementAndGet();
int pos = Math.abs(index) % filterMqs.size();
if (pos < 0) {
pos = 0;
}
return filterMqs.get(pos);
}
}, traceBrokerSet, callback);
}
} catch (Exception e) {
log.error("send trace data failed, the traceData is {}", data, e);
}
}
这样,整个流程就结束了,那么整个流程就是下图所示
消费端
和生产端类似也是通过钩子方法,进行轨迹记录,然后发送给Broker进行保存。【后续跟进,敬请期待】
存储
其实从上面的分析,我们已经得知,RocketMQ的消息轨迹数据存储在到Broker上,那消息轨迹的主题名如何指定?其路由信息又怎么分配才好呢?是每台Broker上都创建还是只在其中某台上创建呢?RocketMQ支持系统默认与自定义消息轨迹的主题。
我们已经知道了,消息轨迹使用的主题是常量值 RMQ_SYS_TRACE_TOPIC,那么此主题什么时候创建呢。如果是系统默认的,那么就从Broker的构造方法,Broker的启动,最终定位到构造方法
public BrokerController(
final BrokerConfig brokerConfig,
final NettyServerConfig nettyServerConfig,
final NettyClientConfig nettyClientConfig,
final MessageStoreConfig messageStoreConfig
) {
//省略了其他无关内容
this.topicConfigManager = messageStoreConfig.isEnableLmq() ? new LmqTopicConfigManager(this) : new TopicConfigManager(this);
}
public TopicConfigManager(BrokerController brokerController) {
{
//如果开启了轨迹记录的功能,找到了
if (this.brokerController.getBrokerConfig().isTraceTopicEnable()) {
String topic = this.brokerController.getBrokerConfig().getMsgTraceTopicName();
TopicConfig topicConfig = new TopicConfig(topic);
TopicValidator.addSystemTopic(topic);
topicConfig.setReadQueueNums(1);
topicConfig.setWriteQueueNums(1);
this.topicConfigTable.put(topicConfig.getTopicName(), topicConfig);
}
}
}
- 如果Broker开启了消息轨迹跟踪(traceTopicEnable=true)时,会自动创建默认消息轨迹的topic路由信息,注意其读写队列数为1。
- 那么相应的存储,自然也就是普通消息的存储方式了
引用借鉴链接:blog.csdn.net/prestigedin…