第二章 核心概念:Trace、Span、SpanContext

做链路追踪,你首先得搞懂三个最基础的概念:Trace、Span 和 SpanContext。这三个词我几乎每天都要跟它们打交道。说白了,它们就是链路追踪世界的“主语、谓语、宾语”——Trace 是一次请求的完整旅程,Span 是旅程中的每一个环节,而 SpanContext 就是各个环节之间传递的“接力棒”。

我在刚接触分布式系统时,就被这些概念绕晕过。后来亲手搭过一次全链路追踪,才真正理解了它们是怎么配合的。咱们一个一个说。

1. Trace —— 一次请求的完整轨迹

Trace 代表一个从用户发起请求到最终返回结果的完整过程。无论这个请求穿过多少服务、调用多少数据库,只要属于同一笔业务,就共享同一个 TraceID。

我记得有一次排查一个订单超时问题,就是通过 TraceID 把 6 个微服务的日志串起来的。如果没有 Trace,你只能在一个一个服务的日志里翻,特别痛苦。

Trace 关键属性 说明
TraceID 全局唯一标识,通常 16 字节或 128 位随机数生成
起始时间 Trace 的开始时间戳,对应第一个 Span 的开始
结束时间 Trace 的结束时间戳,对应最后一个 Span 的结束
Span 列表 该 Trace 下所有 Span 的集合,按父子关系组织
小技巧: 我在项目里习惯给 TraceID 加一个业务前缀(比如 “order_” + 随机数),这样看到 ID 就能大概知道是哪个业务线,省去翻表的时间。

2. Span —— 一个服务或组件的工作单元

Span 是 Trace 的基本组成单位。一次 RPC 调用、一个数据库查询、一个消息队列生产动作,都可以用一个 Span 来表示。每个 Span 有明确的开始和结束时间,还带操作名称、标签、日志等辅助信息。

你想想看,一个 Trace 就像一棵树,根 Span 是入口服务,子 Span 是它调用的下游服务,再往下是更细粒度的操作。我见过一张 Span 树展开后多达几十层,没有父子关系根本理不清。

每个 Span 包含以下核心字段:

  • SpanID —— 当前 Span 的唯一标识
  • ParentSpanID —— 父 Span 的 ID,根 Span 没有父 ID
  • OperationName —— 操作名称,比如 “HTTP GET /api/order”
  • StartTime / EndTime —— 耗时区间
  • Tags —— key-value 形式的描述信息,例如 “http.status_code=200”
  • Logs —— 事件记录,比如异常堆栈
避坑指南: 我曾经接手过一个项目,开发同学给 Span 的 OperationName 全写成 “handle”。结果查链路时根本分不清哪个是哪个。记住:操作名称一定要带上服务名+接口名,最好还能加个版本号。

3. SpanContext —— 跨进程传递的上下文

SpanContext 是保证链路不中断的关键。当服务 A 调用服务 B 时,必须把 TraceID、当前 SpanID 等信息通过 RPC 请求头传给下游,下游才能把自己产生的 Span 挂到同一个 Trace 树下。

SpanContext 一般包含:

  • TraceID —— 当前请求的全局 ID
  • SpanID —— 当前 Span 的 ID(下游用它作为 ParentSpanID)
  • Baggage —— 自定义上下文数据,会随着调用链逐级传递
  • 其他元数据 —— 比如采样标记、跟踪版本等

目前业界主流使用 W3C Trace Context 标准,通过 traceparent 和 tracestate 两个 HTTP Header 传递。我的习惯是在所有微服务网关层统一注入这个 Header,避免每个服务自己拼写导致格式错误。

核心原则: SpanContext 是“只读”的,下游可以从中读取 TraceID 和父 SpanID,但不能修改。如果一定要传递额外的业务数据,请用 Baggage,但要注意 Baggage 会随着请求一直传播,滥用会导致 Header 膨胀。

4. 三者关系与流转

把这三个概念放在一起,就形成了一个完整的链路追踪模型。下面这张图展示了一个典型的请求流程:

Trace / Span / SpanContext 关系图 Trace (根 Span) Span1 (服务A) Span2 (服务B) Span3 (数据库) SpanContext 传递 服务A → 服务B 的请求头中携带 traceparent: 00-{TraceID}-{SpanID}-01 核心含义 Trace = 一棵 Span 树 SpanContext 是“令牌”

从上图可以看到:一个 Trace 包含多个 Span,Span 之间形成父子树形结构。根 Span 没有父 ID,其他 Span 通过 ParentSpanID 找到自己的父亲。SpanContext 就夹在每两个服务请求的 Header 里,保证下游 Span 能正确地认祖归宗。

5. 实战中容易踩的坑

理论讲完了,我来盘点一下现实中我见过最多的问题:

  1. TraceID 不统一 —— 有的服务用 UUID,有的用雪花算法,两个链路追踪系统之间根本无法关联。我建议要么全部用 W3C 标准格式,要么统一一个分布式 ID 生成服务。
  2. SpanContext 丢失 —— 异步线程、消息队列、定时任务是最容易漏传 SpanContext 的地方。我之前遇到过一条链路在消息消费端突然断掉,查了半天发现是开发没把 Header 传递到新线程里。
  3. Baggage 滥用 —— 有人把所有业务参数都塞进 Baggage,结果每个请求的 HTTP Header 膨胀到几十 KB,影响性能。记住 Baggage 只放“穿透上下游的关键标识”,比如用户 ID、会话 ID,别的别往里塞。
  4. Span 结束时间不准确 —— 有些框架在 Span 结束时只记录了当前时间,但真正的操作可能还在后台线程跑。一定要确保 Span 的范围和实际业务逻辑严格对齐。
我曾经…… 遇到一个诡异问题:一个 Trace 下有 3 个 Span,但汇总后的耗时反而比实际 RTT 少很多。后来发现是 Span 的开始和结束时间戳没带时区信息,跨机房时导致时间错位。所以大家一定要统一使用 UTC 时间戳,别用本地时间。

好了,现在你对 Trace、Span、SpanContext 应该有了比较清晰的认识。这三个概念是所有链路追踪工具的基石,后面的章节我们所有的排查技巧都建立在这上面。嗯,理解了它们,你就算入门了。