为什么需要分布式追踪
微服务架构中,一个用户请求可能经过网关 → 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 需自行接入 |
| 手动 Span | tracer.nextSpan().name(...).start() + end() |
| 采样 | management.tracing.sampling.probability 控制 |
下一节 10.3 结构化日志 :JSON 日志、MDC 桥接、ReactorContext 与日志上下文传递。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。