RocketMQ 源码学习--重要机制-02 消息轨迹源码解析

325 阅读14分钟

问题

对于消息而言,必须记录当前消息处理的轨迹,日常开发中,主要使用唯一ID,再不同业务,以及不同中间件中进行传递,然后通过日志的方式全部串联起来,那么MQ也肯定存在这样的消息轨迹机制,便于监控消息的运行情况等等。

那么消息的轨迹肯定需要解决几个问题

  1. 消息轨迹模型什么样子
  2. 消息轨迹什么时候记录
  3. 消息轨迹信息记录在哪里

模型

  • 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");
        }
      }
    }
    
  • 类图结构

    image-20231221162010447转存失败,建议直接上传图片文件

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
  • 主要的过程

    1. 如果topic主题为消息轨迹的Topic,直接返回。
    2. 从MqTraceContext中获取跟踪的TraceBean,虽然设计成List结构体,但在消息发送场景,这里的数据永远只有一条,及时是批量发送也不例外。
    3. 获取消息发送到收到响应结果的耗时。
    4. 设置costTime(耗时)、success(是否发送成功)、regionId(发送到broker所在的分区)、msgId(消息ID,全局唯一)、offsetMsgId(消息物理偏移量,如果是批量消息,则是最后一条消息的物理偏移量)、storeTime,这里使用的是(客户端发送时间 + 二分之一的耗时)来表示消息的存储时间,这里是一个估值。
    5. 将需要跟踪的信息通过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

  1. 遍历消息轨迹请求
  2. 获取轨迹主题
  3. 构造 TraceTransferBean 编码对象,这个对象会针对不同的时期进行编码设置
  4. 发送给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);
  }
}

这样,整个流程就结束了,那么整个流程就是下图所示

20190803204514547.png

消费端

和生产端类似也是通过钩子方法,进行轨迹记录,然后发送给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…