分布式追踪基础: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,从根到任意叶子的路径,就是一次完整的调用链。
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 就是空壳。两者缺一不可。