10.2 分布式追踪

响应式系统分布式追踪:Zipkin/Brave 与 OpenTelemetry Reactor 适配、ReactorContext 传递 trace context、手动创建 Span、 baggage 上下文传播,以及响应式 HTTP 客户端/服务端链路串联。

为什么需要分布式追踪

微服务架构中,一个用户请求可能经过网关 → API → 数据库 → 缓存 → 下游服务。每个环节独立记录日志,无法串联成完整链路。

分布式追踪给每个请求分配唯一 Trace ID,各环节记录 Span(操作片段),最终拼凑出完整的请求路径和耗时。

[Trace: abc-123]
  ├─ [Span: gateway]          0ms  ── 5ms
  │    └─ HTTP GET /api/orders
  ├─ [Span: api]              5ms  ── 45ms
  │    ├─ [Span: auth]        5ms  ── 10ms
  │    ├─ [Span: db-query]    12ms ── 35ms
  │    └─ [Span: cache]       36ms ── 40ms
  └─ [Span: response]         45ms ── 50ms

Brave + Zipkin 基础

Spring Cloud Sleuth 已停止维护,Spring Boot 3 起由 Micrometer Tracing 接替;它通过不同的 bridge 对接 Brave 或 OpenTelemetry。eventd v2 使用 Micrometer Tracing + Brave:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
</dependency>

配置:

management:
  tracing:
    sampling:
      probability: 1.0  # 100% 采样(生产降低)
  zipkin:
    tracing:
      endpoint: http://zipkin:9411/api/v2/spans

自动追踪

Spring Boot 自动追踪:

  • HTTP 服务端请求(每个请求一个 Span)
  • HTTP 客户端调用(通过 WebClient 的 observation)
  • R2DBC 查询:需要 classpath 上有 io.r2dbc:r2dbc-proxy,Boot 的 R2dbcObservationAutoConfiguration 才会把连接工厂包成可观测的代理;只加 R2DBC 驱动和连接池不会自动产生查询 Span
  • Redis 操作:默认不追踪,需要在客户端层自行接入 Observation(例如 Lettuce 的 Tracing 支持或自定义 Observation)

手动创建 Span

import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;

@Service
public class OrderService {
    private final Tracer tracer;

    public Mono<Order> createComplex(Order order) {
        Span newSpan = tracer.nextSpan()
            .name("order-complex-create")
            .start();

        return validate(order)
            .flatMap(this::enrich)
            .flatMap(this::saveToDb)
            .flatMap(this::notifyExternal)
            .doOnError(newSpan::error)        // 把异常记为 error 事件,否则链路里看不出失败
            .doFinally(sig -> newSpan.end()); // 成功、失败、取消都会结束 Span
    }
}

tracer.nextSpan() 以当前 Span 为父创建新 Span,doFinally 确保成功、失败、取消都会结束 Span。注意 start() 只是启动计时,并不会把它设为「当前 Span」;要让日志里的 traceId 和它创建的子 Span 继承它,同步代码里用 try (Tracer.SpanInScope ws = tracer.withSpan(span)) { ... },响应式链里则应交给 Observation/context-propagation,或者接受这段代码只记录一个独立 Span。Micrometer Tracing 没有 NewSpanContext 这个类;自动埋点通过 Observation 机制传播上下文。

如果要显式指定父 Span,用 tracer.nextSpan(parentSpan),或者 tracer.spanBuilder().setParent(traceContext).name(...).start()。

ReactorContext 传递 Trace

响应式链跨线程时,Trace Context 通过 Reactor Context 传递:

@GetMapping("/api/orders/{id}")
public Mono<Order> getOrder(@PathVariable Long id) {
    return orderRepo.findById(id)
        .flatMap(order -> {
            // 以当前 Span 为父;没有当前 Span 时 nextSpan() 会新建根 Span
            Span span = tracer.nextSpan().name("enrich-order").start();

            return enrich(order)
                .doFinally(sig -> span.end());
        });
}

tracer.currentTraceContext().context() 在没有活跃追踪时返回 null,直接 .traceId() 会 NPE;需要显式判空,或者直接用 tracer.nextSpan() 让它处理父子关系。Span 本身没有 .parent(...) 方法,指定父级要用 nextSpan(parentSpan) 或 spanBuilder().setParent(traceContext)。

WebClient 链路串联

HTTP 客户端调用需要把当前 Trace 传播到下游服务:

@Service
public class ExternalClient {
    private final WebClient webClient;

    // 注入 Spring Boot 自动配置的 Builder,它已经带上 ObservationRegistry
    public ExternalClient(WebClient.Builder builder) {
        this.webClient = builder.build();
    }

    public Mono<String> callExternal(String url) {
        return webClient.get()
            .uri(url)
            .retrieve()
            .bodyToMono(String.class);
    }
}

关键是使用注入的 WebClient.Builder,不要用 WebClient.create():后者绕过 Spring Boot 的自动配置,不会挂 Observation,traceparent 也就不会自动注入。配置 micrometer-tracing 与对应的 bridge 后,客户端会把 W3C/B3 头带给下游,无需手工加头。手工写 X-B3-TraceId 反而容易和自动注入的 header 冲突、重复或格式不一致,也不利于以后切换到 W3C 标准。

Baggage 上下文

Baggage 是跨 Span 传递的业务上下文。当前的推荐写法是 createBaggageInScope,它返回的 BaggageInScope 同时是 BaggageView:

import io.micrometer.tracing.BaggageInScope;

// 必须运行在已有 Span/Observation 的上下文中:baggage 挂在当前 trace context 上
try (BaggageInScope scope = tracer.createBaggageInScope("userId", "alice")) {
    String value = scope.get();  // "alice"
    // 在这个作用域内发起的下游调用会携带该字段
}

作用域关闭时会把 baggage 恢复成之前的值,而不是「设置一次永久生效」。Baggage.set(...) 和 Tracer.createBaggage(...) 仍然存在,但 Micrometer Tracing 1.5 已把它们标记为 @Deprecated;BaggageField 则是 Brave 更早的底层 API,新代码都不应再直接用。

跨服务传播还需要显式声明字段,否则 baggage 只存在于本进程:

management:
  tracing:
    baggage:
      remote-fields: userId       # 随 B3/W3C 头传给下游
      correlation-fields: userId  # 写入 MDC,供日志 pattern 使用

BaggageInScope 依赖当前 trace context;响应式链切换线程时,需要 Observation/context-propagation 在恢复订阅上下文时重新挂上 baggage。下游如果不是 Micrometer Tracing 体系,最稳妥的做法仍是把值显式放进请求头。

采样策略

生产环境不能 100% 采样(存储和性能开销大):

management:
  tracing:
    sampling:
      probability: 0.1  # 10% 采样

Micrometer Tracing 的 Tracer 没有 Sampler.create(...) 这种工厂方法;条件采样要由具体 bridge 提供。使用 Brave 时实现 brave.sampler.Sampler(isSampled(long traceId))并注册为 Bean,使用 OpenTelemetry 时则配置 SDK 的 Sampler。这类采样器只能看到 traceId/父级采样结果,拿不到业务字段;「只采错误请求」通常要在请求结束后由后端按状态码决定是否保留,而不是在采样阶段判断。

小结

概念说明
Trace完整请求链路,由多个 Span 组成
Span单个操作片段,有开始/结束时间
TraceContext传播标识(TraceId + SpanId)
Baggage跨 Span 业务上下文
自动追踪HTTP 服务端/客户端自动;R2DBC 需 r2dbc-proxy,Redis 需自行接入
手动 Spantracer.nextSpan().name(...).start() + end()
采样management.tracing.sampling.probability 控制

下一节 10.3 结构化日志 :JSON 日志、MDC 桥接、ReactorContext 与日志上下文传递。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 15.3 全书总结与学习路径回顾
  2. 15.2 响应式流处理的演进趋势
  3. 15.1 引擎选型指南