4. Span 的生命周期:Span 创建、属性设置、事件记录、状态设置、Span 结束
Span 是链路追踪的基本工作单元,代表分布式系统中一个具名、有时间范围的操作。理解 Span 的生命周期是正确埋点、避免数据丢失或语义错误的前提。一个完整的 Span 从创建到结束,依次经历以下五个阶段:创建、属性设置、事件记录、状态设置、结束。
4.1 Span 生命周期总览
下图展示了 Span 从出生到销毁的完整状态流转,以及每个阶段可执行的操作:
核心原则:Span 的生命周期是单向不可逆的。创建后可以多次设置属性、记录事件、更新状态,但一旦调用
End(),Span 即被冻结,后续任何修改都不会生效。
4.2 Span 创建
Span 的创建是链路追踪的起点。在 OpenTelemetry Go SDK 中,通过 Tracer 对象的 Start() 方法创建 Span。创建时需要指定 Span 名称,并可选传入父 Span 上下文来建立父子关系。
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
)
// 获取 Tracer
tracer := otel.Tracer("shop-service")
// 创建 Span(无父 Span,成为根 Span)
ctx, span := tracer.Start(ctx, "process-order")
defer span.End()
| 参数 | 说明 | 是否必填 |
|---|---|---|
ctx |
父上下文,携带父 Span 信息;为 context.Background() 时创建根 Span |
是 |
spanName |
Span 的操作名称,如 "query-database" |
是 |
opts ...SpanStartOption |
可选参数,如设置 SpanKind、时间戳、链接等 | 否 |
常见错误:忘记将
Start() 返回的 ctx 传递给下游操作。如果继续使用旧的 ctx,子 Span 将无法正确关联父 Span,导致链路断裂。
4.3 属性设置
属性(Attributes)是 Span 的键值对元数据,用于描述操作的上下文信息,如请求 ID、用户 ID、数据库表名、HTTP 状态码等。属性应在 Span 活跃期间尽早设置,以便在分析时提供完整上下文。
// 设置单个属性
span.SetAttributes(attribute.String("user.id", "u-10086"))
span.SetAttributes(attribute.Int("http.status_code", 200))
// 批量设置属性(推荐,减少调用次数)
span.SetAttributes(
attribute.String("db.system", "postgresql"),
attribute.String("db.statement", "SELECT * FROM orders"),
attribute.Int("db.rows_affected", 42),
)
| 属性类型 | Go 类型 | 示例 |
|---|---|---|
| 字符串 | attribute.String(key, val) |
attribute.String("service", "payment") |
| 整数 | attribute.Int(key, val) |
attribute.Int("retry_count", 3) |
| 布尔 | attribute.Bool(key, val) |
attribute.Bool("cache_hit", true) |
| 浮点数 | attribute.Float64(key, val) |
attribute.Float64("duration_ms", 23.5) |
| 字符串切片 | attribute.StringSlice(key, val) |
attribute.StringSlice("tags", []string{"v1","v2"}) |
最佳实践:属性值应保持低基数(Low Cardinality),避免将每次请求都不同的值(如 Session ID、时间戳)设为属性,否则会导致指标维度爆炸。高基数信息应放在 Span 事件或日志中。
4.4 事件记录
事件(Events)是 Span 时间轴上的带时间戳的标注,用于记录操作过程中发生的特定时刻,如异常抛出、缓存命中、外部调用开始/结束等。每个事件包含名称和可选的属性集合。
// 记录简单事件
span.AddEvent("cache-miss")
// 记录带属性的事件
span.AddEvent("exception",
attribute.String("exception.type", "TimeoutError"),
attribute.String("exception.message", "connection timeout after 5s"),
)
// 记录带时间戳的事件(使用 SpanContext 时间)
span.AddEvent("retry-attempt",
attribute.Int("attempt", 2),
attribute.String("status", "pending"),
)
| 方法 | 参数 | 说明 |
|---|---|---|
AddEvent(name) |
事件名称(字符串) | 记录一个无属性的事件 |
AddEvent(name, attrs...) |
名称 + 属性列表 | 记录带上下文的事件 |
AddEvent(name, opts...) |
名称 + 事件选项 | 可指定时间戳等高级选项 |
事件 vs 属性:属性描述 Span 整个生命周期的静态特征;事件描述 Span 时间轴上的动态时刻。一个 Span 可以有多个事件,但属性通常在整个 Span 中保持一致。
4.5 状态设置
Span 状态(Status)表示操作的整体结果。OpenTelemetry 定义了三种状态码:Unset(默认)、Ok(成功)、Error(失败)。状态应在 Span 结束前设置,通常放在操作逻辑的末尾。
import "go.opentelemetry.io/otel/codes"
// 操作成功
span.SetStatus(codes.Ok, "order processed successfully")
// 操作失败
span.SetStatus(codes.Error, "database connection failed")
// 不显式设置状态时,默认为 Unset(表示未明确指定)
| 状态码 | 含义 | 使用场景 |
|---|---|---|
codes.Unset |
未设置(默认值) | 操作正常完成,但不需要显式标记成功 |
codes.Ok |
操作成功 | 业务逻辑明确成功,如支付成功、订单创建完成 |
codes.Error |
操作失败 | 发生异常、超时、业务校验失败等 |
注意:不要将
SetStatus(codes.Ok, "") 放在 Span 开头。如果后续发生错误,状态会被覆盖为 Ok,导致错误被掩盖。正确的做法是在操作结束时根据结果设置状态。
4.6 Span 结束
End() 是 Span 生命周期的终点。调用 End() 后,Span 会被标记为完成,其开始时间和结束时间被固定,所有属性、事件、状态被冻结并准备导出。在 Go 中,通常使用 defer span.End() 确保 Span 在函数退出时自动结束。
func ProcessOrder(ctx context.Context, orderID string) error {
tracer := otel.Tracer("order-service")
ctx, span := tracer.Start(ctx, "process-order")
defer span.End() // 确保函数退出时 Span 结束
// 设置属性
span.SetAttributes(attribute.String("order.id", orderID))
// 业务逻辑
if err := validateOrder(orderID); err != nil {
span.SetStatus(codes.Error, "validation failed")
span.AddEvent("order-validation-error",
attribute.String("error", err.Error()))
return err
}
// 处理订单...
span.AddEvent("order-validated")
span.SetStatus(codes.Ok, "order processed")
return nil
}
4.7 完整生命周期示例
以下代码展示了 Span 五个阶段的完整使用模式:
func HandleHTTPRequest(ctx context.Context, req *http.Request) {
tracer := otel.Tracer("api-gateway")
ctx, span := tracer.Start(ctx, "handle-request")
defer span.End()
// 阶段1:创建后立即设置属性
span.SetAttributes(
attribute.String("http.method", req.Method),
attribute.String("http.url", req.URL.String()),
attribute.String("user.agent", req.UserAgent()),
)
// 阶段2:记录事件
span.AddEvent("request-received")
// 模拟业务处理
result, err := processPayment(ctx, req)
if err != nil {
// 阶段3:记录错误事件
span.AddEvent("payment-error",
attribute.String("error.message", err.Error()))
// 阶段4:设置错误状态
span.SetStatus(codes.Error, "payment failed")
http.Error(w, "Internal Error", 500)
return
}
// 阶段5:设置成功状态
span.AddEvent("payment-success")
span.SetStatus(codes.Ok, "payment completed")
// defer span.End() 自动执行,Span 结束
}
总结:Span 生命周期是链路追踪埋点的核心模型。遵循“创建 → 设属性 → 记事件 → 设状态 → 结束”的顺序,可以保证每个 Span 携带完整、准确的上下文信息,为后续的链路分析和问题定位提供可靠数据基础。