链路追踪很多成熟的方案,那么如何只使用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% 的消息)。
-
统一监控数据格式 :
确保日志和指标字段命名一致,便于跨系统关联分析。
通过上述方法,可以全面监控 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. 关键增强点说明
-
生产者 traceId 传递
- 自动从当前线程的
MDC
获取traceId
(适合 Web 请求场景)。 - 支持手动指定
traceId
(适合异步任务场景)。
- 自动从当前线程的
-
消费者 traceId 绑定
- 消费消息时自动将
traceId
存入MDC
,确保日志链路连续。 - 使用
finally
块确保MDC
清理,避免线程池复用导致数据错乱。
- 消费消息时自动将
-
泛型消息处理
通过
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
的核心原理
- 线程绑定 :MDC 使用
ThreadLocal
存储键值对,每个线程独立维护一份数据。 - 日志模板集成 :在日志输出模式(如 Logback 的
pattern
)中引用 MDC 中的键(如%X{traceId}
)。 - 跨线程传递:异步场景下需手动将父线程的 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();
}
}
}
五、注意事项
-
线程池复用问题 :
务必在异步任务结束时调用
MDC.clear()
,避免旧数据污染后续任务。 -
性能影响:
- 频繁调用
MDC.getCopyOfContextMap()
可能影响性能,建议在高并发场景下优化。 - 可改用更轻量级的上下文传递方案(如透传
traceId
而非全量 MDC)。
- 频繁调用
-
框架集成:
- Spring Boot :通过
ThreadPoolTaskExecutor
的TaskDecorator
简化异步传递。 - Dubbo/RPC :通过 Filter 透传
traceId
到远程调用。
- Spring Boot :通过
总结
通过 MDC 传递 traceId
的核心步骤:
- 入口设置 :在请求开始处生成
traceId
并存入 MDC。 - 日志集成 :配置日志模板输出
%X{traceId}
。 - 跨线程传递:装饰线程池任务,复制并清理 MDC。
- 跨系统传递 :通过消息属性或 HTTP Header 透传
traceId
。