分布式链路追踪核心概念:Trace、Span、Annotation、Baggage

说起分布式链路追踪,你最先接触到的基本上就是这四个词:Trace、Span、Annotation、Baggage。坦白讲,我刚开始接触时也一头雾水——这堆概念到底在描述什么?后来在线上排查过一个慢请求,一步步追着日志看,才真正理解了它们。说白了,它们就是描述一次请求的骨架、环节、细节和上下文。下面我一个个拆开讲。

Trace:整条请求的“全息影像”

Trace 代表一次完整的请求链路。从客户端发起请求,到最终返回响应,中间穿越了多少服务、多少消息队列、多少数据库——所有这些构成一个 Trace。每个 Trace 拥有唯一的 traceId。

我习惯把 Trace 想象成一个电影胶卷:它记录了这一趟旅程的每一帧画面。举个例子,你在电商 App 下单,前端调用订单服务,订单服务调用库存服务、支付服务、短信服务……这一串动作就是一个 Trace。

关键特征:一个 Trace 是树形结构,根节点是入口请求,子节点是后续各步调用。

在实际项目中,我遇到过一个问题:两个不同请求的 traceId 意外重复了,导致监控图上把两个完全不同的事务混在一起。排查了半天才发现是跨服务传递时 header 没清干净。所以,保证 traceId 唯一性 是第一个坑。

Span:请求中的“每一帧”

Span 是 Trace 的基本工作单元,代表一次远程调用、一个方法执行、一段 IO 操作。每个 Span 都有自己的 spanId,以及父 Span 的 parentSpanId,从而构成父子关系。多个 Span 通过这个关系拼接成一颗树——这棵树就是 Trace。

还是下单的例子:订单服务的“创建订单”是一个 Span,它内部调用库存的“扣减库存”是另一个 Span(子 Span),支付服务的“发起支付”又是一个子 Span……

每个 Span 包含:

  • 操作名称(operation name),比如 CheckInventory
  • 起止时间,用于计算耗时
  • 标签(Tags / Attributes),比如 http.method=POST
  • 状态(OK / Error)

小技巧:我一般把 Span 的 operation name 设计成 服务名.接口名 的格式,比如 order.createOrder,这样在链路图中一眼就能看出是哪个服务的哪个操作。

Annotation:时间戳上的“标记点”

Annotation 是 Span 内一些关键时间点的记录。注意,它不是普通的日志,而是带有精确时间戳的“事件”。常见的 Annotation 有四个:

  • cs (Client Send):客户端发送请求
  • sr (Server Receive):服务端收到请求
  • ss (Server Send):服务端发送响应
  • cr (Client Receive):客户端收到响应

通过这四个时间点,我们可以计算出网络延迟和服务器处理时间。比如 cr - cs 是总耗时,ss - sr 是服务端处理耗时,sr - cs 和 cr - ss 是网络传输时间。

我曾经用这些 Annotation 抓到一个诡异的慢调用:总耗时 5 秒,但服务端处理时间只有 200ms,剩下 4.8 秒都是网络传输——后来发现是跨机房专线带宽被打满了。如果没有 Annontation,光看总耗时根本猜不到是网络问题。

注意:现在的 OpenTelemetry 标准中,Annotation 更常被称作 Events,但概念内核是一样的——带时间戳的标记点。如果你在 Zipkin 或 Jaeger 中看到“注解”,指的就是这个东西。

Baggage:跨 Span 的“传声筒”

Baggage 是一组 key-value 元数据,它会随着 Trace 自动传递给所有下游 Span。比如用户 ID、请求来源、实验标签等,你希望在整个链路中都能访问到的信息,就可以塞进 Baggage 里。

实现方式通常是通过传递 HTTP header(例如 baggage 头)或 RPC 元数据。在 OpenTelemetry 中,Baggage 通过 propagation 自动传播。

但注意,Baggage 不是免费的。每个 Span 都要序列化反序列化这些数据,如果塞得太大,会显著增加网络开销。我曾经见过一个团队把整页的 JSON 塞进 Baggage,结果 QPS 掉了一半。后来改成了只放一个 userId 和 traceLevel,性能才恢复。

经验原则:Baggage 里只放 全局上下文标识,比如 userId、sessionId、tenantId,不要放业务数据或临时计算结果。

四大概念的关系一览

下面这张图可以帮你快速理清它们之间的组织关系。我把它们画成了一个树形结构:一个 Trace 包含多个 Span,每个 Span 内部有 Annotation 标记关键时间点,同时可以通过 Baggage 向下传递全局上下文。

Trace / Span / Annotation / Baggage 结构关系 Trace (traceId=123) Span1: order.createOrder spanId: abc Span2: inventory.deduct spanId: def Span3: payment.pay spanId: ghi cs: 10:00:00.000 sr: 10:00:00.050 Baggage: userId=9527 一个 Trace 包含多个 Span;每个 Span 内部可以有多个 Annotation 事件; Baggage 随 Trace 自动沿树向下传播,所有子 Span 均可读取。

核心概念对比表

概念 作用 唯一标识 生命周期
Trace 描述一次完整请求链路 traceId 请求开始到结束
Span 描述一个工作单元(调用、方法、操作) spanId + parentSpanId 当前操作开始到结束
Annotation 标记 Span 内的关键时间点 时间戳 + 事件名 瞬间事件
Baggage 跨 Span 传递全局上下文元数据 key-value 整个 Trace 生命周期有效

实战中的一点体会

这四个概念其实很好记:Trace 是树根,Span 是树枝,Annotation 是树叶上的时间标签,Baggage 是树汁(流动的上下文)。在写代码时,我通常会这样做:

  1. 每个服务入口创建一个新的 Span,并设置 operationName。
  2. 在调用下游之前,记录一个 Annotation("cs");收到响应后记录 Annotation("cr")。
  3. 如果需要传递用户信息,把它放到 Baggage.current 中自动传播。
  4. 使用 traceId 关联所有日志,这样查找问题日志时只要搜一个 traceId 就能看到整条链路。

重要提示:不要用 Baggage 传递敏感信息(如密码、身份证号),因为它在传输中可能被中间件记录,存在泄露风险。如果非要传,务必加密。

嗯,这章内容就到这。下回我们聊如何把这些概念落地到实际代码中——我会带你手写一个简单的分布式追踪 SDK。