RocketMQ埋点

链路追踪很多成熟的方案,那么如何只使用MQ的情况下,进行埋点呢?

使用钩子函数HookFunction:MqContext

在 RocketMQ 中埋点(监控和追踪消息生命周期)通常通过 自定义钩子(Hook)消息轨迹(Trace) 实现。以下是基于 Java 的详细实现方案,涵盖生产端和消费端的埋点逻辑。


一、使用 RocketMQ 钩子(Hook)实现埋点

RocketMQ 提供了 SendMessageHook(生产端)和 ConsumeMessageHook(消费端)接口,可在消息发送/消费前后插入监控逻辑。

1. 生产端埋点

步骤 1:实现 SendMessageHook 接口

java 复制代码
import org.apache.rocketmq.client.hook.SendMessageContext;
import org.apache.rocketmq.client.hook.SendMessageHook;
import org.apache.rocketmq.client.producer.SendResult;

public class ProducerMonitorHook implements SendMessageHook {
    @Override
    public String hookName() {
        return "ProducerMonitor";
    }

    @Override
    public void beforeSendMessage(SendMessageContext context) {
        // 发送前的埋点(记录开始时间、消息内容等)
        context.setMqContext(System.currentTimeMillis());
    }

    @Override
    public void afterSendMessage(SendMessageContext context, SendResult sendResult) {
        // 发送后的埋点(计算耗时、记录结果)
        long startTime = (Long) context.getMqContext();
        long cost = System.currentTimeMillis() - startTime;

        // 记录指标(示例:使用日志或 Metrics 库)
        System.out.printf("[Producer埋点] Topic=%s, 耗时=%dms, 状态=%s%n",
                context.getMessage().getTopic(), cost, sendResult.getSendStatus());
    }
}

步骤 2:注册钩子到 Producer

java 复制代码
DefaultMQProducer producer = new DefaultMQProducer("producer_group");
producer.setNamesrvAddr("localhost:9876");
producer.getDefaultMQProducerImpl().registerSendMessageHook(new ProducerMonitorHook());
producer.start();

2. 消费端埋点

步骤 1:实现 ConsumeMessageHook 接口

java 复制代码
import org.apache.rocketmq.client.hook.ConsumeMessageContext;
import org.apache.rocketmq.client.hook.ConsumeMessageHook;
import org.apache.rocketmq.common.message.MessageExt;

public class ConsumerMonitorHook implements ConsumeMessageHook {
    @Override
    public String hookName() {
        return "ConsumerMonitor";
    }

    @Override
    public void beforeConsume(ConsumeMessageContext context) {
        // 消费前的埋点
        context.setMqContext(System.currentTimeMillis());
    }

    @Override
    public void afterConsume(ConsumeMessageContext context, ConsumeStatus status) {
        // 消费后的埋点
        long startTime = (Long) context.getMqContext();
        long cost = System.currentTimeMillis() - startTime;

        // 遍历消息记录
        for (MessageExt msg : context.getMsgList()) {
            System.out.printf("[Consumer埋点] Topic=%s, 耗时=%dms, 状态=%s%n",
                    msg.getTopic(), cost, status);
        }
    }
}

步骤 2:注册钩子到 Consumer

java 复制代码
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("test_topic", "*");
consumer.registerConsumeMessageHook(new ConsumerMonitorHook());
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
    // 业务处理逻辑
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
consumer.start();

二、使用 RocketMQ 消息轨迹(Trace)

RocketMQ 内置了消息轨迹功能,可记录消息的完整生命周期(生产、存储、消费),需启用 Trace 并配置轨迹数据存储。

1. 开启消息轨迹

生产端配置:

java 复制代码
DefaultMQProducer producer = new DefaultMQProducer("producer_group", true); // 启用轨迹
producer.setNamesrvAddr("localhost:9876");
producer.start();

消费端配置:

java 复制代码
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group", true); // 启用轨迹
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("test_topic", "*");
consumer.start();
2. 轨迹数据存储

broker.conf 中配置轨迹 Topic(默认 RMQ_SYS_TRACE_TOPIC)并确保 Broker 允许自动创建 Topic。


三、集成监控系统(示例:Prometheus)

将埋点数据导出到监控系统(如 Prometheus),结合 Micrometer 生成指标。

1. 添加 Micrometer 依赖
xml 复制代码
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-core</artifactId>
    <version>1.9.0</version>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    <version>1.9.0</version>
</dependency>
2. 定义指标
java 复制代码
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;

public class RocketMQMetrics {
    private final Timer producerTimer;
    private final Timer consumerTimer;

    public RocketMQMetrics(MeterRegistry registry) {
        // 生产端耗时统计
        producerTimer = Timer.builder("rocketmq.producer.latency")
                .description("Producer message send latency")
                .register(registry);

        // 消费端耗时统计
        consumerTimer = Timer.builder("rocketmq.consumer.latency")
                .description("Consumer message process latency")
                .register(registry);
    }

    public void recordProducerLatency(long costMs) {
        producerTimer.record(costMs, TimeUnit.MILLISECONDS);
    }

    public void recordConsumerLatency(long costMs) {
        consumerTimer.record(costMs, TimeUnit.MILLISECONDS);
    }
}
3. 在钩子中记录指标
java 复制代码
// 生产端钩子示例
@Override
public void afterSendMessage(SendMessageContext context, SendResult sendResult) {
    long cost = System.currentTimeMillis() - startTime;
    rocketMQMetrics.recordProducerLatency(cost);
}

四、日志埋点(结构化日志)

使用 JSON 格式日志,方便后续分析(如 ELK 收集)。

示例:Logback 配置
xml 复制代码
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
在钩子中记录结构化日志
java 复制代码
// 使用 SLF4J
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ProducerMonitorHook implements SendMessageHook {
    private static final Logger logger = LoggerFactory.getLogger("RocketMQMonitor");

    @Override
    public void afterSendMessage(SendMessageContext context, SendResult sendResult) {
        long cost = System.currentTimeMillis() - startTime;
        logger.info("{{\"event\": \"producer\", \"topic\": \"{}\", \"cost\": {}, \"status\": \"{}\"}}",
                context.getMessage().getTopic(), cost, sendResult.getSendStatus());
    }
}

五、最佳实践

  1. 合理选择埋点位置

    • 生产端:关注发送耗时、失败率、消息大小。
    • 消费端:关注消费耗时、重试次数、死信队列情况。
  2. 避免过度埋点

    高频消息场景下,建议采用采样率控制(如仅记录 1% 的消息)。

  3. 统一监控数据格式

    确保日志和指标字段命名一致,便于跨系统关联分析。

通过上述方法,可以全面监控 RocketMQ 的消息链路,快速定位性能瓶颈或异常问题。

使用 User Properties

在原有封装代码的基础上,增加 traceId 的传递和日志集成。以下是补充后的完整代码:


1. 生产者模板类(增强版)

java 复制代码
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.slf4j.MDC;
import java.util.UUID;

public class RocketMQProducerTemplate {
    private final DefaultMQProducer producer;

    public RocketMQProducerTemplate(RocketMQConfig config) throws MQClientException {
        producer = new DefaultMQProducer(config.getProducerGroup());
        producer.setNamesrvAddr(config.getNameServerAddr());
        producer.start();
    }

    /**
     * 发送消息(自动生成 traceId)
     */
    public SendResult send(String topic, String tag, Object body) {
        return send(topic, tag, body, generateTraceId());
    }

    /**
     * 发送消息(指定 traceId)
     */
    public SendResult send(String topic, String tag, Object body, String traceId) {
        try {
            Message msg = buildMessage(topic, tag, body, traceId);
            return producer.send(msg);
        } catch (Exception e) {
            throw new RuntimeException("MQ消息发送失败", e);
        }
    }

    /**
     * 构建消息(注入 traceId)
     */
    private Message buildMessage(String topic, String tag, Object body, String traceId) {
        Message msg = new Message(topic, tag, SerializeUtils.serialize(body));
        msg.putUserProperty("traceId", traceId); // 关键:设置 traceId
        return msg;
    }

    /**
     * 生成 traceId(可重写为分布式 ID)
     */
    protected String generateTraceId() {
        // 优先从当前线程上下文获取(如 Web 请求已生成)
        String traceId = MDC.get("traceId");
        return traceId != null ? traceId : UUID.randomUUID().toString();
    }

    // shutdown() 方法省略
}

2. 消费者模板类(增强版)

java 复制代码
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.slf4j.MDC;
import java.util.List;

public class RocketMQConsumerTemplate {
    // ... 其他代码同前 ...

    /**
     * 订阅主题并自动处理 traceId
     */
    public void subscribe(String topic, String tag, MessageHandler handler) throws MQClientException {
        consumer.subscribe(topic, tag);
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                // 提取 traceId 并绑定到 MDC
                String traceId = msg.getUserProperty("traceId");
                MDC.put("traceId", traceId != null ? traceId : "N/A");

                try {
                    Object body = SerializeUtils.deserialize(msg.getBody(), handler.getBodyType());
                    handler.handle(body);
                } finally {
                    MDC.clear(); // 必须清理,避免内存泄漏
                }
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        consumer.start();
    }

    /**
     * 消息处理接口(泛型支持)
     */
    public interface MessageHandler<T> {
        void handle(T body);
        Class<T> getBodyType();
    }
}

3. 业务方调用示例

发送消息(自动传递 traceId)
java 复制代码
public class OrderService {
    private RocketMQProducerTemplate producer;

    public void createOrder(Order order) {
        // 场景1:自动生成 traceId
        producer.send("ORDER_TOPIC", "CREATE", order);

        // 场景2:跨服务传递已有 traceId(如从 HTTP 请求上下文获取)
        String traceId = MDC.get("traceId");
        producer.send("ORDER_TOPIC", "CREATE", order, traceId);
    }
}
消费消息(自动绑定 traceId)
java 复制代码
public class OrderListener {
    public void init() throws MQClientException {
        RocketMQConsumerTemplate consumer = RocketMQFactory.createConsumer();
        consumer.subscribe("ORDER_TOPIC", "CREATE", new RocketMQConsumerTemplate.MessageHandler<Order>() {
            @Override
            public void handle(Order order) {
                // 日志自动携带 traceId
                logger.info("处理订单: {}", order.getId());
                // 调用其他服务时传递 traceId
                restTemplate.getForEntity("http://inventory/check?traceId={traceId}", ...);
            }

            @Override
            public Class<Order> getBodyType() {
                return Order.class;
            }
        });
    }
}

4. 关键增强点说明

  1. 生产者 traceId 传递

    • 自动从当前线程的 MDC 获取 traceId(适合 Web 请求场景)。
    • 支持手动指定 traceId(适合异步任务场景)。
  2. 消费者 traceId 绑定

    • 消费消息时自动将 traceId 存入 MDC,确保日志链路连续。
    • 使用 finally 块确保 MDC 清理,避免线程池复用导致数据错乱。
  3. 泛型消息处理

    通过 MessageHandler 接口,业务方直接处理反序列化后的对象,无需关注底层细节。


5. 日志配置示例(logback.xml)

确保日志模式包含 traceId

xml 复制代码
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n</pattern>

输出示例

css 复制代码
[2023-10-01 14:30:22] [f81d4fae-7dec-41] [ConsumeMessageThread_1] INFO  OrderListener - 处理订单: 1001

通过以上封装,业务代码无需关心 traceId 的传递细节,只需关注业务逻辑,同时保证了全链路追踪能力。

补充:MDC 传递 traceId

在 Java 的日志框架(如 SLF4J + Logback)中,MDC(Mapped Diagnostic Context) 通过线程绑定的上下文(ThreadLocal)来传递 traceId,确保同一请求链路中的所有日志自动携带该标识。以下是具体实现步骤和代码示例:


一、MDC 传递 traceId 的核心原理

  1. 线程绑定 :MDC 使用 ThreadLocal 存储键值对,每个线程独立维护一份数据。
  2. 日志模板集成 :在日志输出模式(如 Logback 的 pattern)中引用 MDC 中的键(如 %X{traceId})。
  3. 跨线程传递:异步场景下需手动将父线程的 MDC 内容复制到子线程。

二、基础使用示例(同步场景)

1. 在请求入口处设置 traceId
java 复制代码
import org.slf4j.MDC;
import javax.servlet.*;
import java.io.IOException;
import java.util.UUID;

// Web 过滤器示例(Spring Boot)
public class TraceFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
        throws IOException, ServletException {
        try {
            // 生成 traceId 并存入 MDC
            String traceId = UUID.randomUUID().toString();
            MDC.put("traceId", traceId);
            
            chain.doFilter(request, response);
        } finally {
            MDC.clear(); // 请求结束后清理
        }
    }
}
2. 在日志中显示 traceId

配置 logback.xml

xml 复制代码
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
  <root level="INFO">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

日志输出

css 复制代码
[2023-10-01 14:30:22] [f81d4fae-7dec-41] INFO  com.example.Service - 处理用户请求

三、跨线程传递(异步场景)

1. 装饰 Runnable/Callable
java 复制代码
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.*;

public class MDCThreadPoolExecutor extends ThreadPoolExecutor {

    public MDCThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, 
                               BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    public void execute(Runnable task) {
        // 复制当前线程的 MDC 到子线程
        Map<String, String> context = MDC.getCopyOfContextMap();
        super.execute(() -> {
            try {
                MDC.setContextMap(context);
                task.run();
            } finally {
                MDC.clear();
            }
        });
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        Map<String, String> context = MDC.getCopyOfContextMap();
        return super.submit(() -> {
            try {
                MDC.setContextMap(context);
                return task.call();
            } finally {
                MDC.clear();
            }
        });
    }
}
2. 使用示例
java 复制代码
public class AsyncService {
    private static final ExecutorService executor = new MDCThreadPoolExecutor(
        8, 8, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()
    );

    public void asyncProcess() {
        executor.execute(() -> {
            // 子线程日志自动携带 traceId
            logger.info("异步处理任务");
        });
    }
}

四、消息队列集成(如 RocketMQ)

1. 生产端:传递 traceId 到消息属性
java 复制代码
public class MQProducer {
    public void sendMessage(String topic, String tag, Object body) {
        Message msg = new Message(topic, tag, SerializeUtils.serialize(body));
        // 从 MDC 获取 traceId 并存入消息属性
        msg.putUserProperty("traceId", MDC.get("traceId"));
        producer.send(msg);
    }
}
2. 消费端:从消息属性提取 traceId
java 复制代码
public class MQConsumer {
    public void onMessage(MessageExt msg) {
        try {
            // 提取 traceId 并存入 MDC
            String traceId = msg.getUserProperty("traceId");
            MDC.put("traceId", traceId);
            
            logger.info("处理消息: {}", msg.getMsgId());
            // 业务逻辑...
        } finally {
            MDC.clear();
        }
    }
}

五、注意事项

  1. 线程池复用问题

    务必在异步任务结束时调用 MDC.clear(),避免旧数据污染后续任务。

  2. 性能影响

    • 频繁调用 MDC.getCopyOfContextMap() 可能影响性能,建议在高并发场景下优化。
    • 可改用更轻量级的上下文传递方案(如透传 traceId 而非全量 MDC)。
  3. 框架集成

    • Spring Boot :通过 ThreadPoolTaskExecutorTaskDecorator 简化异步传递。
    • Dubbo/RPC :通过 Filter 透传 traceId 到远程调用。

总结

通过 MDC 传递 traceId 的核心步骤:

  1. 入口设置 :在请求开始处生成 traceId 并存入 MDC。
  2. 日志集成 :配置日志模板输出 %X{traceId}
  3. 跨线程传递:装饰线程池任务,复制并清理 MDC。
  4. 跨系统传递 :通过消息属性或 HTTP Header 透传 traceId
相关推荐
uhakadotcom24 分钟前
Python 量化计算入门:基础库和实用案例
后端·算法·面试
小萌新上大分24 分钟前
SpringCloudGateWay
java·开发语言·后端·springcloud·springgateway·cloudalibaba·gateway网关
uhakadotcom1 小时前
使用Python获取Google Trends数据:2025年详细指南
后端·面试·github
uhakadotcom1 小时前
使用 Python 与 Google Cloud Bigtable 进行交互
后端·面试·github
直视太阳1 小时前
springboot+easyexcel实现下载excels模板下拉选择
java·spring boot·后端
追逐时光者2 小时前
C#/.NET/.NET Core技术前沿周刊 | 第 33 期(2025年4.1-4.6)
后端·.net
灼华十一2 小时前
Golang系列 - 内存对齐
开发语言·后端·golang
兰亭序咖啡2 小时前
学透Spring Boot — 009. Spring Boot的四种 Http 客户端
java·spring boot·后端
Asthenia04122 小时前
深入解析Pandas索引机制:离散选择与聚合选择的差异及常见误区
后端