这篇文章,我们聊一聊 RocketMQ 的消息轨迹设计思路。

查询消息轨迹可作为生产环境中排查问题强有力的数据支持 ,也是研发同学解决线上问题的重要武器之一。

1 基础概念

消息轨迹是指一条消息从生产者发送到 Broker , 再到消费者消费,整个过程中的各个相关节点的时间、状态等数据汇聚而成的完整链路信息。

当我们需要查询消息轨迹时,需要明白一点:消息轨迹数据是存储在 Broker 服务端,我们需要定义一个主题,在生产者,消费者端定义轨迹钩子

2 开启轨迹

2.1 修改 Broker 配置文件

  1. # 开启消息轨迹
  2. traceTopicEnable=true

2.2 生产者配置

  1. public DefaultMQProducer(final String producerGroup, boolean enableMsgTrace)
  2. public DefaultMQProducer(final String producerGroup, boolean enableMsgTrace, final String customizedTraceTopic)

在生产者的构造函数里,有两个核心参数:

  • enableMsgTrace:是否开启消息轨迹
  • customizedTraceTopic:记录消息轨迹的 Topic , 默认是: RMQ_SYS_TRACE_TOPIC

执行如下的生产者代码:

  1. public class Producer {
  2. public static final String PRODUCER_GROUP = "mytestGroup";
  3. public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
  4. public static final String TOPIC = "example";
  5. public static final String TAG = "TagA";
  6. public static void main(String[] args) throws MQClientException, InterruptedException {
  7. DefaultMQProducer producer = new DefaultMQProducer(PRODUCER_GROUP, true);
  8. producer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
  9. producer.start();
  10. try {
  11. String key = UUID.randomUUID().toString();
  12. System.out.println(key);
  13. Message msg = new Message(
  14. TOPIC,
  15. TAG,
  16. key,
  17. ("Hello RocketMQ ").getBytes(RemotingHelper.DEFAULT_CHARSET));
  18. SendResult sendResult = producer.send(msg);
  19. System.out.printf("%s%n", sendResult);
  20. } catch (Exception e) {
  21. e.printStackTrace();
  22. }
  23. // 这里休眠十秒,是为了异步发送轨迹消息成功。
  24. Thread.sleep(10000);
  25. producer.shutdown();
  26. }
  27. }

在生产者代码中,我们指定了消息的 key 属性, 便于对于消息进行高性能检索。

执行成功之后,我们从控制台查看轨迹信息。

从图中可以看到,消息轨迹中存储了消息的 存储时间 存储服务器IP发送耗时

2.3 消费者配置

和生产者类似,消费者的构造函数可以传递轨迹参数:

  1. public DefaultMQPushConsumer(final String consumerGroup, boolean enableMsgTrace);
  2. public DefaultMQPushConsumer(final String consumerGroup, boolean enableMsgTrace, final String customizedTraceTopic);

执行如下的消费者代码:

  1. public class Consumer {
  2. public static final String CONSUMER_GROUP = "exampleGruop";
  3. public static final String DEFAULT_NAMESRVADDR = "127.0.0.1:9876";
  4. public static final String TOPIC = "example";
  5. public static void main(String[] args) throws InterruptedException, MQClientException {
  6. DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP , true);
  7. consumer.setNamesrvAddr(DEFAULT_NAMESRVADDR);
  8. consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
  9. consumer.subscribe(TOPIC, "*");
  10. consumer.registerMessageListener((MessageListenerConcurrently) (msg, context) -> {
  11. System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msg);
  12. return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
  13. });
  14. consumer.start();
  15. System.out.printf("Consumer Started.%n");
  16. }
  17. }

3 实现原理

轨迹的实现原理主要是在生产者发送、消费者消费时添加相关的钩子。 因此,我们只需要了解钩子的实现逻辑即可。

下面的代码是 DefaultMQProducer 的构造函数。

  1. public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook,
  2. boolean enableMsgTrace, final String customizedTraceTopic) {
  3. this.namespace = namespace;
  4. this.producerGroup = producerGroup;
  5. defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
  6. // if client open the message trace feature
  7. if (enableMsgTrace) {
  8. try {
  9. //异步轨迹分发器
  10. AsyncTraceDispatcher dispatcher = new AsyncTraceDispatcher(producerGroup, TraceDispatcher.Type.PRODUCE, customizedTraceTopic, rpcHook);
  11. dispatcher.setHostProducer(this.defaultMQProducerImpl);
  12. traceDispatcher = dispatcher;
  13. // 发送消息时添加执行钩子
  14. this.defaultMQProducerImpl.registerSendMessageHook(
  15. new SendMessageTraceHookImpl(traceDispatcher));
  16. // 结束事务时添加执行钩子
  17. this.defaultMQProducerImpl.registerEndTransactionHook(
  18. new EndTransactionTraceHookImpl(traceDispatcher));
  19. } catch (Throwable e) {
  20. log.error("system mqtrace hook init failed ,maybe can't send msg trace data");
  21. }
  22. }
  23. }

当是否开启轨迹开关打开时,创建异步轨迹分发器 AsyncTraceDispatcher ,然后给默认的生产者实现类在发送消息的钩子 SendMessageTraceHookImpl

  1. //发送消息时添加执行钩子
  2. this.defaultMQProducerImpl.registerSendMessageHook(new SendMessageTraceHookImpl(traceDispatcher));

我们把生产者发送消息的流程简化如下代码 :

  1. //DefaultMQProducerImpl#sendKernelImpl
  2. this.executeSendMessageHookBefore(context);
  3. // 发生消息
  4. this.mQClientFactory.getMQClientAPIImpl().sendMessage(....)
  5. // 生产者发送消息后会执行
  6. this.executeSendMessageHookAfter(context);

进入SendMessageTraceHookImpl 类 ,该类主要有两个方法 sendMessageBefore sendMessageAfter

1、sendMessageBefore 方法

  1. public void sendMessageBefore(SendMessageContext context) {
  2. //if it is message trace data,then it doesn't recorded
  3. if (context == null || context.getMessage().getTopic().startsWith(((AsyncTraceDispatcher) localDispatcher).getTraceTopicName())) {
  4. return;
  5. }
  6. //build the context content of TuxeTraceContext
  7. TraceContext tuxeContext = new TraceContext();
  8. tuxeContext.setTraceBeans(new ArrayList<TraceBean>(1));
  9. context.setMqTraceContext(tuxeContext);
  10. tuxeContext.setTraceType(TraceType.Pub);
  11. tuxeContext.setGroupName(NamespaceUtil.withoutNamespace(context.getProducerGroup()));
  12. //build the data bean object of message trace
  13. TraceBean traceBean = new TraceBean();
  14. traceBean.setTopic(NamespaceUtil.withoutNamespace(context.getMessage().getTopic()));
  15. traceBean.setTags(context.getMessage().getTags());
  16. traceBean.setKeys(context.getMessage().getKeys());
  17. traceBean.setStoreHost(context.getBrokerAddr());
  18. traceBean.setBodyLength(context.getMessage().getBody().length);
  19. traceBean.setMsgType(context.getMsgType());
  20. tuxeContext.getTraceBeans().add(traceBean);
  21. }

发送消息之前,先收集消息的 topic 、tag、key 、存储 Broker 的 IP 地址、消息体的长度等基础信息,并将消息轨迹数据存储在调用上下文中。

2、sendMessageAfter 方法

  1. public void sendMessageAfter(SendMessageContext context) {
  2. // ...省略部分代码
  3. TraceContext tuxeContext = (TraceContext) context.getMqTraceContext();
  4. TraceBean traceBean = tuxeContext.getTraceBeans().get(0);
  5. int costTime = (int) ((System.currentTimeMillis() - tuxeContext.getTimeStamp()) / tuxeContext.getTraceBeans().size());
  6. tuxeContext.setCostTime(costTime);
  7. if (context.getSendResult().getSendStatus().equals(SendStatus.SEND_OK)) {
  8. tuxeContext.setSuccess(true);
  9. } else {
  10. tuxeContext.setSuccess(false);
  11. }
  12. tuxeContext.setRegionId(context.getSendResult().getRegionId());
  13. traceBean.setMsgId(context.getSendResult().getMsgId());
  14. traceBean.setOffsetMsgId(context.getSendResult().getOffsetMsgId());
  15. traceBean.setStoreTime(tuxeContext.getTimeStamp() + costTime / 2);
  16. localDispatcher.append(tuxeContext);
  17. }

跟踪对象里会保存 costTime (消息发送时间)、success (是否发送成功)、regionId (发送到 Broker 所在的分区) 、 msgId (消息 ID,全局唯一)、offsetMsgId (消息物理偏移量) ,storeTime (存储时间 ) 。

存储时间并没有取消息的实际存储时间,而是估算出来的:客户端发送时间的一般的耗时表示消息的存储时间。

最后将跟踪上下文添加到本地轨迹分发器:

  1. localDispatcher.append(tuxeContext);

下面我们分析下轨迹分发器的原理:

  1. public AsyncTraceDispatcher(String group, Type type, String traceTopicName, RPCHook rpcHook) {
  2. // 省略代码 ....
  3. this.traceContextQueue = new ArrayBlockingQueue<TraceContext>(1024);
  4. this.appenderQueue = new ArrayBlockingQueue<Runnable>(queueSize);
  5. if (!UtilAll.isBlank(traceTopicName)) {
  6. this.traceTopicName = traceTopicName;
  7. } else {
  8. this.traceTopicName = TopicValidator.RMQ_SYS_TRACE_TOPIC;
  9. }
  10. this.traceExecutor = new ThreadPoolExecutor(//
  11. 10,
  12. 20,
  13. 1000 * 60,
  14. TimeUnit.MILLISECONDS,
  15. this.appenderQueue,
  16. new ThreadFactoryImpl("MQTraceSendThread_"));
  17. traceProducer = getAndCreateTraceProducer(rpcHook);
  18. }
  19. public void start(String nameSrvAddr, AccessChannel accessChannel) throws MQClientException {
  20. if (isStarted.compareAndSet(false, true)) {
  21. traceProducer.setNamesrvAddr(nameSrvAddr);
  22. traceProducer.setInstanceName(TRACE_INSTANCE_NAME + "_" + nameSrvAddr);
  23. traceProducer.start();
  24. }
  25. this.accessChannel = accessChannel;
  26. this.worker = new Thread(new AsyncRunnable(), "MQ-AsyncTraceDispatcher-Thread-" + dispatcherId);
  27. this.worker.setDaemon(true);
  28. this.worker.start();
  29. this.registerShutDownHook();
  30. }

上面的代码展示了分发器的构造函数和启动方法,构造函数创建了一个发送消息的线程池 traceExecutor ,启动 start 后会启动一个 worker线程

  1. class AsyncRunnable implements Runnable {
  2. private boolean stopped;
  3. @Override
  4. public void run() {
  5. while (!stopped) {
  6. synchronized (traceContextQueue) {
  7. long endTime = System.currentTimeMillis() + pollingTimeMil;
  8. while (System.currentTimeMillis() < endTime) {
  9. try {
  10. TraceContext traceContext = traceContextQueue.poll(
  11. endTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS
  12. );
  13. if (traceContext != null && !traceContext.getTraceBeans().isEmpty()) {
  14. // get the topic which the trace message will send to
  15. String traceTopicName = this.getTraceTopicName(traceContext.getRegionId());
  16. // get the traceDataSegment which will save this trace message, create if null
  17. TraceDataSegment traceDataSegment = taskQueueByTopic.get(traceTopicName);
  18. if (traceDataSegment == null) {
  19. traceDataSegment = new TraceDataSegment(traceTopicName, traceContext.getRegionId());
  20. taskQueueByTopic.put(traceTopicName, traceDataSegment);
  21. }
  22. // encode traceContext and save it into traceDataSegment
  23. // NOTE if data size in traceDataSegment more than maxMsgSize,
  24. // a AsyncDataSendTask will be created and submitted
  25. TraceTransferBean traceTransferBean = TraceDataEncoder.encoderFromContextBean(traceContext);
  26. traceDataSegment.addTraceTransferBean(traceTransferBean);
  27. }
  28. } catch (InterruptedException ignore) {
  29. log.debug("traceContextQueue#poll exception");
  30. }
  31. }
  32. // NOTE send the data in traceDataSegment which the first TraceTransferBean
  33. // is longer than waitTimeThreshold
  34. sendDataByTimeThreshold();
  35. if (AsyncTraceDispatcher.this.stopped) {
  36. this.stopped = true;
  37. }
  38. }
  39. }
  40. }

worker 启动后,会从轨迹上下文队列 traceContextQueue 中不断的取出轨迹上下文,并将上下文转换成轨迹数据片段 TraceDataSegment

为了提升系统的性能,并不是每一次从队列中获取到数据就直接发送到 MQ ,而是积累到一定程度的临界点才触发这个操作,我们可以简单的理解为批量操作

这里面有两个维度 :

  1. 轨迹数据片段的数据大小大于某个数据大小阈值。笔者认为这段 RocketMQ 4.9.4 版本代码存疑,因为最新的 5.0 版本做了优化。

    1. if (currentMsgSize >= traceProducer.getMaxMessageSize()) {
    2. List<TraceTransferBean> dataToSend = new ArrayList(traceTransferBeanList);
    3. AsyncDataSendTask asyncDataSendTask = new AsyncDataSendTask(traceTopicName, regionId, dataToSend);
    4. traceExecutor.submit(asyncDataSendTask);
    5. this.clear();
    6. }
  2. 当前时间 - 轨迹数据片段的首次存储时间 是否大于刷新时间 ,也就是每500毫秒刷新一次。

    1. private void sendDataByTimeThreshold() {
    2. long now = System.currentTimeMillis();
    3. for (TraceDataSegment taskInfo : taskQueueByTopic.values()) {
    4. if (now - taskInfo.firstBeanAddTime >= waitTimeThresholdMil) {
    5. taskInfo.sendAllData();
    6. }
    7. }
    8. }

轨迹数据存储的格式如下:

  1. TraceBean bean = ctx.getTraceBeans().get(0);
  2. //append the content of context and traceBean to transferBean's TransData
  3. case Pub: {
  4. sb.append(ctx.getTraceType()).append(TraceConstants.CONTENT_SPLITOR)
  5. .append(ctx.getTimeStamp()).append(TraceConstants.CONTENT_SPLITOR)
  6. .append(ctx.getRegionId()).append(TraceConstants.CONTENT_SPLITOR)
  7. .append(ctx.getGroupName()).append(TraceConstants.CONTENT_SPLITOR)
  8. .append(bean.getTopic()).append(TraceConstants.CONTENT_SPLITOR)
  9. .append(bean.getMsgId()).append(TraceConstants.CONTENT_SPLITOR)
  10. .append(bean.getTags()).append(TraceConstants.CONTENT_SPLITOR)
  11. .append(bean.getKeys()).append(TraceConstants.CONTENT_SPLITOR)
  12. .append(bean.getStoreHost()).append(TraceConstants.CONTENT_SPLITOR)
  13. .append(bean.getBodyLength()).append(TraceConstants.CONTENT_SPLITOR)
  14. .append(ctx.getCostTime()).append(TraceConstants.CONTENT_SPLITOR)
  15. .append(bean.getMsgType().ordinal()).append(TraceConstants.CONTENT_SPLITOR)
  16. .append(bean.getOffsetMsgId()).append(TraceConstants.CONTENT_SPLITOR)
  17. .append(ctx.isSuccess()).append(TraceConstants.FIELD_SPLITOR);
  18. }
  19. break;

下图展示了事务轨迹消息数据,每个数据字段是按照 CONTENT_SPLITOR 分隔。

注意:

分隔符 CONTENT_SPLITOR = (char) 1 它在内存中的值是:00000001 , 但是 char i = '1' 它在内存中的值是 49 ,即 00110001。


参考资料:

阿里云文档:

https://help.aliyun.com/zh/apsaramq-for-rocketmq/cloud-message-queue-rocketmq-4-x-series/user-guide/query-a-message-trace

石臻臻:

https://mp.weixin.qq.com/s/saYD3mG9F1z-oAU6STxewQ

聊聊 RocketMQ 消息轨迹的更多相关文章

  1. RocketMQ消息轨迹-设计篇

    目录 1.消息轨迹数据格式 2.记录消息轨迹 3.如何存储消息轨迹数据 @(本节目录) RocketMQ消息轨迹主要包含两篇文章:设计篇与源码分析篇,本节将详细介绍RocketMQ消息轨迹-设计相关. ...

  2. 源码分析RocketMQ消息轨迹

    目录 1.发送消息轨迹流程 1.1 DefaultMQProducer构造函数 1.2 SendMessageTraceHookImpl钩子函数 1.3 TraceDispatcher实现原理 2. ...

  3. RocketMQ之八:重试队列,死信队列,消息轨迹

    问题思考 死信队列的应用场景? 死信队列中的数据是如何产生的? 如何查看死信队列中的数据? 死信队列的读写权限? 死信队列如何消费? 重试队列和死信队列的配置 消息轨迹 1.应用场景 一般应用在当正常 ...

  4. RocketMq消息队列使用

    最近在看消息队列框架 ,alibaba的RocketMQ单机支持1万以上的持久化队列,支持诸多特性, 目前RocketMQ在阿里集团被广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理,bin ...

  5. 程序重启RocketMQ消息重复消费

    最近在调试RocketMQ消息发送与消费的Demo时,发现一个问题:只要重启程序,RocketMQ消息就会重复消费. 那么这是什么原因导致的,又该如何解决呢? 经过一番排查,发现程序使用的Rocket ...

  6. RocketMQ 消息队列单机部署及使用

    转载请注明来源:http://blog.csdn.net/loongshawn/article/details/51086876 相关文章: <RocketMQ 消息队列单机部署及使用> ...

  7. 关于RocketMQ消息消费与重平衡的一些问题探讨

    其实最好的学习方式就是互相交流,最近也有跟网友讨论了一些关于 RocketMQ 消息拉取与重平衡的问题,我姑且在这里写下我的一些总结. ## 关于 push 模式下的消息循环拉取问题 之前发表了一篇关 ...

  8. rocketMq消息的发送和消息消费

    rocketMq消息的发送和消息消费 一.消息推送 public void pushMessage() { String message = "推送消息内容!"; try { De ...

  9. RocketMQ(消息重发、重复消费、事务、消息模式)

    分布式开放消息系统(RocketMQ)的原理与实践 RocketMQ基础:https://github.com/apache/rocketmq/tree/rocketmq-all-4.5.1/docs ...

  10. RocketMQ消息丢失解决方案:同步刷盘+手动提交

    前言 之前我们一起了解了使用RocketMQ事务消息解决生产者发送消息时消息丢失的问题,但使用了事务消息后消息就一定不会丢失了吗,肯定是不能保证的. 因为虽然我们解决了生产者发送消息时候的消息丢失问题 ...

随机推荐

  1. Spring 的依赖注入

    Spring 的依赖注入 @ 目录 Spring 的依赖注入 每博一文案 1. 依赖注入 1.1 构造注入 1.1.1 通过参数名进行构造注入 1.1.2 通过参数的下标,进行构造注入 1.1.3 不 ...

  2. 在windows平台使用Visual Studio 2017编译动态库并使用

    使用VS stdio制作顺序表的库文件 .lib与.dll 区别 lib是编译时需要的 dll是运行时需要的 1.新建头文件和源文件 SeqList.h // SeqList.h #ifndef SE ...

  3. Linux 命令:ps

    ps -ef ps -e f # 树形显示

  4. Unity UGUI的PhysicsRaycaster (物理射线检测)组件的介绍及使用

    Unity UGUI的PhysicsRaycaster (物理射线检测)组件的介绍及使用 1. 什么是PhysicsRaycaster组件? PhysicsRaycaster是Unity UGUI中的 ...

  5. 【译】摇摆你的调试游戏:你需要知道的 Parallel Stack Window 小知识!

    在 Visual Studio 2022 17.6和17.7中,我们在 Parallel Stack 窗口中添加了大量新功能,可以将您的多线程调试提升到一个新的水平. 但是 Parallel Stac ...

  6. 部分 Linux 换国内源

    Centos 8 / Redhat 8 换国内源 操作步骤 先把原本的官方 yum 源 删除 或 备份 cd /etc/yum.repos.d/ 备份(Redhat 同理) rename repo r ...

  7. IDEA使用@Autowired注解为什么会提示不建议?

    ​在使用IDEA编写Spring相关的项目时,当在字段上使用@Autowired注解时,总会出现一个波浪线提示:"Field injection is not recommended.&qu ...

  8. 论文解读(MetaAdapt)《MetaAdapt: Domain Adaptive Few-Shot Misinformation Detection via Meta Learning》

    Note:[ wechat:Y466551 | 可加勿骚扰,付费咨询 ] 论文信息 论文标题:MetaAdapt: Domain Adaptive Few-Shot Misinformation De ...

  9. C# 合并Word文档

    需要安装NuGet程序包 Spire.Doc DocX 注:DocX包去除警告提示用 Spire.Doc.Document document = new Spire.Doc.Document();// ...

  10. iframe子窗口调用父窗口方法

    //一个iframe页面调用另一个iframe页面的方法self.parent.frames["sort_bottom"].mapp($("#id").val( ...