第2章:分布式追踪基本原理 — Span、Trace 与 Context Propagation
做分布式系统,最怕什么?出了问题不知道根在哪。我跟很多团队聊过,大家第一反应就是「上链路追踪」。但链路追踪到底在追踪什么?说白了,就是三个核心概念:Span、Trace 和 Context Propagation。这一章,我把这三个东西掰开揉碎讲清楚。
2.1 Span:追踪的最小单元
Span 是分布式追踪的最小工作单元。你每次发起一个 RPC 调用、执行一条 SQL 查询、或者往消息队列里发一条消息,都可以用一个 Span 来记录。每个 Span 描述了一次有开始和结束的操作。
我习惯把 Span 想象成一个「操作快照」。它记录了什么操作、什么时候开始、什么时候结束、结果如何。一个 Span 的核心属性包括:
| 属性 | 说明 | 示例值 |
|---|---|---|
| name | 操作名称,描述这个 Span 在做什么 | /api/order/create |
| spanId | Span 的唯一标识,64 位或 128 位 | abc123def456 |
| parentSpanId | 父 Span 的 ID,形成树形结构的关键 | parentSpanId: xyz789 |
| traceId | 所属 Trace 的唯一标识,所有 Span 共享 | trace-001 |
| startTime / endTime | 操作开始和结束的时间戳 | 1700000000000 |
| status | 操作结果状态:OK / ERROR / UNSET | OK |
| tags | 键值对,附加业务或技术属性 | http.method=POST |
| events | 时间戳 + 消息,记录关键事件 | 缓存命中 |
我在项目中遇到过一个问题:一个接口偶尔超时,但没法复现。后来我在 Span 里加了 tags,把请求参数和响应大小打进去,一查发现是某个大订单的查询走了全表扫描。嗯,Span 的 tags 用好了,排查效率能翻倍。
service.name、operation.type、business.key。这样不管从哪个维度查,都能快速定位。
2.2 Trace:一次请求的完整视图
一个 Span 只能看到单次操作。但一次请求往往经过多个服务、多个步骤。把这些 Span 按照调用关系串起来,就形成了Trace。
Trace 的本质是一个有向无环图(DAG),或者更具体一点——一棵树。根 Span 是请求的入口,子 Span 是后续的每一步调用。同一 Trace 里的所有 Span 共享同一个 traceId。
你想想看,没有 Trace 的话,你只能看到一个个孤立的 Span。有了 Trace,你才能回答这些问题:
- 这个请求总共花了多长时间?
- 哪个环节最慢?
- 哪个服务抛了异常?
- 调用链路有没有环路或断点?
我记得有一次线上告警说下单成功率下降。我打开 Trace 视图,发现从入口到支付服务之间多了一个没见过的跳转节点。顺着 Trace 树一查,原来是灰度发布配错了路由规则。要是没有 Trace,光看各个服务的日志,可能查到明天都找不到根因。
这里有个容易混淆的点——Trace 不是日志,而是 Span 的组合。日志是离散的,Trace 是有结构的。你可以通过 Trace 还原出请求的完整执行路径。
2.3 Context Propagation:让 Span 不再孤立
Span 和 Trace 听着挺美好,但有个现实问题:服务 A 调用服务 B 的时候,服务 B 怎么知道当前属于哪个 Trace?怎么知道自己的父 Span 是谁?
这就需要Context Propagation(上下文传播)。说白了,就是把 traceId 和 spanId 从调用方传到被调用方。没有这一步,链路就断了。
传播的方式主要有以下几种:
| 传播方式 | 载体 | 典型字段 |
|---|---|---|
| HTTP | 请求头 | traceparent、tracestate |
| gRPC | Metadata | grpc-trace-bin |
| 消息队列 | 消息头 | traceparent |
| 进程内 | 线程局部变量 / 协程上下文 | Context.current() |
目前业界的主流标准是 W3C Trace Context,它定义了 traceparent 和 tracestate 两个 header。前者传递核心的 traceId 和 spanId,后者传递额外的供应商信息。
为什么会丢?因为 Context Propagation 不是自动发生的。你需要在发送请求时从当前线程或协程里取出上下文,塞到请求头里;接收方再从请求头里取出上下文,恢复成新的 Span。这一步遗漏了,链路就断了。
2.4 三者如何协同工作
Span、Trace、Context Propagation 不是三个独立的概念,它们是一个整体:
- Span 是砖块,记录每一次操作。
- Trace 是房子,把所有 Span 组织成一棵完整的调用树。
- Context Propagation 是水泥,让 Span 跨服务也能保持父子关系。
下面这张图展示了三者如何配合:
从图中可以看到:根 Span 是入口,子 Span 分别在不同的服务中执行。Context Propagation 把 traceId 和 parentSpanId 从 API Gateway 传递到 Service A 和 Service B,让它们知道自己的「身世」。
2.5 实战:用 OpenTelemetry 构建第一个 Trace
理论讲完了,上手敲一个。下面是用 OpenTelemetry Python SDK 创建一个 Trace 的示例。代码很简单,但涵盖了 Span、Trace 和 Context Propagation 的核心动作:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
# 1. 初始化 TracerProvider(创建 Trace 的工厂)
provider = TracerProvider()
processor = SimpleSpanProcessor(ConsoleSpanExporter())
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
# 2. 获取 Tracer 实例
tracer = trace.get_tracer("order-service")
# 3. 创建根 Span(开启一个 Trace)
with tracer.start_as_current_span("create_order") as root_span:
root_span.set_attribute("http.method", "POST")
root_span.set_attribute("business.order_id", "ORD-2024-001")
# 4. 在根 Span 下创建子 Span(模拟调用库存服务)
with tracer.start_as_current_span("inventory.deduct") as child_span:
child_span.set_attribute("inventory.sku", "SKU-888")
child_span.add_event("库存扣减成功")
# 5. 另一个子 Span(模拟调用支付服务)
with tracer.start_as_current_span("payment.charge") as child_span2:
child_span2.set_attribute("payment.amount", "99.00")
child_span2.set_status(trace.status.Status(trace.status.StatusCode.OK))
# 6. 程序退出时,Trace 自动导出到控制台
运行这段代码,你会看到控制台输出了三个 Span,它们共享同一个 traceId,并且 child_span 和 child_span2 的 parentSpanId 指向了根 Span 的 spanId。这就是一个完整的 Trace。
start_as_current_span 会自动处理 Context Propagation。在第一个 with 块内部,当前上下文被设为根 Span;第二个 with 块启动时,SDK 自动从上下文中取出父 Span 信息,把新 Span 挂到根 Span 下面。你不需要手动传 traceId 和 parentSpanId。
在真实分布式环境中,跨服务的 Context Propagation 需要手动或通过拦截器处理。比如在 Flask 中,你需要在请求入口处从 traceparent header 中提取上下文,再创建 Span。不过核心原理都是一样的——传递 traceId,维护 parentSpanId。
spanContext 序列化到消息中,执行时反序列化恢复。OpenTelemetry 提供了 propagate 模块专门干这事。
好了,这一章的核心就是这三样东西:Span 打点、Trace 组树、Context Propagation 传上下文。把这三个概念搞明白,后面讲分布式追踪的架构设计、数据采样、存储优化你都能很快上手。