4. OpenTelemetry 基础:Trace、Span、Context 核心概念与数据模型

在分布式系统中,一个用户请求往往跨越多个微服务、数据库、消息队列和第三方 API。当出现错误或性能瓶颈时,传统的单机日志和指标无法还原请求的完整路径。OpenTelemetry(简称 OTel)正是为了解决这一难题而诞生的统一可观测性标准。

4.1 什么是 OpenTelemetry?

OpenTelemetry 是 CNCF(云原生计算基金会)的孵化项目,由 OpenTracing 和 OpenCensus 合并而来。它提供了一套厂商中立的 API、SDK 和工具,用于生成、采集和导出遥测数据(Traces、Metrics、Logs)。

核心定位: 可观测性领域的“HTTP 协议”——你只需按照 OTel 标准埋点,就能将数据发送到任何兼容的后端(Jaeger、Zipkin、Prometheus、Datadog 等)。
特性 说明
统一 API 一套 API 覆盖 Trace、Metric、Log,避免供应商锁定
多语言支持 官方支持 Python、Go、Java、JavaScript、.NET 等主流语言
自动/手动埋点 提供自动插桩(Instrumentation)库,也支持手动创建 Span
可扩展导出 通过 OTLP 协议或 Exporter 适配器对接任意后端
上下文传播 内置 W3C TraceContext 和 Baggage 传播协议

4.2 Trace、Span、Context 三大核心概念

理解 OTel 的调用链模型,必须从以下三个概念入手:

  • Trace(追踪):代表一个完整的请求或事务在分布式系统中的全生命周期。一个 Trace 由多个 Span 组成,通过唯一的 TraceID 标识。
  • Span(跨度):Trace 中的最小工作单元,描述一个具体的操作(如“查询数据库”、“调用订单服务”)。每个 Span 包含名称、开始/结束时间、属性、事件和状态。
  • Context(上下文):携带 TraceID、SpanID 和 Baggage 等传播信息的对象,负责在进程内和进程间传递调用关系。
记忆口诀: Trace 是“整条链路”,Span 是“链路中的每一步”,Context 是“串联每一步的绳子”。

4.3 Span 数据模型详解

Span 是 OTel 数据模型的核心。以下是一个 Span 的完整结构:

字段 类型 说明
TraceId 16 字节 全局唯一,标识所属 Trace
SpanId 8 字节 当前 Span 的唯一标识
ParentSpanId 8 字节 父 Span 的 ID,根 Span 为空
Name string 操作名称,如 HTTP GET /api/order
Kind enum Span 角色:CLIENT、SERVER、INTERNAL、PRODUCER、CONSUMER
StartTime / EndTime timestamp 操作开始和结束的精确时间
Attributes map 键值对,记录请求 URL、状态码、数据库语句等
Events list 带时间戳的注解,用于记录异常、日志等
Status StatusCode UNSET、OK、ERROR
Links list 关联其他 Trace 的 Span,用于异步场景

4.4 Trace 与 Span 的层级关系(SVG 图)

下图展示了一个典型的分布式 Trace 结构:根 Span 代表前端请求,子 Span 分别代表网关、订单服务和数据库操作。

Trace 与 Span 层级关系图 Trace 与 Span 层级关系 时间→ 根 Span: GET /checkout TraceID: abc123 子 Span: 网关路由 子 Span: 订单服务 createOrder 子 Span: MySQL INSERT order 说明: • 每个 Span 都包含相同的 TraceID,但 SpanID 不同 • 子 Span 通过 ParentSpanId 指向父 Span • Span 的宽度代表操作耗时,越宽耗时越长

4.5 Context 传播机制

Context 是 OTel 调用链的“粘合剂”。它确保跨进程的 Span 能够正确关联。传播过程分为两步:

  1. 注入(Inject):在客户端(如 HTTP 调用方)将当前 Context 序列化到请求头中。
  2. 提取(Extract):在服务端从请求头中反序列化出 Context,并创建子 Span。

OTel 默认使用 W3C TraceContext 协议,通过 traceparent 和 tracestate 两个 HTTP 头传递上下文。

常见陷阱: 如果使用异步消息队列(如 Kafka、RabbitMQ),Context 不会自动传播。必须手动在生产者端注入、消费者端提取,否则调用链会断裂。

4.6 Span Kind 与角色定义

Span Kind 描述了 Span 在分布式调用中的角色,帮助后端系统更好地可视化调用关系:

Kind 适用场景 示例
INTERNAL 进程内部操作,无外部网络调用 函数计算、本地缓存查询
CLIENT 发起外部请求的一方 HTTP 客户端、gRPC 客户端
SERVER 接收外部请求的一方 HTTP 服务端、gRPC 服务端
PRODUCER 向消息队列发送消息 Kafka 生产者、RabbitMQ 发布者
CONSUMER 从消息队列接收消息 Kafka 消费者、RabbitMQ 订阅者

4.7 数据模型与传播流程(SVG 图)

下图展示了两个服务之间通过 HTTP 调用时,Context 的注入、提取以及 Span 的创建过程:

Context 传播与 Span 创建流程 Context 传播与 Span 创建流程 服务 A(客户端) 服务 B(服务端) 1. 创建客户端 Span 2. 注入 Context 到 HTTP 头 3. 发送 HTTP 请求 4. 提取 Context 5. 创建服务端 Span 6. 处理业务逻辑 traceparent 头

4.8 代码示例:Python 手动创建 Span

以下代码展示了如何在 Python 中使用 OTel SDK 手动创建 Trace 和 Span:

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor

# 1. 初始化 TracerProvider
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)

# 2. 获取 Tracer
tracer = trace.get_tracer(__name__)

# 3. 创建根 Span
with tracer.start_as_current_span("parent-span") as parent:
    parent.set_attribute("http.method", "GET")
    parent.add_event("开始处理请求")
    
    # 4. 创建子 Span
    with tracer.start_as_current_span("child-span") as child:
        child.set_attribute("db.system", "mysql")
        child.set_attribute("db.statement", "SELECT * FROM orders")
        child.add_event("查询完成")
    
    # 5. 设置状态
    parent.set_status(trace.Status(trace.StatusCode.OK))
关键点: start_as_current_span 会自动将新 Span 的父 Span 设置为当前激活的 Span。如果当前没有激活的 Span,则新 Span 会成为根 Span。

4.9 本章小结

  • OpenTelemetry 是统一的可观测性标准,解决厂商锁定问题。
  • Trace 代表完整请求链路,Span 是链路中的最小工作单元,Context 负责跨进程传播。
  • Span 数据模型包含 TraceId、SpanId、ParentSpanId、Attributes、Events、Status 等核心字段。
  • Context 通过 W3C TraceContext 协议在 HTTP 头中传播,异步场景需手动处理。
  • Span Kind 定义了 Span 在调用中的角色,影响后端可视化效果。