链路追踪

在微服务架构中,一次用户请求会跨越多个服务。链路追踪通过在所有服务间传递 TraceId / SpanId,将散落各处的日志串联成完整的调用链,方便定位性能瓶颈和故障。

核心概念

用户请求
   │
   ▼  TraceId: abc123  SpanId: 001
[订单服务]─────────────────────────────────────────
   │ 调用                 Span 001(根 Span)
   ▼  TraceId: abc123  SpanId: 002  ParentSpanId: 001
[库存服务]─────────────────────
   │   Span 002(子 Span)
   ▼  TraceId: abc123  SpanId: 003  ParentSpanId: 001
[支付服务]─────────────────────
       Span 003(子 Span)
概念说明
TraceId全局唯一,贯穿整条请求链路
SpanId每个服务调用的唯一标识
ParentSpanId父 Span,描述调用关系
Span一次具体操作(HTTP 调用、DB 查询、MQ 发送)的时间记录
Baggage随链路传播的业务数据(如 userId)

Spring Boot 3.x:Micrometer Tracing

Spring Boot 3.x 用 Micrometer Tracing 替代了 Spring Cloud Sleuth(已归档)。

依赖

// Micrometer Tracing 核心
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
 
// 上报到 Zipkin
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
 
// 若上报到 OTLP / Jaeger,改用:
// implementation 'io.micrometer:micrometer-tracing-bridge-otel'
// implementation 'io.opentelemetry:opentelemetry-exporter-otlp'

基础配置

management:
  tracing:
    sampling:
      probability: 1.0     # 采样率:1.0 = 100%(开发),生产建议 0.1~0.01
  zipkin:
    tracing:
      endpoint: http://zipkin:9411/api/v2/spans
 
logging:
  pattern:
    # 日志中自动插入 traceId 和 spanId
    level: "%5p [${spring.application.name},%X{traceId},%X{spanId}]"

配置后,每条日志自动带上链路信息:

INFO  [order-service,abc123def456,001122aabb] c.e.OrderService - 创建订单 #1001

日志与链路关联(MDC)

Micrometer Tracing 自动将 traceId / spanId 写入 MDC(Mapped Diagnostic Context),Logback / Log4j2 在输出日志时自动读取。

自定义 MDC 字段(如请求来源、用户 ID):

@Component
public class TraceFilter extends OncePerRequestFilter {
 
    private final Tracer tracer;
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        // 从请求头提取业务字段写入 MDC
        String userId = request.getHeader("X-User-Id");
        if (StringUtils.hasText(userId)) {
            MDC.put("userId", userId);
        }
 
        // 将 traceId 写回响应头,方便前端关联
        Span currentSpan = tracer.currentSpan();
        if (currentSpan != null) {
            response.setHeader("X-Trace-Id",
                currentSpan.context().traceId());
        }
 
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
}

Logback 配置输出自定义 MDC:

<!-- logback-spring.xml -->
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level
  [%X{traceId},%X{spanId}] [userId:%X{userId}]
  %logger{36} - %msg%n</pattern>

日志配置详见 日志


自定义 Span

在关键业务逻辑上手动创建 Span,精确追踪耗时:

@Service
@RequiredArgsConstructor
public class OrderService {
 
    private final Tracer tracer;
 
    public Order createOrder(CreateOrderRequest req) {
        // 方式一:手动管理 Span
        Span span = tracer.nextSpan().name("order.create").start();
        span.tag("userId", req.getUserId());
        span.tag("productCount", String.valueOf(req.getItems().size()));
 
        try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
            Order order = doCreate(req);
            span.tag("orderId", order.getId().toString());
            return order;
        } catch (Exception e) {
            span.error(e);
            throw e;
        } finally {
            span.end();
        }
    }
 
    // 方式二:注解(需引入 micrometer-tracing 注解支持)
    @NewSpan("inventory.check")
    public boolean checkInventory(@SpanTag("productId") Long productId) {
        return inventoryRepo.hasStock(productId);
    }
}

Baggage(跨服务传递业务数据)

Baggage 随 HTTP Header 自动跨服务传播,适合透传 userId、租户 ID 等:

// 写入 Baggage(在入口服务)
@Component
public class UserBaggageFilter extends OncePerRequestFilter {
 
    private final BaggageManager baggageManager;
 
    @Override
    protected void doFilterInternal(...) throws ServletException, IOException {
        String userId = request.getHeader("X-User-Id");
        if (StringUtils.hasText(userId)) {
            baggageManager.createBaggage("userId").set(userId);
        }
        chain.doFilter(request, response);
    }
}
 
// 读取 Baggage(在任意下游服务)
@Service
public class AuditService {
 
    private final BaggageManager baggageManager;
 
    public void audit(String action) {
        String userId = baggageManager.getBaggage("userId").get();
        log.info("用户 {} 执行了 {}", userId, action);
    }
}
# 允许传播的 Baggage key(需显式声明)
management:
  tracing:
    baggage:
      remote-fields: userId, tenantId
      correlation:
        fields: userId, tenantId    # 同时写入 MDC

异步与线程池中的链路传递

默认情况下,新线程不会继承父线程的 Span 上下文,需使用 Micrometer 提供的包装器:

@Configuration
public class AsyncConfig implements AsyncConfigurer {
 
    private final ObservationRegistry observationRegistry;
 
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(20);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        // 包装 Executor,自动传递 Span 上下文
        return new ContextPropagatingTaskDecorator(executor);
    }
}

异步与线程池详见 异步与线程池


部署 Zipkin

# docker-compose.yml
services:
  zipkin:
    image: openzipkin/zipkin:latest
    ports:
      - "9411:9411"

访问 http://localhost:9411 查看调用链瀑布图。

生产环境建议 Zipkin + Elasticsearch 持久化存储(默认内存存储会丢失)。


采样策略

management:
  tracing:
    sampling:
      probability: 0.1    # 生产环境采 10%,降低性能开销和存储压力

自定义采样(对慢请求或错误请求 100% 采样):

@Bean
public Sampler sampler() {
    return (traceContext, sampledFlag) -> {
        // 已被标记跳过 → 继续跳过
        if (Boolean.FALSE.equals(sampledFlag)) return false;
        // 错误请求强制采样
        return true;
    };
}

与监控指标的结合

Micrometer Tracing 与 Micrometer Metrics 共享 ObservationRegistry,一次 @Observed 注解同时产生指标和链路数据:

@Observed(name = "order.create",
          contextualName = "creating-order",
          lowCardinalityKeyValues = {"type", "standard"})
public Order createOrder(CreateOrderRequest req) {
    return doCreate(req);
}

指标采集和监控详见 指标采集Actuator监控


相关链接