溯源链路基本概念:Trace、Span、Context,调用链模型

做溯源链路,你得先弄明白几个核心概念。说白了,它们就像盖楼的砖、水泥和图纸。我最早接触这个东西,是在排查一个线上慢接口时,日志散落得到处都是,根本串不起来。后来才发现——原来缺的就是一套标准化的链路模型。

Trace —— 一次请求的完整旅途

Trace 代表一次完整的请求处理过程。从客户端发起,到后端多个服务接力,最后返回响应——整条路径就是一个 Trace。

每个 Trace 有一个全局唯一的 Trace ID,你可以把它当作快递单号。有了它,所有参与这次请求的服务都能通过这个 ID 关联起来。

核心特征:
  • Trace ID 全局唯一,通常由网关或入口服务生成
  • 覆盖请求经过的所有微服务、中间件、数据库
  • 一条 Trace 由多个 Span 组成

Span —— 链路中的最小工作单元

Span 是 Trace 里的一个工作片段。一次 RPC 调用、一次数据库查询、一段业务逻辑——都可以用一个 Span 来表示。

每个 Span 也有自己的 ID,并且会记录它的父 Span ID。这样一来,Span 之间就形成了树状层级关系。我遇到过不少新手,以为只要记个 Trace ID 就够了,结果定位慢请求时还得人工猜调用顺序——这就是忽略了 Span 父子关系的后果。

属性 说明 我常用的字段名
spanId 当前 Span 唯一 ID uuid 短格式
parentSpanId 父 Span ID,根 Span 为空 直接关联上一级
operationName 操作名称,如 POST /api/order 动词 + 资源路径
startTime / duration 开始时间与耗时 毫秒时间戳
tags 关键标签,如 responseCode、error 按需打标

Context —— 链路上的“快递员”

Context 负责把 Trace ID 和 Span ID 在服务之间传递。没有 Context,就算你记录了日志,也连接不起来。

常见的传递方式:HTTP 请求头、gRPC metadata、消息队列的 header。我自己在项目里,一般会在入口拦截器里自动注入 x-trace-id 和 x-span-id,保证后续所有子调用都能继承上下文。

一个小技巧: 在异步线程池里要显式传递 Context,否则子线程会丢失父链路的 Trace ID。我踩过这个坑,排查了一下午才发现是线程池没处理好。

调用链模型 —— 它们怎么组合在一起?

把这几个概念合起来,就是调用链模型。我一般这样理解:

  1. 用户请求到达网关,生成 Trace ID,创建根 Span。
  2. 网关调用服务A,通过 Context 传递 Trace ID + 当前 Span ID 给服务A。
  3. 服务A收到后,创建子 Span,把父 Span ID 设为网关传过来的 Span ID。
  4. 服务A调用服务B,同样传递 Context…… 如此递归。
  5. 所有 Span 上报到链路系统(如 Jaeger / Zipkin),按 Trace ID 聚合,按父子关系排列,就形成了完整的调用链。
注意: Context 的传递协议必须统一。我曾经见过一个团队,HTTP 用 header 传,但 gRPC 没做拦截器,导致一部分链路永远断着。要么全用 OpenTelemetry 的标准,要么自己实现一套一致的传递机制。

一张图看懂调用链模型

下面是 Trace、Span、Context 的关系图。你可以看到:一个 Trace 里嵌套多个 Span,Context 沿着箭头在服务间流转。

调用链模型:Trace、Span、Context Trace (traceId=abc123) —— 根 Span (spanId=root) Span A:网关调用服务A Span B:服务A调用服务B Span A-1:查询数据库 Span B-1:缓存查询 Context 传递:顺着箭头方向,携带 traceId + parentSpanId HTTP Header: x-trace-id: abc123 ; x-span-id: spanA 每次调用时,接收方用父 spanId 作为 parentSpanId,生成新的子 span

实际生产中的例子

假设有一个下单接口。用户点击下单后,链路大致是:

  1. API 网关收到请求,生成 Trace ID = t-001,创建根 Span。
  2. 网关调用订单服务,HTTP header 里带上 traceparent: 00-t-001-root-span。
  3. 订单服务收到后,创建子 Span,执行库存扣减(再调用库存服务),再次传递 Context。
  4. 库存服务执行完,把结果、耗时、状态码打标到自己的 Span,并上报。
  5. 最终所有 Span 按 Trace ID 聚合,展示出完整的调用链:网关→订单→库存。
为什么你需要在代码里显式记录 Context? 我写过一段 Python 代码,用 OpenTelemetry SDK 搞定这个事情,核心就是几行拦截器。后面章节会专门讲实现,你先记住这个传递模型。

清楚了 Trace、Span、Context,你就掌握了溯源链路的骨架。剩下的就是怎么埋点、怎么传输、怎么存储。心里有了这个模型,后面编码才不会跑偏。