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、TraceFlagstracestate:携带厂商自定义的扩展信息
举个例子,一个 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 之间的关系,以及传播过程:
4.6 实战:如何接入 OpenTelemetry
接入 OTel 其实不复杂。以 Java 为例,你只需要:
- 引入依赖:
opentelemetry-api和opentelemetry-sdk - 配置导出器:比如 OTLP 导出到 Jaeger
- 在代码中创建 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。