Skip to content

微服务链路追踪:OpenTelemetry + Jaeger/Zipkin,TraceId 透传与采样策略

提出问题

一个请求跨 5 个服务,用户反馈"页面加载慢",你打开日志发现 5 个服务各自打印了耗时,但根本连不起来——不知道是 A 服务慢了还是调用 C 服务时网络延迟了。这就是微服务架构下最常见的排障困境:服务间调用链是断裂的,每个服务只看到自己的局部视角

链路追踪(Distributed Tracing)就是为了解决这个问题的。它通过一个全局唯一的 TraceId 把一次请求经过的所有服务、所有调用串成一条完整的调用链,告诉你:整个请求花了多少时间,每个环节各花了多少,哪个环节最慢,哪个环节出错了。2025 年 OpenTelemetry 已取代 Jaeger/Zipkin 等成为链路追踪的事实标准,"有没有接入链路追踪"已经从加分项变成了微服务基础设施的标配

分析问题

核心概念:Trace、Span、SpanContext

链路追踪的三个核心抽象:

  • Trace:一次完整请求的调用链,由全局唯一的 TraceId 标识。一个 Trace 由多个 Span 组成。
  • Span:Trace 中某个服务节点的处理单元,记录开始时间、结束时间、状态、标签。每个 Span 有 SpanId 和 ParentSpanId,通过 ParentSpanId 构建父子关系形成调用树。
  • SpanContext:TraceId + SpanId + Baggage 的载体,通过 HTTP Header(traceparent / uber-trace-id)或 gRPC Metadata 在服务间传递。

举个例子,用户下单请求经过网关 → 订单服务 → 库存服务 → 支付服务,会生成 4 个 Span,共享同一个 TraceId,通过 ParentSpanId 串联为如下的调用树:

Trace: abc123
├── Span: 网关 (开始 → 结束, 耗时 350ms, parentId=null)
│   ├── Span: 订单服务 (耗时 300ms, parentId=网关.spanId)
│   │   ├── Span: 库存服务 (耗时 100ms, parentId=订单.spanId)
│   │   └── Span: 支付服务 (耗时 150ms, parentId=订单.spanId)

OpenTelemetry 架构:API → SDK → Exporter → Collector

OpenTelemetry(简称 OTel)是 CNCF 的孵化项目,统一了链路追踪、指标和日志的采集规范。它的架构分四层:

  1. API:定义 Trace、Span、Metric、Log 的接口规范,不绑定具体实现。各语言实现同一套 API 接口。
  2. SDK:各语言的具体实现,负责 Span 创建、上下文传递、采样决策、数据导出。Java 端通过 opentelemetry-java 包引入,自动织入 Spring Boot 的 RestTemplate、WebClient、gRPC 等常见框架。
  3. Exporter:将数据发送到后端,支持 Jaeger、Zipkin、Prometheus、Datadog、阿里云 ARMS 等。Exporter 是异步的,不会阻塞业务请求。
  4. Collector:可选的中转层,负责接收数据 → 处理(过滤、采样、属性增强)→ 转发到后端。强烈推荐部署 Collector,它让 SDK 变得轻量(只发数据不做处理),同时提供统一的处理管道。
java
// OpenTelemetry Java SDK 手动创建 Span 示例
Tracer tracer = OpenTelemetrySdk.builder()
    .setTracerProvider(SdkTracerProvider.builder()
        .addSpanProcessor(BatchSpanProcessor.builder(OtlpGrpcSpanExporter.builder()
            .setEndpoint("http://otel-collector:4317")
            .build()).build())
        .build())
    .build().getTracer("order-service");

Span span = tracer.spanBuilder("createOrder")
    .setAttribute("orderId", "ORD-20260720-0001")
    .setAttribute("userId", "user_12345")
    .startSpan();
try (Scope scope = span.makeCurrent()) {
    // 业务逻辑
    Thread.sleep(50); // 模拟处理
    span.setStatus(StatusCode.OK);
} catch (Exception e) {
    span.recordException(e);
    span.setStatus(StatusCode.ERROR, e.getMessage());
    throw e;
} finally {
    span.end();
}

TraceId 跨服务透传的三种场景

TraceId 的透传是链路追踪能否工作的核心。不同通信方式有不同的透传策略:

HTTP 场景(REST/gRPC):OpenTelemetry SDK 自动通过 traceparent Header(W3C TraceContext 标准格式:00-traceId-spanId-01)传递,Spring Boot 应用引入 opentelemetry-spring-boot-starter 后零配置。

消息队列场景(Kafka/RabbitMQ):TraceId 无法通过 MDC 自动传递,需要手动在消息体里携带 TraceId,消费者端从消息中提取并重新设置 MDC。

java
// 生产者:在消息头中携带 TraceId
Span span = tracer.spanBuilder("sendOrderMessage").startSpan();
try {
    String traceId = span.getSpanContext().getTraceId();
    ProducerRecord<String, String> record = new ProducerRecord<>("order-topic", messageBody);
    record.headers().add("traceparent", ("00-" + traceId + "-" + span.getSpanContext().getSpanId() + "-01").getBytes());
    kafkaTemplate.send(record);
} finally {
    span.end();
}

// 消费者:从消息头恢复 TraceId
@KafkaListener(topics = "order-topic")
public void onMessage(ConsumerRecord<String, String> record) {
    String traceparent = new String(record.headers().lastHeader("traceparent").value());
    // 解析 traceparent 并设置到当前线程的 MDC 上下文
    SpanContext extracted = W3CTraceContextPropagator.getInstance().extract(
        Context.current(), record.headers(), (headers, key) -> {
            Header h = headers.lastHeader(key);
            return h != null ? new String(h.value()) : null;
        });
    try (Scope scope = Span.fromContext(extracted).makeCurrent()) {
        // 业务处理
    }
}

线程池场景:ThreadPoolExecutor 的线程复用会导致 MDC 中的 TraceId 被污染——线程 A 处理完请求后 MDC 没清干净,线程 B 复用时拿到了线程 A 的 TraceId。解决方案是用装饰器模式为每个任务单独设置 MDC 上下文:

java
public class TraceAwareThreadPool extends ThreadPoolExecutor {
    @Override
    public void execute(Runnable command) {
        // 提交任务时记录当前线程的 TraceContext
        Context currentContext = Context.current();
        super.execute(() -> {
            // 在新的工作线程中恢复 TraceContext
            try (Scope scope = currentContext.makeCurrent()) {
                command.run();
            }
        });
    }
}

采样策略:全量 vs 概率 vs 自适应

一个日均 1 亿请求的微服务系统,如果全量采样,每天存储量可达 200GB+,远超多数团队预算。因此采样是链路追踪落地必须考虑的问题。

采样策略原理适用场景优缺点
头部采样(Head-based)请求入口决定是否采样,后续所有 Span 跟随QPS 稳定、流量均匀的系统简单高效,但可能错过慢请求
尾部采样(Tail-based)请求完成后根据延迟/错误率决定是否保留需要精确抓取慢请求和异常存储开销大,能抓到关键异常
概率采样固定比例(如 1%、10%)流量大的稳定系统最省资源,但采样比例固定不够灵活
自适应采样根据错误率动态调整,错误率升高时自动提高采样比例生产环境推荐实现复杂,但效果最好

生产推荐方案分层采样——QPS 低于 5000 的接口全量采样,QPS 超过 50000 的接口采样率降到 0.1%,同时配置错误请求自动提升采样率(错误采样率 100%)。OpenTelemetry Collector 的 Tail Sampling Processor 可以基于错误状态、延迟阈值等条件精确控制哪些 Span 保留。

实战:OpenTelemetry Collector 采样配置

yaml
# otel-collector-config.yaml — 尾部采样配置
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  tail_sampling:
    decision_wait: 30s          # 等待 30 秒后再做采样决策
    num_traces: 100000          # 最多同时追踪 10 万条 Trace
    expected_new_traces_per_sec: 1000
    policies:
      - name: error-policy      # 错误请求 100% 保留
        type: status_code
        status_code:
          status_codes:
            - ERROR
      - name: slow-policy       # 延迟超过 500ms 的请求 100% 保留
        type: latency
        latency:
          threshold_ms: 500
      - name: prob-policy       # 剩下的请求按 1% 概率采样
        type: probabilistic
        probabilistic:
          sampling_percentage: 1

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [tail_sampling]
      exporters: [jaeger]

总结

关键点清单

  1. TraceId 透传是链路追踪的基础——HTTP 用 traceparent Header 自动传递,MQ 和线程池场景需要手动处理,踩过的坑最多。
  2. OpenTelemetry Collector 是必选项——不要在 SDK 中直接配 Jaeger/Zipkin 地址,统一走 Collector 做采样、过滤、缓冲,减少 SDK 耦合。
  3. 采样策略按接口分层——低 QPS 全量采样,高 QPS 概率采样,错误/慢请求提权采样,这是生产级链路追踪的标配实践。
  4. 链路追踪与日志打通——日志中输出 traceId,Kibana 搜 traceId:abc123 就能看到整个请求的日志,这才是链路追踪的真正价值——不是"看调用链",而是"用调用链关联日志"。

参考:OpenTelemetry 官方文档 · Trace Context Propagation,Jaeger 官方 · Architecture & Sampling,OpenTelemetry Collector · Tail Sampling Processor

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。