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 代表内部调用的子操作。树形结构直观地展示了请求的调用链和耗时分布。
关键概念: 树形结构中的每个节点都是一个 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 时的完整传播路径:
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 丢失父级关系。