分布式链路追踪核心概念: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 | 描述一次完整请求链路 | traceId | 请求开始到结束 |
| Span | 描述一个工作单元(调用、方法、操作) | spanId + parentSpanId | 当前操作开始到结束 |
| Annotation | 标记 Span 内的关键时间点 | 时间戳 + 事件名 | 瞬间事件 |
| Baggage | 跨 Span 传递全局上下文元数据 | key-value | 整个 Trace 生命周期有效 |
实战中的一点体会
这四个概念其实很好记:Trace 是树根,Span 是树枝,Annotation 是树叶上的时间标签,Baggage 是树汁(流动的上下文)。在写代码时,我通常会这样做:
- 每个服务入口创建一个新的
Span,并设置operationName。 - 在调用下游之前,记录一个
Annotation("cs");收到响应后记录Annotation("cr")。 - 如果需要传递用户信息,把它放到
Baggage.current中自动传播。 - 使用
traceId关联所有日志,这样查找问题日志时只要搜一个traceId就能看到整条链路。
重要提示:不要用 Baggage 传递敏感信息(如密码、身份证号),因为它在传输中可能被中间件记录,存在泄露风险。如果非要传,务必加密。
嗯,这章内容就到这。下回我们聊如何把这些概念落地到实际代码中——我会带你手写一个简单的分布式追踪 SDK。