4. Span 的生命周期:Span 创建、属性设置、事件记录、状态设置、Span 结束

Span 是链路追踪的基本工作单元,代表分布式系统中一个具名、有时间范围的操作。理解 Span 的生命周期是正确埋点、避免数据丢失或语义错误的前提。一个完整的 Span 从创建到结束,依次经历以下五个阶段:创建、属性设置、事件记录、状态设置、结束。

4.1 Span 生命周期总览

下图展示了 Span 从出生到销毁的完整状态流转,以及每个阶段可执行的操作:

Span 生命周期状态流转图 Span 生命周期状态流转 创建 StartSpan() 属性设置 SetAttribute() 事件记录 AddEvent() 状态设置 SetStatus() 结束 End() 属性设置、事件记录、状态设置可在 Span 结束前重复调用 Span 结束(End())后,任何修改操作都将被忽略 时间轴 →
核心原则: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 携带完整、准确的上下文信息,为后续的链路分析和问题定位提供可靠数据基础。