4. Span 与 Trace 基础:生命周期、树形结构与传播机制

在分布式追踪中,Span 和 Trace 是最核心的两个抽象概念。Span 代表一次单一的操作单元,而 Trace 则是由多个 Span 组成的端到端请求路径。理解 Span 的生命周期、Trace 的树形结构以及 SpanContext 的传播机制,是构建可观测系统的基石。

4.1 Span 的生命周期

一个 Span 从创建到结束,经历四个关键阶段。每个阶段都对应着特定的状态和操作,开发者需要精确控制这些阶段以保证追踪数据的完整性。

阶段 状态 关键操作 说明
创建 未开始 tracer.startSpan(name) 分配 Span ID,记录开始时间戳,但尚未写入任何属性。
活跃 进行中 设置属性、添加事件 Span 处于当前上下文中,可以添加标签、日志事件和状态。
结束 已关闭 span.end() 记录结束时间戳,冻结 Span 数据,不再接受修改。
导出 已导出 SpanProcessor 处理 Span 被传递给 Exporter,发送到后端存储或分析系统。
最佳实践: 始终使用 try-with-resources 或 defer 模式确保 Span 在异常路径下也能正确结束。未结束的 Span 会导致 Trace 不完整,影响延迟分析。

4.2 Trace 的树形结构

一个 Trace 由多个 Span 组成,这些 Span 通过父子关系形成一棵有向无环树。根 Span(Root Span)代表整个请求的入口,子 Span 代表内部调用的子操作。树形结构直观地展示了请求的调用链和耗时分布。

Trace 树形结构示例 Trace 树形结构:HTTP 请求调用链 Root Span: GET /api/order Auth: validateToken DB: queryOrder Cache: getPrice RPC: userService RPC: inventorySvc SQL: select 根 Span 是整个 Trace 的入口
关键概念: 树形结构中的每个节点都是一个 Span。根 Span 的 parentSpanId 为空;子 Span 通过 parentSpanId 指向父 Span。这种结构允许后端工具(如 Jaeger、Zipkin)重建完整的调用拓扑。

4.3 SpanContext 传播机制

SpanContext 是 Span 的“身份证”,它包含了在分布式系统中唯一标识一个 Span 所需的所有元数据。传播机制确保跨进程、跨线程的 Span 能够正确关联,形成完整的 Trace。

4.3.1 SpanContext 的核心字段

字段 类型 说明
TraceId 16 字节(128 位) 全局唯一标识一个 Trace。所有属于同一 Trace 的 Span 共享同一个 TraceId。
SpanId 8 字节(64 位) 唯一标识当前 Span。每个 Span 都有自己的 SpanId。
TraceFlags 1 字节 控制位,例如是否采样(sampled flag)。
TraceState 键值对列表 用于传递供应商特定的追踪元数据(如 W3C Trace Context 标准)。

4.3.2 传播方式:W3C Trace Context 标准

OpenTelemetry 默认采用 W3C Trace Context 标准进行跨进程传播。该标准定义了两个 HTTP 头:

  • traceparent:携带 TraceId、SpanId 和 TraceFlags。格式:00-{traceId}-{spanId}-{flags}。
  • tracestate:携带供应商自定义的键值对,用于扩展。
// 示例:traceparent 头
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
// 分解:
// 00        - 版本号
// 0af7...9c - TraceId (32 位十六进制)
// b7ad...31 - SpanId (16 位十六进制)
// 01        - TraceFlags (sampled = true)
常见陷阱: 在异步编程(如回调、协程)中,SpanContext 不会自动传播。必须显式使用 Context 对象将 SpanContext 注入到异步任务中,否则会导致 Span 丢失父级关系,形成“孤儿 Span”。

4.4 传播机制流程图

下图展示了 SpanContext 在服务 A 调用服务 B 时的完整传播路径:

SpanContext 跨进程传播流程 SpanContext 跨进程传播流程 服务 A (客户端) 1. 创建 Client Span 2. 注入 SpanContext 到 HTTP 头 HTTP 请求 (携带 traceparent) 服务 B (服务端) 3. 提取 SpanContext 4. 创建 Server Span 设置 parentSpanId 注入:将当前 SpanContext 序列化到传输协议(HTTP/gRPC/消息队列) 提取:从传输协议反序列化 SpanContext,并设置为新 Span 的父级

4.5 代码示例:手动创建与传播 Span

以下 Java 示例展示了如何手动创建 Span、设置父子关系以及通过 HTTP 头传播 SpanContext:

// 1. 创建根 Span
Span rootSpan = tracer.spanBuilder("GET /api/order")
    .setSpanKind(SpanKind.SERVER)
    .startSpan();

try (Scope scope = rootSpan.makeCurrent()) {
    // 2. 创建子 Span(数据库查询)
    Span dbSpan = tracer.spanBuilder("queryOrder")
        .setParent(Context.current())
        .setSpanKind(SpanKind.CLIENT)
        .startSpan();
    try {
        // 模拟数据库调用
        dbSpan.setAttribute("db.system", "mysql");
        dbSpan.setAttribute("db.statement", "SELECT * FROM orders WHERE id = ?");
        // ... 执行查询
    } finally {
        dbSpan.end();
    }

    // 3. 跨进程传播:注入到 HTTP 头
    HttpURLConnection connection = (HttpURLConnection) new URL("http://service-b/api").openConnection();
    OpenTelemetryPropagators.getTextMapPropagator()
        .inject(Context.current(), connection, (carrier, key, value) -> {
            carrier.setRequestProperty(key, value);
        });
    // ... 发送请求
} finally {
    rootSpan.end();
}
Mock 隔离提示: 在单元测试中,可以使用 InMemorySpanExporter 验证 Span 的创建和传播逻辑,无需真实后端。这是 OTelMock 隔离的核心实践之一。

4.6 本章小结

  • Span 生命周期:创建 → 活跃 → 结束 → 导出,每个阶段都有明确的职责和约束。
  • Trace 树形结构:通过 parentSpanId 形成有向无环树,根 Span 是整个请求的入口。
  • SpanContext 传播:W3C Trace Context 标准通过 traceparent 和 tracestate 头实现跨进程传递。
  • 上下文管理:在异步场景下必须显式传播 Context,避免 Span 丢失父级关系。