3. Span 与 Trace 基础:定义、生命周期与属性

在分布式系统中,一次用户请求往往跨越多个微服务、数据库和消息队列。为了完整追踪这次请求的路径,OpenTelemetry 引入了两个核心抽象:Span(跨度)和 Trace(追踪)。本章将深入剖析它们的定义、生命周期以及如何通过属性和事件丰富观测数据。

3.1 Span 定义

Span 是分布式追踪中的最小工作单元,代表系统中一个具有开始时间和结束时间的操作。每个 Span 封装了以下关键信息:

字段 说明 示例值
Span ID 全局唯一标识符,用于区分单个 Span a1b2c3d4e5f6g7h8
Trace ID 所属 Trace 的唯一标识,所有相关 Span 共享此 ID abc123def456ghi789
Parent Span ID 父 Span 的 ID,用于构建调用链层级 parent-span-id
Span Name 描述操作的人类可读名称 HTTP GET /api/users
Start Timestamp 操作开始的精确时间(Unix 纳秒) 1710000000000000000
End Timestamp 操作结束的精确时间 1710000000500000000
Span Kind 标识 Span 在 Trace 中的角色 Server, Client, Internal
Status 操作结果状态(Unset / Ok / Error) Ok
Attributes 键值对形式的元数据 http.method=GET
Events 带时间戳的结构化日志 cache.miss
关键概念: Span 的 Span Kind 决定了它在追踪拓扑中的语义。例如,Server 表示服务端接收请求,Client 表示客户端发起调用,Internal 表示内部方法调用。正确设置 Kind 有助于后端系统自动构建服务依赖图。

3.2 Trace 定义

Trace 是一条贯穿整个分布式系统的请求路径记录,由一组具有相同 Trace ID 的 Span 组成。这些 Span 通过父子关系形成一棵有向无环图(DAG),完整还原请求从入口到出口的完整流程。

Trace 的核心特征:

  • 全局唯一性:每个 Trace 由唯一的 Trace ID 标识,通常由入口服务生成。
  • 跨进程传播:通过上下文传播机制(如 W3C TraceContext 协议),Trace ID 在服务间传递。
  • 层级结构:根 Span(Root Span)代表请求的起点,子 Span 代表下游调用。
  • 时间边界:Trace 的持续时间等于根 Span 的持续时间。
实践要点: 在微服务架构中,一个 Trace 可能包含数百个 Span。合理设置采样策略(如基于头部采样或概率采样)可以控制数据量,同时保留关键路径的可观测性。

3.3 Span 生命周期

每个 Span 从创建到结束经历四个明确阶段。理解生命周期是正确使用 OpenTelemetry API 的基础。

  1. 创建(Start):通过 Tracer 创建 Span,记录开始时间戳,设置初始属性。
  2. 激活(Activate):将 Span 设置为当前活跃 Span,使其成为后续子 Span 的父 Span。
  3. 操作(Operation):在 Span 活跃期间添加属性、事件或记录错误。
  4. 结束(End):记录结束时间戳,将 Span 标记为完成并导出。

以下代码展示了 Span 生命周期的典型实现:

// 1. 创建 Span
Span span = tracer.spanBuilder("processOrder")
    .setSpanKind(SpanKind.SERVER)
    .startSpan();

// 2. 激活 Span(使其成为当前上下文)
try (Scope scope = span.makeCurrent()) {
    // 3. 操作:添加属性和事件
    span.setAttribute("order.id", "12345");
    span.addEvent("payment.processed", 
        Attributes.of("amount", 99.99));

    // 执行业务逻辑...
    Thread.sleep(100);

} catch (Exception e) {
    span.recordException(e);
    span.setStatus(StatusCode.ERROR, "Order processing failed");
} finally {
    // 4. 结束 Span
    span.end();
}
常见陷阱: 忘记调用 span.end() 会导致 Span 永远处于打开状态,造成内存泄漏和追踪数据不完整。始终使用 try-finally 或 try-with-resources 确保 Span 正确关闭。

3.4 Span 属性与事件

属性(Attributes) 是附加在 Span 上的键值对,用于描述操作的上下文信息。OpenTelemetry 定义了语义约定(Semantic Conventions),推荐使用标准属性名以提高互操作性。

属性类别 推荐属性 示例
HTTP 请求 http.method, http.url, http.status_code http.method=POST
数据库操作 db.system, db.statement, db.connection_string db.system=postgresql
消息队列 messaging.system, messaging.destination messaging.system=kafka
业务指标 自定义属性(如 user.id, tier) user.id=user_001

事件(Events) 是带时间戳的结构化日志记录,用于标记 Span 生命周期内的关键时刻。与属性不同,事件可以多次添加,且每个事件独立携带时间戳和属性集。

// 添加事件示例
span.addEvent("cache.hit", 
    Attributes.of("cache.key", "user_profile_123"));

span.addEvent("retry.attempt", 
    Attributes.of("attempt.number", 2, "delay.ms", 500));

// 记录异常事件
span.recordException(new RuntimeException("Connection timeout"),
    Attributes.of("peer.address", "10.0.0.1:8080"));
最佳实践: 属性用于描述 Span 的静态上下文(如请求方法、数据库名称),事件用于记录动态行为(如缓存命中、重试、异常)。避免将高频变化的数据放入属性,以免增加存储开销。

3.5 Span 与 Trace 关系图解

下图展示了一个典型 Trace 的结构,包含根 Span 和多个子 Span,以及它们之间的父子关系:

Trace 结构示例:用户请求处理 T0 T0+500ms Root Span: API Gateway /api/orders Child: Auth Service Child: Order Service Child: DB Query Child: Cache Check 图例说明: • 所有 Span 共享同一个 Trace ID(如:abc123def456ghi789) • 根 Span 是整个请求的入口,子 Span 通过 Parent Span ID 形成层级 • 时间轴从左到右,Span 的宽度表示其持续时间

3.6 上下文传播与 Trace 完整性

Span 和 Trace 的关联依赖于上下文传播(Context Propagation)。OpenTelemetry 使用 Context 对象在进程内和进程间传递 Trace 信息。进程内通过 Scope 管理活跃 Span,进程间通过 W3C TraceContext 或 B3 协议在 HTTP 头或消息元数据中传递。

下图展示了上下文如何在两个服务间传播:

上下文传播流程 服务 A(前端) 创建 Span A 注入 TraceContext 到 HTTP 头 服务 B(后端) 提取 TraceContext 创建 Span B(父 Span ID = Span A) HTTP 请求 传播协议示例(HTTP 头): traceparent: 00-abc123def456ghi789-a1b2c3d4e5f6g7h8-01 其中:版本- Trace ID - Span ID - 追踪标志
关键总结: Span 是 Trace 的构建块,Trace 是 Span 的集合。正确管理 Span 生命周期(创建→激活→操作→结束)和上下文传播是构建可靠分布式追踪的基础。属性与事件为 Span 提供了丰富的语义上下文,使问题定位从“知道哪里慢”升级为“知道为什么慢”。