分布式链路ID是什么?如何快速排查问题?

摘要:从一次"用户投诉查不到日志"的故障排查出发,深度剖析分布式链路追踪的核心原理。通过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(大众点评)

核心流程

  1. 入口生成TraceId
  2. 所有日志带上TraceId
  3. 跨服务通过HTTP Header传递
  4. 跨线程通过TTL传递
  5. 日志系统统一收集(ELK)
  6. 通过TraceId查询完整链路

题目:如何快速排查分布式系统的问题?

答案

步骤

  1. 获取TraceId

    • 用户反馈时提供(响应头)
    • 从Redis查询用户最近的TraceId
    • 从日志系统搜索(Kibana)
  2. 查询完整链路

    • grep TraceId查所有服务日志
    • SkyWalking UI查看调用链路
  3. 定位慢服务

    • 看每个服务的耗时
    • 找出耗时最长的服务
  4. 定位具体问题

    • 查看错误日志
    • 分析SQL慢查询
    • 检查外部接口调用

最佳实践

  • 所有接口返回TraceId(响应头)
  • 用户反馈时提供TraceId
  • 日志统一收集到ELK
  • 接入SkyWalking全链路监控

🎉 结束语

一周后,哈吉米把全链路追踪系统上线了。

哈吉米:"现在排查问题太方便了!用户给个TraceId,几秒钟就能找到所有日志!"

南北绿豆:"对,分布式链路追踪是微服务的标配,没有它根本无法排查问题。"

阿西噶阿西:"记住:TraceId贯穿整个链路,跨服务通过HTTP Header传递,跨线程用TTL。"

哈吉米:"还有SkyWalking,可视化调用链路,太方便了!"

南北绿豆:"对,生产环境强烈推荐接入SkyWalking或Zipkin!"


记忆口诀

TraceId链路唯一标,贯穿微服务全链路
ThreadLocal加MDC,日志自动带traceId
跨服务Header传,跨线程TTL传
Feign拦截自动加,MQ消息也要带
SkyWalking可视化,排查问题快准狠


希望这篇文章能帮你彻底理解分布式链路追踪!记住:微服务必须有TraceId,否则无法排查问题!💪