摘要:从一次"用户投诉查不到日志"的故障排查出发,深度剖析分布式链路追踪的核心原理。通过TraceId和SpanId的生成规则、MDC日志上下文的传递机制、以及跨线程/跨服务的链路传递方案,揭秘如何用一个TraceId串联起微服务调用链路、如何在几十万条日志中秒级定位问题、以及SkyWalking全链路监控的实现原理。配合时序图展示链路传递流程,给出生产环境的完整实现方案。
💥 翻车现场
周一早上,客服主管在群里@了哈吉米。
客服主管:@哈吉米 用户10086反馈下单失败,你查查日志
哈吉米:好的,我看看
打开日志文件(100GB,1000万行):
2024-10-07 09:15:32.123 [http-nio-8080-exec-1] INFO - 用户下单,userId=10086
2024-10-07 09:15:32.234 [http-nio-8080-exec-5] INFO - 查询库存,productId=1001
2024-10-07 09:15:32.345 [http-nio-8080-exec-3] INFO - 用户下单,userId=10087
2024-10-07 09:15:32.456 [http-nio-8080-exec-1] ERROR - 库存扣减失败
2024-10-07 09:15:32.567 [http-nio-8080-exec-7] INFO - 用户下单,userId=10088
... (几百万条日志混在一起)
哈吉米:"卧槽,userId=10086的日志和其他用户的日志混在一起,根本找不到完整的调用链路!"
问题:
- ❌ 不知道是哪个请求的日志
- ❌ 多个线程并发,日志交叉
- ❌ 调用链路断裂(订单服务 → 库存服务 → 支付服务,日志分散)
哈吉米:"怎么快速找到用户10086这次下单的所有日志?"
南北绿豆和阿西噶阿西来了。
南北绿豆:"这就需要分布式链路追踪!每个请求生成一个唯一的TraceId,所有日志都带上这个TraceId。"
哈吉米:"TraceId是啥?"
阿西噶阿西:"来,我给你讲讲。"
🤔 什么是TraceId和SpanId?
核心概念
TraceId(链路ID):
- 一次请求的唯一标识
- 贯穿整个调用链路
- 用于关联所有相关日志
SpanId(跨度ID):
- 一次服务调用的唯一标识
- 一个Trace包含多个Span
- 用于标识调用层级
调用链路示例
用户请求:创建订单
调用链路:
用户 → API网关 → 订单服务 → 库存服务 → 支付服务
↓ ↓ ↓
订单入库 扣减库存 扣减余额
TraceId:trace-123456(整个链路唯一)
SpanId:
- API网关:span-1
- 订单服务:span-2(parent=span-1)
- 库存服务:span-3(parent=span-2)
- 支付服务:span-4(parent=span-2)
树形结构:
trace-123456
├─ span-1 (API网关)
│ └─ span-2 (订单服务)
│ ├─ span-3 (库存服务)
│ └─ span-4 (支付服务)
南北绿豆:"有了TraceId,grep一下就能找到这次请求的所有日志!"
🎯 如何实现TraceId?
方案1:基础版(单体应用)
/**
* TraceId工具类
*/
public class TraceContext {
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
/**
* 生成TraceId
*/
public static String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 设置TraceId
*/
public static void setTraceId(String traceId) {
TRACE_ID.set(traceId);
MDC.put("traceId", traceId); // 设置到日志MDC
}
/**
* 获取TraceId
*/
public static String getTraceId() {
return TRACE_ID.get();
}
/**
* 清理TraceId
*/
public static void clear() {
TRACE_ID.remove();
MDC.remove("traceId");
}
}
拦截器设置TraceId
@Component
public class TraceIdInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, ...) {
// 1. 尝试从请求头获取TraceId(可能是从网关传来的)
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null) {
// 2. 生成新的TraceId
traceId = TraceContext.generateTraceId();
}
// 3. 设置到ThreadLocal和MDC
TraceContext.setTraceId(traceId);
return true;
}
@Override
public void afterCompletion(...) {
// 清理TraceId
TraceContext.clear();
}
}
Logback配置
<!-- logback-spring.xml -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n
↑ 从MDC取traceId
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
日志输出:
2024-10-07 09:15:32.123 [http-nio-8080-exec-1] [a1b2c3d4] INFO - 用户下单,userId=10086
2024-10-07 09:15:32.234 [http-nio-8080-exec-1] [a1b2c3d4] INFO - 查询库存,productId=1001
2024-10-07 09:15:32.456 [http-nio-8080-exec-1] [a1b2c3d4] ERROR - 库存扣减失败
↑ 同一个TraceId
快速查询:
# 查询TraceId=a1b2c3d4的所有日志
grep "a1b2c3d4" application.log
# 输出:这次请求的完整日志
哈吉米:"卧槽,有了TraceId,秒级定位问题!"
🎯 跨服务传递TraceId
问题:微服务调用链路
用户请求 → 订单服务 → 库存服务 → 支付服务
问题:
- 订单服务生成TraceId=a1b2c3d4
- 调用库存服务时,库存服务的日志没有TraceId ❌
- 调用支付服务时,支付服务的日志没有TraceId ❌
结果:链路断裂
解决方案:HTTP请求头传递
/**
* Feign拦截器(自动传递TraceId)
*/
@Component
public class TraceIdFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 从当前线程获取TraceId
String traceId = TraceContext.getTraceId();
if (traceId != null) {
// 设置到HTTP请求头
template.header("X-Trace-Id", traceId);
}
}
}
/**
* RestTemplate拦截器(自动传递TraceId)
*/
@Component
public class TraceIdRestTemplateInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ...) {
// 从当前线程获取TraceId
String traceId = TraceContext.getTraceId();
if (traceId != null) {
// 设置到HTTP请求头
request.getHeaders().add("X-Trace-Id", traceId);
}
return execution.execute(request, body);
}
}
下游服务接收TraceId
/**
* 所有服务的拦截器
*/
@Component
public class TraceIdInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, ...) {
// 从请求头获取TraceId
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null) {
// 没有TraceId,生成新的(说明是第一个服务)
traceId = TraceContext.generateTraceId();
}
// 设置到当前线程
TraceContext.setTraceId(traceId);
return true;
}
@Override
public void afterCompletion(...) {
TraceContext.clear();
}
}
完整链路传递
sequenceDiagram
participant User as 用户
participant Gateway as API网关
participant OrderSvc as 订单服务
participant StockSvc as 库存服务
participant PaySvc as 支付服务
User->>Gateway: 1. 创建订单
Note over Gateway: 生成TraceId=a1b2c3d4
Gateway->>Gateway: MDC.put("traceId", "a1b2c3d4")
Note over Gateway: 日志:[a1b2c3d4] 网关收到请求
Gateway->>OrderSvc: 2. HTTP请求(Header: X-Trace-Id=a1b2c3d4)
Note over OrderSvc: 从请求头读取TraceId
OrderSvc->>OrderSvc: MDC.put("traceId", "a1b2c3d4")
Note over OrderSvc: 日志:[a1b2c3d4] 创建订单
OrderSvc->>StockSvc: 3. HTTP请求(Header: X-Trace-Id=a1b2c3d4)
Note over StockSvc: 从请求头读取TraceId
StockSvc->>StockSvc: MDC.put("traceId", "a1b2c3d4")
Note over StockSvc: 日志:[a1b2c3d4] 扣减库存
OrderSvc->>PaySvc: 4. HTTP请求(Header: X-Trace-Id=a1b2c3d4)
Note over PaySvc: 从请求头读取TraceId
PaySvc->>PaySvc: MDC.put("traceId", "a1b2c3d4")
Note over PaySvc: 日志:[a1b2c3d4] 扣减余额
Note over Gateway,PaySvc: 所有服务的日志都有TraceId=a1b2c3d4<br/>可以快速串联整个调用链路
查询日志:
# 网关日志
grep "a1b2c3d4" gateway.log
2024-10-07 09:15:32.001 [a1b2c3d4] INFO - 网关收到请求
# 订单服务日志
grep "a1b2c3d4" order-service.log
2024-10-07 09:15:32.123 [a1b2c3d4] INFO - 创建订单
# 库存服务日志
grep "a1b2c3d4" stock-service.log
2024-10-07 09:15:32.234 [a1b2c3d4] INFO - 扣减库存
2024-10-07 09:15:32.345 [a1b2c3d4] ERROR - 库存不足
# 一眼看出:库存服务报错导致下单失败
哈吉米:"有了TraceId,几秒钟就定位到问题了!"
🤔 跨线程传递TraceId
问题:线程池场景
@Service
public class OrderService {
@Autowired
private ThreadPoolExecutor executor;
public void createOrder(Order order) {
String traceId = TraceContext.getTraceId(); // traceId=a1b2c3d4
log.info("开始创建订单"); // [a1b2c3d4] 开始创建订单
// 提交到线程池
executor.execute(() -> {
String threadTraceId = TraceContext.getTraceId(); // null ❌
log.info("异步处理订单"); // [] 异步处理订单(没有TraceId)
});
}
}
问题:ThreadLocal不能传递到子线程。
解决方案1:手动传递
public void createOrder(Order order) {
String traceId = TraceContext.getTraceId(); // 保存TraceId
executor.execute(() -> {
try {
// 手动设置TraceId
TraceContext.setTraceId(traceId);
log.info("异步处理订单"); // [a1b2c3d4] 异步处理订单 ✅
} finally {
TraceContext.clear();
}
});
}
解决方案2:TransmittableThreadLocal(推荐)
// 引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.3</version>
</dependency>
// 改用TransmittableThreadLocal
public class TraceContext {
private static final TransmittableThreadLocal<String> TRACE_ID =
new TransmittableThreadLocal<>();
// 其他方法不变
}
// 包装线程池
ThreadPoolExecutor rawExecutor = new ThreadPoolExecutor(...);
ExecutorService executor = TtlExecutors.getTtlExecutorService(rawExecutor);
// 使用(自动传递)
executor.execute(() -> {
log.info("异步处理订单"); // [a1b2c3d4] 自动有TraceId ✅
});
🎯 完整实现方案
步骤1:TraceId工具类
@Component
public class TraceIdUtil {
private static final TransmittableThreadLocal<String> TRACE_ID =
new TransmittableThreadLocal<>();
/**
* 生成TraceId
*/
public static String generate() {
// 格式:服务名-时间戳-随机数
return "order-" + System.currentTimeMillis() + "-" +
ThreadLocalRandom.current().nextInt(10000);
}
/**
* 设置TraceId
*/
public static void set(String traceId) {
TRACE_ID.set(traceId);
MDC.put("traceId", traceId);
}
/**
* 获取TraceId
*/
public static String get() {
return TRACE_ID.get();
}
/**
* 清理TraceId
*/
public static void clear() {
TRACE_ID.remove();
MDC.remove("traceId");
}
}
步骤2:Filter拦截器
@Component
@WebFilter(urlPatterns = "/*", filterName = "traceIdFilter")
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
try {
// 1. 尝试从请求头获取TraceId
String traceId = req.getHeader("X-Trace-Id");
if (traceId == null || traceId.isEmpty()) {
// 2. 生成新的TraceId
traceId = TraceIdUtil.generate();
}
// 3. 设置TraceId
TraceIdUtil.set(traceId);
// 4. 设置到响应头(方便前端查看)
HttpServletResponse resp = (HttpServletResponse) response;
resp.setHeader("X-Trace-Id", traceId);
// 5. 继续处理
chain.doFilter(request, response);
} finally {
// 6. 清理TraceId
TraceIdUtil.clear();
}
}
}
步骤3:Feign拦截器(跨服务传递)
@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor traceIdInterceptor() {
return template -> {
// 从当前线程获取TraceId
String traceId = TraceIdUtil.get();
if (traceId != null) {
// 设置到Feign请求头
template.header("X-Trace-Id", traceId);
}
};
}
}
步骤4:MQ消息传递
/**
* RabbitMQ生产者(发送消息时带上TraceId)
*/
@Service
public class OrderMessageProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendOrderMessage(Order order) {
OrderMessage message = new OrderMessage();
message.setOrder(order);
message.setTraceId(TraceIdUtil.get()); // 携带TraceId
rabbitTemplate.convertAndSend("order.queue", message);
}
}
/**
* RabbitMQ消费者(接收消息时设置TraceId)
*/
@Component
public class OrderMessageConsumer {
@RabbitListener(queues = "order.queue")
public void handleMessage(OrderMessage message) {
try {
// 设置TraceId
TraceIdUtil.set(message.getTraceId());
// 处理消息
log.info("处理订单消息"); // [a1b2c3d4] 处理订单消息
} finally {
TraceIdUtil.clear();
}
}
}
🎯 如何快速排查问题?
场景1:通过TraceId查询完整链路
# 用户反馈:下单失败,TraceId=a1b2c3d4
# 1. 网关日志
grep "a1b2c3d4" gateway.log
2024-10-07 09:15:32.001 [a1b2c3d4] INFO - 请求进入,path=/order/create
# 2. 订单服务日志
grep "a1b2c3d4" order-service.log
2024-10-07 09:15:32.123 [a1b2c3d4] INFO - 创建订单,userId=10086
2024-10-07 09:15:32.234 [a1b2c3d4] INFO - 调用库存服务
# 3. 库存服务日志
grep "a1b2c3d4" stock-service.log
2024-10-07 09:15:32.345 [a1b2c3d4] ERROR - 库存不足,productId=1001
# 定位问题:库存服务报错,库存不足
场景2:通过userId查询历史TraceId
/**
* 记录用户的TraceId(Redis)
*/
@Component
public class UserTraceRecorder {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void record(Long userId, String traceId) {
String key = "user:trace:" + userId;
// 记录最近10个TraceId(List)
redisTemplate.opsForList().leftPush(key, traceId);
redisTemplate.opsForList().trim(key, 0, 9); // 只保留10个
redisTemplate.expire(key, 7, TimeUnit.DAYS);
}
public List<String> getTraceIds(Long userId) {
String key = "user:trace:" + userId;
return redisTemplate.opsForList().range(key, 0, -1);
}
}
// 拦截器中记录
@Override
public boolean preHandle(HttpServletRequest request, ...) {
String traceId = TraceIdUtil.generate();
TraceIdUtil.set(traceId);
// 记录用户的TraceId
Long userId = getCurrentUserId(request);
if (userId != null) {
userTraceRecorder.record(userId, traceId);
}
return true;
}
查询:
# 用户反馈:昨天下单失败了,但不知道具体时间
# 1. 查询用户最近的TraceId
redis-cli> LRANGE user:trace:10086 0 -1
1) "a1b2c3d4"
2) "b2c3d4e5"
3) "c3d4e5f6"
...
# 2. 逐个查询日志,找到失败的那次
grep "a1b2c3d4" *.log
grep "b2c3d4e5" *.log
...
🎯 升级版:SpanId和调用链路
完整的链路追踪
/**
* 链路追踪上下文
*/
public class TraceContext {
private static final TransmittableThreadLocal<String> TRACE_ID = new TransmittableThreadLocal<>();
private static final TransmittableThreadLocal<String> SPAN_ID = new TransmittableThreadLocal<>();
private static final TransmittableThreadLocal<String> PARENT_SPAN_ID = new TransmittableThreadLocal<>();
public static void setTraceInfo(String traceId, String spanId, String parentSpanId) {
TRACE_ID.set(traceId);
SPAN_ID.set(spanId);
PARENT_SPAN_ID.set(parentSpanId);
MDC.put("traceId", traceId);
MDC.put("spanId", spanId);
MDC.put("parentSpanId", parentSpanId);
}
public static String generateSpanId() {
return UUID.randomUUID().toString().substring(0, 8);
}
}
Feign拦截器:
@Component
public class TraceFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String traceId = TraceContext.getTraceId();
String spanId = TraceContext.generateSpanId(); // 生成新的SpanId
String parentSpanId = TraceContext.getSpanId(); // 当前SpanId作为父SpanId
template.header("X-Trace-Id", traceId);
template.header("X-Span-Id", spanId);
template.header("X-Parent-Span-Id", parentSpanId);
}
}
日志输出:
# 订单服务
[traceId=a1b2c3d4, spanId=span-1, parentSpanId=null] 创建订单
# 库存服务
[traceId=a1b2c3d4, spanId=span-2, parentSpanId=span-1] 扣减库存
# 支付服务
[traceId=a1b2c3d4, spanId=span-3, parentSpanId=span-1] 扣减余额
调用关系:
trace-a1b2c3d4
└─ span-1 (订单服务)
├─ span-2 (库存服务, parent=span-1)
└─ span-3 (支付服务, parent=span-1)
🎯 对接SkyWalking全链路监控
什么是SkyWalking?
功能:
- 自动采集TraceId和SpanId
- 可视化调用链路
- 性能分析(每个服务耗时)
- 告警(慢请求、错误率)
使用SkyWalking
# 1. 启动SkyWalking OAP服务
java -jar skywalking-oap-server.jar
# 2. 启动SkyWalking UI
java -jar skywalking-webapp.jar
# 3. 应用启动时加入agent
java -javaagent:/path/to/skywalking-agent.jar \
-Dskywalking.agent.service_name=order-service \
-jar order-service.jar
效果:
- ✅ 自动采集链路(不用改代码)
- ✅ 可视化调用链路(UI界面)
- ✅ 性能分析(每个服务的耗时)
- ✅ 慢请求告警
SkyWalking UI界面
调用链路:
订单服务 (100ms)
├─ 库存服务 (30ms)
├─ 支付服务 (50ms)
└─ 通知服务 (20ms)
性能分析:
- 总耗时:100ms
- 订单服务自身:10ms
- 调用下游:90ms
慢请求Top 10:
1. traceId=a1b2c3d4, 耗时5.2秒, 库存服务超时
2. traceId=b2c3d4e5, 耗时3.8秒, 支付服务超时
...
🎓 面试标准答案
题目:什么是分布式链路追踪?如何实现?
答案:
定义:
- TraceId:一次请求的唯一标识,贯穿整个调用链路
- SpanId:一次服务调用的唯一标识
- 作用:串联分布式系统的所有日志,快速定位问题
实现方案:
1. 自研方案
- 生成TraceId(UUID或雪花算法)
- 存储到ThreadLocal和MDC
- HTTP请求头传递(X-Trace-Id)
- Feign/RestTemplate拦截器自动传递
- 跨线程用TransmittableThreadLocal
2. 开源方案
- SkyWalking(推荐)
- Zipkin
- Jaeger
- CAT(大众点评)
核心流程:
- 入口生成TraceId
- 所有日志带上TraceId
- 跨服务通过HTTP Header传递
- 跨线程通过TTL传递
- 日志系统统一收集(ELK)
- 通过TraceId查询完整链路
题目:如何快速排查分布式系统的问题?
答案:
步骤:
-
获取TraceId
- 用户反馈时提供(响应头)
- 从Redis查询用户最近的TraceId
- 从日志系统搜索(Kibana)
-
查询完整链路
- grep TraceId查所有服务日志
- SkyWalking UI查看调用链路
-
定位慢服务
- 看每个服务的耗时
- 找出耗时最长的服务
-
定位具体问题
- 查看错误日志
- 分析SQL慢查询
- 检查外部接口调用
最佳实践:
- 所有接口返回TraceId(响应头)
- 用户反馈时提供TraceId
- 日志统一收集到ELK
- 接入SkyWalking全链路监控
🎉 结束语
一周后,哈吉米把全链路追踪系统上线了。
哈吉米:"现在排查问题太方便了!用户给个TraceId,几秒钟就能找到所有日志!"
南北绿豆:"对,分布式链路追踪是微服务的标配,没有它根本无法排查问题。"
阿西噶阿西:"记住:TraceId贯穿整个链路,跨服务通过HTTP Header传递,跨线程用TTL。"
哈吉米:"还有SkyWalking,可视化调用链路,太方便了!"
南北绿豆:"对,生产环境强烈推荐接入SkyWalking或Zipkin!"
记忆口诀:
TraceId链路唯一标,贯穿微服务全链路
ThreadLocal加MDC,日志自动带traceId
跨服务Header传,跨线程TTL传
Feign拦截自动加,MQ消息也要带
SkyWalking可视化,排查问题快准狠
希望这篇文章能帮你彻底理解分布式链路追踪!记住:微服务必须有TraceId,否则无法排查问题!💪