写作本文的目的在于项目内技术的总结,侧重分享自己的理解,代码部分参考ChatGPT。
链路追踪:追踪请求的路径、监控性能、由一条线来排查问题。
问题引入:笔者在多线程执行任务时,出现了A用户提交的任务显示B用户的用户信息,此时ThreadLocal作为存储用户信息的载体,经过排查发现,是因为多线程中在fork子任务时,子线程并不能继承父线程的用户信息。
第一次尝试:查询发现InheritableThreadLocal作为用户信息存放载体时,初始化子线程时,会把信息带入到子线程,此时存在一个问题,如果子线程用户信息存在,此时InheritableThreadLocal并不会往子线程塞入用户信息。
第二次尝试:使用的阿里巴巴开源的TTL(TransmittableThreadLocal),可以解决线程池中ThreadLocal变量传递的问题。
排查上述问题,耗费了大量的时间,翻出来之前的代码逐一排查分析后,才找到问题。在笔者所在的项目中,虽然系统主要牵涉到两个系统,但由于数据交互非常频繁,借此机会在项目中引入分布式链路追踪,提高问题的排查效率。
1、单体服务
借助MDC工具,底层实现ThreadLocal<Map<String, String>> localMap
(对比存储用户信息使用的ThreadLocal localMap)
举例说明:利用SLF4J与MDC(Mapped Diagnostic Context)结合实现
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ExampleService {
private static final Logger logger = LoggerFactory.getLogger(ExampleService.class);
public void handleRequest() {
// 生成Trace ID
String traceId = UUID.randomUUID().toString();
// 将Trace ID放入MDC
MDC.put("traceId", traceId);
try {
logger.info("Request received");
// 执行业务逻辑
logger.info("Processing business logic");
} finally {
// 移除MDC中的Trace ID,避免线程复用导致的污染
MDC.remove("traceId");
}
}
}
使用MDC后,你可以在日志格式配置中自动将Trace ID加入到日志输出中。例如,使用Logback时,可以在logback.xml中配置日志格式:
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %X{traceId} - %msg%n</pattern>
这样,所有日志都会自动带上Trace ID,无需手动在每条日志记录中添加。
2、跨服务的链路追踪实现
笔者所涉及的项目,项目传递路线明确,对不同服务传递逐一实现更切合需求(引入三方框架,一方面太重,系统复杂度也提高),此处作为重点实现
2.1、网关(加入全局过滤器)
1、设置traceId到MDC(此刻网关服务的日志中将会打印traceId
2、通过header传递到下游服务。
首先,需要实现一个自定义的过滤器,用于在请求处理前后执行 Trace ID 的生成和传递。以Spring框架为例,你可以使用javax.servlet.Filter接口来实现。
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
@Component
public class TraceIdFilter implements Filter {
private static final String TRACE_ID_HEADER = "X-B3-TraceId";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化配置
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 将ServletRequest和ServletResponse转换为HTTP请求和响应
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 从请求头中获取Trace ID,如果不存在,则生成一个新的Trace ID
String traceId = httpRequest.getHeader(TRACE_ID_HEADER);
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString(); // 生成新的Trace ID
}
//1、为当前网关服务加入traceId,TraceIdUtil实际上是MDC封装
TraceIdUtil.setTraceId(traceId);
// 2、将Trace ID添加到请求头中
HttpServletRequest.setHeader(TRACE_ID_HEADER, traceId);
httpResponse.setHeader(TRACE_ID_HEADER, traceId);
// 继续传递请求给下一个过滤器或目标
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 资源清理
}
}
2.2、网关路由转发(过滤器)
设置traceId到MDC(和网关类似)
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
public class TraceIdFilter implements Filter {
private static final String TRACE_ID_HEADER = "X-B3-TraceId";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化配置(如果有必要)
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 将请求转换为HttpServletRequest
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 从请求头中获取Trace ID
String traceId = httpRequest.getHeader(TRACE_ID_HEADER);
// 设置traceId到MDC
TraceIdUtil.setTraceId(traceId);
// 继续将请求传递给下一个过滤器或最终的服务处理
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 清理资源(如果有必要)
}
}
2.3、Dubbo调用(过滤器RpcContext)
- 使用
RpcContext传递Trace ID
2、通过实现org.apache.dubbo.rpc.Filter接口,定义一个过滤器来处理Trace ID的传递和接收。
import org.apache.dubbo.rpc.*;
@Activate(group = {"provider", "consumer"})
public class TraceIdFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// 获取Trace ID
String traceId = RpcContext.getContext().getAttachment("traceId");
if (traceId == null || traceId.isEmpty()) {
// 如果调用者没有传递Trace ID,则从之前过滤器中重新塞入
traceId = todo;
RpcContext.getContext().setAttachment("traceId", traceId);
}
try {
// 继续调用链路
return invoker.invoke(invocation);
} finally {
// 输出Trace ID
System.out.println("Trace ID: " + traceId);
}
}
}
2.4、RocketMq调用(钩子函数HookFunction)
RocketMQ提供了SendMessageHook和ConsumeMessageHook两类钩子函数,分别用于拦截消息的发送和消费过程。
1、 SendMessageHook 用于在消息发送前后自动处理
Trace ID,可以在消息发送之前将Trace ID设置到消息的属性中。
2、 ConsumeMessageHook 用于在消息消费前后拦截消息,并从消息属性中提取Trace ID,用于链路追踪。
- 创建一个自定义的
SendMessageHook,在消息发送前为消息注入Trace ID。
import org.apache.rocketmq.client.hook.SendMessageContext;
import org.apache.rocketmq.client.hook.SendMessageHook;
import java.util.UUID;
public class TraceIdSendMessageHook implements SendMessageHook {
@Override
public String hookName() {
return "TraceIdSendMessageHook";
}
@Override
public void sendMessageBefore(SendMessageContext context) {
// 生成或获取Trace ID
String traceId = UUID.randomUUID().toString();
// 将Trace ID添加到消息属性中
context.getMessage().putUserProperty("traceId", traceId);
System.out.println("Sending message with Trace ID: " + traceId);
}
@Override
public void sendMessageAfter(SendMessageContext context) {
// 发送后处理
System.out.println("Message sent successfully with Trace ID: " + context.getMessage().getUserProperty("traceId"));
}
}
- 创建一个自定义的
ConsumeMessageHook,从消息中提取Trace ID。
import org.apache.rocketmq.client.hook.ConsumeMessageContext;
import org.apache.rocketmq.client.hook.ConsumeMessageHook;
public class TraceIdConsumeMessageHook implements ConsumeMessageHook {
@Override
public String hookName() {
return "TraceIdConsumeMessageHook";
}
@Override
public void consumeMessageBefore(ConsumeMessageContext context) {
// 从消息属性中获取Trace ID
context.getMsgList().forEach(msg -> {
String traceId = msg.getUserProperty("traceId");
//塞入TraceIdUtil
TraceIdUtil.set("traceId", traceId);
});
}
@Override
public void consumeMessageAfter(ConsumeMessageContext context) {
System.out.println("Message consumed successfully.");
}
}
3、注册自定义钩子函数
通过Spring Boot的@Configuration或者自定义的Spring Bean后置处理器(BeanPostProcessor)来自动为Producer和Consumer注册钩子,间接实现自动化的效果
(1)生产者注册`SendMessageHook
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RocketMQProducerConfig {
@Bean
public DefaultMQProducer defaultMQProducer() throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroup");
producer.setNamesrvAddr("127.0.0.1:9876");
// 注册SendMessageHook
producer.getDefaultMQProducerImpl().registerSendMessageHook(new TraceIdSendMessageHook());
producer.start();
return producer;
}
}
(2)消费者注册ConsumeMessageHook
java
复制代码
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RocketMQConsumerConfig {
@Bean
public DefaultMQPushConsumer defaultMQPushConsumer() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroup");
consumer.setNamesrvAddr("127.0.0.1:9876");
// 注册ConsumeMessageHook
consumer.getDefaultMQPushConsumerImpl().registerConsumeMessageHook(new TraceIdConsumeMessageHook());
consumer.subscribe("TopicTest", "*");
consumer.start();
return consumer;
}
}
3、常见的开源工具
这里只作为简单说明,后续有时间具体实际操作一下。
-
Zipkin:一种分布式追踪系统,可以在微服务架构中追踪请求。Zipkin使用HTTP headers传递Trace ID和Span ID。
-
Jaeger:Uber开源的追踪系统,类似于Zipkin,提供分布式上下文传递功能。
-
SkyWalking:Apache开源项目,支持多种语言,并提供端到端的性能监控和链路追踪功能。
-
OpenTelemetry:这是一个新的标准化项目,整合了OpenTracing和OpenCensus,为分布式追踪和指标监控提供统一的API和工具。
参考链接:# 自实现分布式链路追踪 方案&实践