4. 全链路追踪技术:OpenTelemetry协议介绍、Trace/Span/Context模型、分布式上下文传播机制

聊到全链路监控,有个东西绕不开——分布式追踪。说白了,就是一次请求在多服务间穿梭时,我们得知道它去了哪、干了啥、花了多久。

我刚开始做监控那会儿,各家都有自己的追踪协议。Zipkin 用 B3,Jaeger 用 Jaeger 格式,SkyWalking 又搞一套。你想想看,一个公司内部可能还好,一旦涉及跨团队、跨技术栈,那叫一个乱。后来 OpenTelemetry 出现了,算是把这事儿给统一了。

4.1 OpenTelemetry 协议概览

OpenTelemetry,简称 OTel,是 CNCF 下的一个可观测性标准。它不是某个公司的私有协议,而是社区共同维护的规范。

OTel 主要做三件事:

  • 生成数据:通过 SDK 自动或手动采集 Trace、Metrics、Logs
  • 传输数据:定义了一套协议(OTLP),把数据从应用送到后端
  • 导出数据:支持多种后端,比如 Prometheus、Jaeger、Grafana

我个人习惯把 OTel 看作一个「数据管道」。你只需要在代码里埋好点,剩下的传输、转换、导出,它都帮你搞定。

核心要点:OTel 不绑定任何后端。你今天用 Jaeger,明天想换 Datadog,改个配置就行,代码不用动。

4.2 Trace / Span / Context 模型

这三个概念是整个分布式追踪的基石。我一个个说。

4.2.1 Trace

Trace 代表一次完整的请求链路。比如用户下单,这个请求可能经过网关、订单服务、库存服务、支付服务。整个路径就是一个 Trace。

每个 Trace 有一个全局唯一的 TraceID。所有参与的服务,都带着这个 ID 打日志、上报数据。这样我们就能把散落在各处的信息串起来。

4.2.2 Span

Span 是 Trace 里的最小工作单元。一次 RPC 调用、一次数据库查询、一次消息发送,都可以是一个 Span。

每个 Span 包含:

  • SpanID:当前 Span 的唯一标识
  • ParentSpanID:父 Span 的 ID,用来构建树形结构
  • OperationName:操作名称,比如 "HTTP GET /api/order"
  • StartTime / EndTime:开始和结束时间
  • Attributes:自定义属性,比如请求参数、状态码
  • Events:事件记录,比如异常堆栈、缓存命中

我记得有一次线上排查慢请求,发现某个 Span 耗时异常高。点进去一看,Attributes 里记录了 SQL 语句,原来是没走索引。这种场景,Span 的 Attributes 就是救命稻草。

4.2.3 Context

Context 是 Span 的「快递员」。它负责把 TraceID、SpanID 等信息,从一个服务传到下一个服务。

Context 里通常包含:

  • TraceID
  • 当前 SpanID
  • 其他传播属性(比如 Baggage)

你想想看,服务 A 调用服务 B,如果 Context 没传过去,服务 B 就不知道自己是哪个 Trace 的一部分。那整个链路就断了。

避坑指南:我曾经遇到过一个坑——异步线程里 Context 丢失。主线程创建的 Span,在子线程里拿不到。后来用了 OTel 的 Context 传播 API,手动把 Context 注入到子线程,才解决。

4.3 分布式上下文传播机制

上下文传播,说白了就是「接力棒怎么传」。常见的传播方式有两种:

4.3.1 基于 HTTP 头传播

服务间通过 HTTP 调用时,Context 信息放在请求头里。OTel 默认使用 W3C Trace Context 标准,两个关键头:

  • traceparent:包含 TraceID、SpanID、TraceFlags
  • tracestate:携带厂商自定义的扩展信息

举个例子,一个 HTTP 请求的头部可能是这样:

traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
tracestate: congo=t61rcWkgMzE

其中 0af7651916cd43dd8448eb211c80319c 是 TraceID,b7ad6b7169203331 是 SpanID。

4.3.2 基于消息队列传播

异步场景下,比如 Kafka、RabbitMQ,Context 需要嵌入到消息的 Header 里。消费者消费时,从 Header 中提取 Context,再创建新的 Span。

我遇到过一个问题:消息队列的 Header 有大小限制。如果 Baggage 塞了太多数据,可能导致消息发送失败。所以,Baggage 只放关键信息,别什么都往里塞。

4.4 传播机制对比

传播方式 适用场景 优点 缺点
HTTP 头 同步 RPC 调用 标准、简单、广泛支持 需要中间件支持
消息队列 Header 异步消息处理 解耦、异步链路完整 Header 大小限制
gRPC Metadata gRPC 服务间调用 原生支持、性能好 仅限 gRPC
进程内 Context 同一进程内跨线程 无网络开销 需手动传播

4.5 核心流程:一张图看懂

下面这张图展示了 Trace、Span、Context 之间的关系,以及传播过程:

Trace / Span / Context 传播流程 服务 A Span: HTTP GET /order 服务 B Span: 查询库存 服务 C Span: 扣减库存 traceparent traceparent Context 传播内容 TraceID: 0af7651916cd43dd8448eb211c80319c SpanID: b7ad6b7169203331 (服务A) → 9c3d2e1f (服务B) → 4a5b6c7d (服务C) Baggage: userId=12345, requestId=abc-def Trace 树形结构 Root Span: 用户下单 Span A: HTTP GET /order Span B: 查询库存 Span C: 扣减库存

4.6 实战:如何接入 OpenTelemetry

接入 OTel 其实不复杂。以 Java 为例,你只需要:

  1. 引入依赖:opentelemetry-api 和 opentelemetry-sdk
  2. 配置导出器:比如 OTLP 导出到 Jaeger
  3. 在代码中创建 Span

代码示例:

// 创建 Tracer
Tracer tracer = OpenTelemetry.getGlobalTracer("my-service");

// 创建 Span
Span span = tracer.spanBuilder("HTTP GET /api/order")
    .setAttribute("http.method", "GET")
    .setAttribute("http.url", "/api/order")
    .startSpan();

try (Scope scope = span.makeCurrent()) {
    // 业务逻辑
    doSomething();
} catch (Exception e) {
    span.recordException(e);
    span.setStatus(StatusCode.ERROR);
} finally {
    span.end();
}

注意:span.end() 一定要在 finally 里调用。我曾经见过线上 Span 泄漏,就是因为异常时忘了 end,导致内存暴涨。

4.7 常见问题与避坑

  • Span 泄漏:Span 创建后必须 end,否则内存泄漏。建议用 try-with-resources 或 finally 块。
  • Context 丢失:异步线程、消息队列、定时任务,都需要手动传播 Context。
  • 采样率设置:全量采集会带来性能开销。我一般建议生产环境用 10% 采样率,错误链路 100% 采样。
  • Baggage 滥用:Baggage 会随每个请求传播,别放太多数据。我见过有人把整个用户信息塞进去,结果网络开销暴涨。

嗯,这一章的内容就这些。Trace、Span、Context 这三个概念,你只要理解了,全链路追踪的骨架就搭起来了。后面我们会聊怎么把这些数据用起来,比如构建服务依赖图、计算 SLO。