分布式追踪基础:Trace与Span

聊分布式追踪,得先搞懂两个最核心的概念:Trace(追踪) 和 Span(跨度)。说白了,整个追踪体系就是围绕着这俩玩意儿转的。

我刚开始接触这个领域时,也觉得很抽象。后来在线上排查一个慢请求,打开 Jaeger 界面一看,就全明白了。一个请求从进入网关到返回结果,中间调用了七八个服务,每一个调用就是一个 Span,所有这些 Span 串起来,就是一个 Trace。

1. 什么是 Trace

一个 Trace,代表一次完整的请求流。从用户点击按钮,到后端处理完毕返回响应,整条路径上的所有操作,都属于同一个 Trace。

举个例子:用户在你的电商 App 上「点击付款」。这个请求先到 API 网关,然后调用订单服务,订单服务又去查库存、扣余额、发消息。这里面每个步骤产生的日志、耗时、状态,都被一个全局唯一的 Trace ID 串联起来。

核心特点:Trace 是树形结构。根节点是入口请求,子节点是内部调用。所有节点共享同一个 Trace ID。

2. 什么是 Span

Span 是 Trace 的基本单元。一个 Span 代表一次具体的操作。比如「调用订单服务」、「查询数据库」、「发送短信」,都可以是一个 Span。

每个 Span 都包含这些关键信息:

  • Span ID:当前操作的唯一标识
  • Parent Span ID:父操作的 ID,用来构建层级关系
  • Operation Name:操作名称,比如 POST /api/order
  • 开始时间 / 结束时间:记录耗时
  • Tags / Attributes:附加信息,比如 HTTP 状态码、错误信息

你可能会问,为什么要 Parent Span ID?嗯,有了它,你才能把分散在各个服务里的 Span 拼成一棵完整的调用树。我见过不少团队,Span 是打了,但没传 Parent ID,最后只能看到一个一个孤立的点,根本串不起来。

3. Trace 与 Span 的关系

一个 Trace 包含多个 Span,这些 Span 按调用顺序组成树。我习惯用这种结构来理解:

  • 根 Span:用户的入口请求
  • 子 Span:内部调用的下游服务
  • 孙 Span:每个服务内部再调用的数据库、缓存等

想象一棵倒挂的树,根在顶上,叶子在下面。每个节点就是一个 Span,从根到任意叶子的路径,就是一次完整的调用链。

Trace与Span树形结构示例 Trace 与 Span 树形结构 入口请求 (Trace 起点) 服务A - 处理登录 服务B - 创建订单 服务C - 发送通知 数据库查询 订单缓存 消息队列 同一个 Trace ID 下:每个服务输出的 Span 通过 Parent Span ID 串联成调用链 从入口到叶子节点,所有 Span 的集合就是一个完整的 Trace

4. Trace ID 驱动传播

所有分布式追踪系统,核心机制都是一样的:Trace ID 驱动传播。

一个请求进来,网关生成 Trace ID,然后把这个 ID 通过 HTTP 头或消息队列的元数据,传给下游服务。每个服务在处理时,会基于这个 Trace ID 创建自己的 Span,并记录完成时间、状态、异常信息。

我早期在项目里犯过一个错:只传了 Trace ID,没传 Parent Span ID。结果下游服务创建的 Span 全都挂到根节点上,无法还原真实的调用层级。你想想看,一个请求调了 5 个服务,但你看不到谁先谁后,排查问题就变成猜谜。

经验之谈:建议用 W3C tracecontext 协议。它定义了 traceparent 和 tracestate 两个头,自动帮你传递 Trace ID 和 Parent Span ID。大部分框架(如 OpenTelemetry)都原生支持,省去自己造轮子。

5. 关键概念对照表

我把这几个容易混淆的概念整理了一张表。刚入门的时候,这张表帮了我不少忙。

概念 含义 类比 注意点
Trace 一次请求的完整调用链路 一条完整的 IO 请求路径 全局唯一 ID,贯穿所有服务
Span Trace 中的一个独立操作单元 一个函数调用或方法 包含起止时间、状态、标签
Trace ID 唯一标识一次请求 订单号 生成后不可变,全局唯一
Span ID 唯一标识一个 Span 操作流水号 每个 Span 独立生成
Parent Span ID 标识当前 Span 的父节点 上级审批编号 空值表示根 Span

避坑指南:我曾经遇到过 Span ID 生成不随机的情况,导致两个服务生成了相同的 Span ID,追踪系统直接崩溃。记住,Span ID 必须全局唯一,建议用 UUID 或者 Snowflake 算法。

6. 实际应用中的 Span 结构

拿一个 HTTP 请求举例,你可能会看到这样的 Span:

{
  "traceId": "abc123def456",
  "spanId": "001a002b003c",
  "parentSpanId": null,  // 根 Span,无父节点
  "operationName": "GET /api/users",
  "startTime": 1700100000000,
  "endTime": 1700100000120,
  "status": "OK",
  "tags": {
    "http.method": "GET",
    "http.url": "https://example.com/api/users?id=42",
    "http.status_code": 200
  }
}

如果内部调用了数据库,就会多一个子 Span:

{
  "traceId": "abc123def456",
  "spanId": "002a002b003d",
  "parentSpanId": "001a002b003c",  // 指向父 Span
  "operationName": "DB SELECT",
  "startTime": 1700100000050,
  "endTime": 1700100000100,
  "status": "OK",
  "tags": {
    "db.type": "mysql",
    "db.statement": "SELECT * FROM users WHERE id = 42"
  }
}

看到没有?父 Span 开始早、结束晚,子 Span 在父 Span 的区间内执行。这才能精确还原执行顺序。

7. 一句话总结

Trace 是完整的请求路径,Span 是路径上的每一步。

搞懂这两者的关系,后面讲上下文传播、采样、回滚定位,你就能很快上手。我当初就是先把这俩概念刻在脑子里,后面的学习路径才顺了很多。

关键记忆点:没有 Trace,Span 就是孤岛;没有 Span,Trace 就是空壳。两者缺一不可。