链路追踪核心概念:Trace、Span、SpanContext 与分布式传播
做链路追踪这几年,我最大的感受是:概念不搞清楚,后面所有的设计都是空中楼阁。不管你用什么开源框架——Jaeger、Zipkin、SkyWalking,还是自研的 APM,底子都是这几个核心概念。下面我一个一个拆开给你看。
1. Trace:一次完整的请求旅程
Trace 是什么?说白了,就是一次请求从客户端发出,经过网关、微服务 A、微服务 B、缓存、数据库,最后返回给客户端的完整路径。每个 Trace 有唯一的 Trace ID,标识整条链。
打个比方:你下单买东西,订单服务调用库存服务,库存服务查数据库,数据库返回结果给库存服务,库存服务再返回给订单服务。这整个调用链就是一条 Trace。
2. Span:Trace 里的每一小步
Span 是 Trace 的最小工作单元。每个 Span 记录了一个操作:比如一次 RPC 调用、一次 SQL 查询、一个消息发送。Span 有开始时间、结束时间、状态、标签等。
同样用下单的例子:
一条 Trace 包含:
- SpanA:订单服务调用库存服务
- SpanB:库存服务查询数据库
- SpanC:数据库执行 SQL
每个 Span 必须携带它所属的 Trace ID,以及它自己的 Span ID。
Span 的核心字段
| 字段 | 说明 | 我踩过的坑 |
|---|---|---|
| TraceId | 全局唯一的 Trace 标识 | 长度固定 16 字节,别用随机字符串 |
| SpanId | 当前 Span 的标识 | 父子 SpanId 要足够随机,避免碰撞 |
| ParentSpanId | 父 Span 的 ID(根 Span 为空) | 空值处理不当会导致树结构断裂 |
| OperationName | 操作名(如 "POST /order") | 命名风格要统一,我常用 HTTP_METHOD /path |
| StartTime / EndTime | 开始和结束时间戳 | 精度用微秒,纳秒在跨语言时容易丢精度 |
| Tags | 键值对标签(如 http.status_code=200) | 标签数别超过 20 个,否则存储压力大 |
3. SpanContext:Span 的身份证+快递单
SpanContext 是跨服务传递的最小信息包。它包含:
- Trace ID
- Span ID
- 其他传播必需的元数据(如采样标记、Baggage)
为什么要单独抽一个 Context 出来?
因为 Span 对象很大,包含开始时间、结束时间、标签、日志等。在 HTTP 头或消息队列里传递整个 Span 太浪费,所以只传递关键信息——SpanContext。
4. Parent-Child 关系:Span 的家族树
每个 Span 除了有自己 ID,还有父 Span ID。根 Span 的 ParentSpanId 为空。这种父子关系构成了一棵有向无环图(DAG)。
举个例子:
TraceId: abc123
├── Span A (ParentSpanId: 空) —— 根 Span,订单服务接收请求
│ ├── Span B (ParentSpanId: A) —— 查询库存服务
│ │ ├── Span D (ParentSpanId: B) —— 执行 Redis 查询
│ │ └── Span E (ParentSpanId: B) —— 执行 MySQL 查询
│ └── Span C (ParentSpanId: A) —— 发送支付请求
│ └── Span F (ParentSpanId: C) —— 支付回调
这种模型的好处是:你可以精确知道每个操作的耗时和调用顺序。哪个环节慢,一目了然。
但注意:子 Span 可能不是严格按照顺序执行的。比如 Span D 和 Span E 可以是并行的。所以追踪系统需要根据时间戳还原真实时序,不能只依赖父子关系。
5. 分布式传播方式:让 SpanContext 跑起来
分布式传播,就是把 SpanContext 从上游服务传到下游服务。常见的方式有:
5.1 HTTP 传播:通过请求 Header
最经典的做法是在 HTTP 请求头里注入 SpanContext。两个主流协议:
| 协议 | Header 名称 | 示例值 |
|---|---|---|
| W3C Trace Context | traceparent | 00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01 |
| Zipkin B3 | X-B3-TraceIdX-B3-SpanIdX-B3-ParentSpanId | 0af7651916cd43dd00f067aa0ba902b7... 可选 |
5.2 消息队列传播:通过消息属性
对于 RabbitMQ、Kafka 等中间件,SpanContext 会被放入消息的 Header 里。生产者在发送消息前注入,消费者在接收后提取。
// 生产者注入
const headers = {
'traceparent': `00-${traceId}-${spanId}-01`
};
await channel.sendToQueue(queueName, content, { headers });
// 消费者提取
const traceparent = msg.properties.headers['traceparent'];
// 解析 traceparent 得到 traceId, spanId
5.3 gRPC 传播:通过 Metadata
gRPC 原生支持拦截器(Interceptor),可以在客户端和服务端拦截器里自动注入和提取 SpanContext。大部分框架(如 OpenTelemetry)已经内置了 gRPC 传播中间件,开箱即用。
6. 一张图总结核心概念
7. 实践中的两个关键点
第一,不要让 Span 变成性能杀手。 我见过一个团队把每个循环里的数据库查询都做成一个 Span,结果内存和 IO 直接爆炸。适当采样就好,比如 10% 的请求。
第二,一定注意 Baggage 的滥用。 Baggage 是 SpanContext 里可以随请求传递的键值对。有人用它传用户 ID、租户 ID,甚至传 ,导致每次请求 Header 越来越大。我的原则:Baggage 只传链路追踪需要的元数据,业务数据交给业务 Header。
好了,核心概念就这些。有了 Trace、Span、SpanContext 和父子关系,你就能把任意调用的因果关系清晰地记录下来。下一块内容我们会深入实际搭建追踪系统时遇到的坑——传播协议兼容性、采样策略、存储选型等。到时候再跟你细聊。