3. Span生命周期:Span的创建、开始、结束、异常与状态码的语义

Span是分布式追踪的最小工作单元,其生命周期定义了从请求入口到出口的完整观测边界。在MTTR复盘场景中,Span生命周期的每个阶段都对应着明确的故障定位锚点。若生命周期语义模糊,将直接导致Trace链路断裂、耗时归因错误、异常根因漂移。

3.1 Span生命周期的五个阶段

一个标准的Span从创建到销毁经历五个核心阶段,每个阶段在标签设计中都有对应的语义字段:

阶段 语义定义 关键标签示例 MTTR复盘价值
创建 Span对象实例化,分配SpanContext,生成TraceId/SpanId span.creation_timestamp 确认链路起点,判断是否存在延迟创建
开始 记录操作起始时间戳,标记业务逻辑入口 span.start_time 计算端到端耗时,识别调度延迟
结束 记录操作完成时间戳,计算Span总耗时 span.end_time, span.duration_ms 判断是否超时,定位慢调用
异常 捕获并记录业务异常或系统异常 span.error, span.error.message 快速锁定故障类型与异常堆栈
状态码 设置Span最终状态(OK/ERROR/UNSET) span.status.code 判定调用成功/失败,支撑SLO计算
MTTR复盘要点: 在故障复盘时,若Span的"开始"与"创建"时间差过大(>100ms),通常表明存在线程池排队或连接池等待问题;若"结束"时间缺失,则可能是Span泄漏或异步回调未正确关闭。

3.2 Span生命周期状态机

Span的状态转换遵循严格的有向流程。下图展示了从创建到最终状态码确定的完整路径,以及异常分支的处理逻辑:

Span生命周期状态机 创建 (Created) 开始 (Started) 结束 (Ended) 异常 (Error) 状态码 (StatusCode) 最终状态 (Final) start() end() 异常抛出 记录后继续 setStatus() 完成 ⚠ 关键规则: 异常阶段不改变Span的"结束"状态;状态码必须在结束之后设置,且只能设置一次。

3.3 异常与状态码的语义边界

在MTTR复盘实践中,异常与状态码是最容易被混淆的两个概念。两者的语义边界必须清晰:

  • 异常(Error):记录的是"发生了什么问题",属于事件层面的描述。例如:数据库连接超时、空指针异常、业务校验失败。异常可以多次出现,支持堆栈追踪。
  • 状态码(StatusCode):记录的是"这个Span最终是否成功",属于结果层面的判定。状态码是终态,一旦设置不可变更。
标准化建议: 异常标签使用 span.error 前缀,状态码标签使用 span.status 前缀。两者在标签命名空间上严格隔离,避免在查询时产生语义冲突。

3.4 状态码枚举与MTTR映射

参考OpenTelemetry规范,状态码分为三个枚举值。在MTTR复盘时,每个枚举值对应不同的故障排查路径:

状态码 数值 语义 MTTR复盘动作
UNSET 0 未显式设置状态,默认值 检查是否遗漏了状态码设置;若Span正常结束但状态码为UNSET,视为埋点缺陷
OK 1 Span执行成功,无错误 无需介入;若OK但耗时异常,需检查资源竞争或锁等待
ERROR 2 Span执行失败,存在错误 立即关联 span.error 标签,提取异常堆栈,定位故障根因

3.5 生命周期标签标准化清单

以下为Span生命周期各阶段必须采集的标准化标签清单,直接用于MTTR复盘的数据对齐:

# 创建阶段
span.creation_timestamp: 1712563200000    # 创建时间戳(ms)
span.trace_id: "abc123def456"             # 所属Trace ID
span.span_id: "span001"                   # 当前Span ID

# 开始阶段
span.start_time: 1712563200100            # 开始时间戳(ms)
span.operation_name: "query_user_db"      # 操作名称

# 结束阶段
span.end_time: 1712563200150              # 结束时间戳(ms)
span.duration_ms: 50                      # 耗时(ms)

# 异常阶段
span.error: true                          # 是否发生异常
span.error.type: "DbTimeoutException"     # 异常类型
span.error.message: "Connection pool exhausted"  # 异常消息
span.error.stacktrace: "com.xxx.dao... line 42" # 异常堆栈(可选)

# 状态码阶段
span.status.code: 2                       # 状态码枚举值
span.status.description: "database timeout" # 状态描述(可选)
常见陷阱: 部分实现将异常信息直接写入 span.status.description 而忽略 span.error 标签,导致在聚合查询时无法按异常类型分组。标准化要求:异常详情必须使用 span.error.* 命名空间,状态码描述仅作为补充说明。

3.6 生命周期时序图

下图展示了Span生命周期在分布式调用中的完整时序,突出创建、开始、结束、异常、状态码之间的时间关系:

Span生命周期时序与标签采集点 时间 创建 span.creation_timestamp 开始 span.start_time 异常 span.error.* 结束 span.end_time 状态码 span.status.code 调度延迟 执行耗时 (duration_ms) 异常处理耗时 📐 MTTR复盘公式: 故障定位时间 = 调度延迟 + 异常处理耗时 + 状态码判定时间 其中调度延迟 = span.start_time - span.creation_timestamp

3.7 生命周期常见反模式

在MTTR复盘实践中,以下三种Span生命周期反模式会直接导致故障定位效率下降:

  1. 提前设置状态码:在Span尚未结束时调用 setStatus(OK),后续异常无法覆盖状态码,导致Trace显示成功但实际存在错误。
  2. 异常与状态码分离:记录了 span.error 但未将状态码设置为 ERROR,导致监控系统按状态码聚合时漏报。
  3. 创建后未开始:Span对象创建后因条件分支未调用 start(),导致该Span成为"幽灵Span",在Trace中显示为0耗时且无结束时间。
标准化检查项: 在代码Review阶段,检查每个Span的创建/开始/结束是否成对出现;检查每个异常分支是否同时设置了 span.error 和 span.status.code = 2;检查状态码是否在 end() 之后设置。