bigywater.tech
微服务 · 61

trace、span、context 与 W3C traceparent 头:一次调用怎么串起来,请求 ID 怎么传

traceparent 头里到底有什么?一次调用怎么在几个服务之间串成一棵树?中间一跳忘了传会怎样?baggage 和 tracestate 怎么传?收到格式不对的头 SDK 怎么办?这篇用 OpenTelemetry Python SDK 和三个真实的 HTTP 服务逐项试出来。

1 个 trace_id,5 个 span
三个服务正确传播时,一次调用留下的 trace_id 个数与 span 个数
2 个 trace
中间一跳 B 不转发 traceparent 时,A 与 C 分成的 trace 个数
8 / 11
收到的 11 种 traceparent 里,SDK 丢掉并另起新 trace 的种数

traceparent 与父子关系

实测 · 本机执行输出
  服务 | span 名   | 类型     | trace_id  | span_id          | parent_span_id  
  ---+----------+--------+-----------+------------------+-----------------
  A  | A handle | SERVER | 0f2cf66d… | 66a3413344054327 | (根)             
  A  | A call   | CLIENT | 0f2cf66d… | 7baa12638f1c8308 | 66a3413344054327
  B  | B handle | SERVER | 0f2cf66d… | 7a9eee88cdffcdac | 7baa12638f1c8308
  B  | B call   | CLIENT | 0f2cf66d… | dc2d8129d88e93d7 | 7a9eee88cdffcdac
  C  | C handle | SERVER | 0f2cf66d… | b43d3b8a9c3667b1 | dc2d8129d88e93d7
  这次调用里不同的 trace_id 个数:1
  服务 | 收到的 traceparent                                         | 收到的 X-Request-Id
  ---+---------------------------------------------------------+-----------------
  A  | (无)                                                     | req-123         
  B  | 00-0f2cf66d652cac053fbad118b2ab6ff0-7baa12638f1c8308-03 | req-123         
  C  | 00-0f2cf66d652cac053fbad118b2ab6ff0-dc2d8129d88e93d7-03 | req-123         
  C 收到的 traceparent 的四段:version=00,trace-id=0f2cf66d652cac053fbad118b2ab6ff0(32 个十六进制字符),parent-id=dc2d8129d88e93d7(16 个),trace-flags=03。A 没有收到 traceparent,B 和 C 收到的 trace-id 相同、parent-id 不同:parent-id 是上一跳发出请求的那个 CLIENT span 的 span_id
  C 收到的 parent-id 对应的 span:B 的 “B call”

trace-id 一路不变,每一跳的 parent-id 是上一跳 CLIENT span 的 span_id;这个 SDK 版本的 trace-flags 是 03。

不传播会断链

实测 · 本机执行输出
  情形                         | trace 关系           | C 的 span         | C 收到的 X-Request-Id
  ---------------------------+--------------------+------------------+-------------------
  (a)B 不传 traceparent,传请求 ID | A 与 C 是两个不同的 trace | C 的 span 是根      | req-777           
  (b)B 传 traceparent,不传请求 ID | A 与 C 同一个 trace    | C 的 span 有父 span | (没有)              
  (c)B 两个都不传                 | A 与 C 是两个不同的 trace | C 的 span 是根      | (没有)              
  上下文只通过请求头传播:中间一跳没把 traceparent 带出去,下游就从一个新的 trace 开始,链路在这里断开;请求 ID 头同样要每一跳自己转发,SDK 不会替你转

上下文只通过请求头传播,中间一跳不转发,下游就另起一个 trace;请求 ID 头也要每一跳自己转发。

baggage 与 tracestate

实测 · 本机执行输出
  (a)上下文只作为 context 参数传入:
  服务 | traceparent                                             | tracestate               | baggage    | 三个头的字符数
  ---+---------------------------------------------------------+--------------------------+------------+--------
  A  | 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 | vendor1=opaque,vendor2=x | user=alice | 89     
  B  | 00-4bf92f3577b34da6a3ce929d0e0e4736-546d58d166c3f735-01 | vendor1=opaque,vendor2=x | (无)        | 79     
  C  | 00-4bf92f3577b34da6a3ce929d0e0e4736-cd9479ca94faeb09-01 | vendor1=opaque,vendor2=x | (无)        | 79     
  (b)先 attach 提取出来的上下文:
  服务 | traceparent                                             | tracestate               | baggage    | 三个头的字符数
  ---+---------------------------------------------------------+--------------------------+------------+--------
  A  | 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 | vendor1=opaque,vendor2=x | user=alice | 89     
  B  | 00-4bf92f3577b34da6a3ce929d0e0e4736-47dc25bb1f754e39-01 | vendor1=opaque,vendor2=x | user=alice | 89     
  C  | 00-4bf92f3577b34da6a3ce929d0e0e4736-2bec85dc504703d2-01 | vendor1=opaque,vendor2=x | user=alice | 89     
  tracestate 跟着 span 上下文走,baggage 是独立的上下文内容:只把提取出来的上下文当作 start_as_current_span 的父上下文传进去,新 span 的父子关系是对的,但是当前上下文里没有 baggage,发给下游的请求头里就没有它;要先 attach 才行

tracestate 跟着 span 上下文走;baggage 在当前上下文里,只把提取出的上下文传给 start_as_current_span 不够,要先 attach。

格式不对的头

实测 · 本机执行输出
  请求带的 traceparent | 值(截取)                                           | SDK 的处理                            
  -----------------+-------------------------------------------------+------------------------------------
  合法,已采样(flags=01) | 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0b… | 延续了这个 trace(parent 是请求里的 parent-id)
  合法,未采样(flags=00) | 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0b  | 没有导出任何 span(未采样,span 没有被记录)        
  十六进制用了大写         | 00-4BF92F3577B34DA6A3CE929D0E0E4736-00F067AA0B… | 另起新 trace(根 span)                  
  trace-id 全 0     | 00-00000000000000000000000000000000-00f067aa0b… | 另起新 trace(根 span)                  
  parent-id 全 0    | 00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000… | 另起新 trace(根 span)                  
  version=ff(非法)   | ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0b… | 另起新 trace(根 span)                  
  version=01 且多出字段 | 01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0b… | 延续了这个 trace(parent 是请求里的 parent-id)
  缺 trace-flags    | 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0b… | 另起新 trace(根 span)                  
  trace-id 太短      | 00-4bf92f3577b34da6-00f067aa0ba902b7-01         | 另起新 trace(根 span)                  
  尾部有多余字符          | 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0b… | 另起新 trace(根 span)                  
  随便一个字符串          | hello                                           | 另起新 trace(根 span)                  
  格式不对的 traceparent 会被丢掉,服务从一个新的 trace 开始,不会报错;“未采样”标志(flags=00)会让默认采样器也不记录这条 span
ERRORS {}

格式不对的 traceparent 被丢掉,服务另起新 trace,不报错;flags=00 让默认采样器不记录 span。

分步演示

trace 与 traceparent
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。OpenTelemetry Python SDK 1.45.1;三个服务是本进程里的 HTTP 服务,每个服务一个独立的 TracerProvider 和内存导出器,上下文只通过 HTTP 头传递;trace_id 和 span_id 每次运行都不同,正文里的数值取自最后一次运行;传播代码是我手写的(没有用 requests / flask 的自动插桩);每种情形测 1 次
    • 没有验证:自动插桩库(opentelemetry-instrumentation-*)的行为;B3、Jaeger 等其他传播格式;Java / Go SDK 对异常 traceparent 的处理(只测了 Python);trace-flags 里 0x02 位(这个版本发出 03)在其他实现里的含义;baggage 的大小限制与敏感信息泄漏;消息队列场景的传播(见下一篇)
    京ICP备2026067502号-1京公网安备11010502063487号