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
相关推荐
天天摸鱼的java工程师19 分钟前
解释 Spring 框架中 bean 的生命周期:一个八年 Java 开发的实战视角
java·后端
往事随风去32 分钟前
那个让老板闭嘴、让性能翻倍的“黑科技”:基准测试最全指南
后端·测试
李广坤42 分钟前
JAVA线程池详解
后端
调试人生的显微镜1 小时前
深入剖析 iOS 26 系统流畅度,多工具协同监控与性能优化实践
后端
蹦跑的蜗牛1 小时前
Spring Boot使用Redis实现消息队列
spring boot·redis·后端
非凡ghost1 小时前
HWiNFO(专业系统信息检测工具)
前端·javascript·后端
非凡ghost1 小时前
FireAlpaca(免费数字绘图软件)
前端·javascript·后端
非凡ghost1 小时前
Sucrose Wallpaper Engine(动态壁纸管理工具)
前端·javascript·后端
间彧1 小时前
从零到一搭建Spring Cloud Alibbaba项目
后端
楼田莉子1 小时前
C++学习:C++11关于类型的处理
开发语言·c++·后端·学习