深入 OpenTelemetry 06 - W3C Trace Context 如何跨越 HTTP 服务
订单服务在 JVM 内有当前 Span,并不意味着库存服务在另一个 JVM 能读到它。一次 HTTP 请求只能传送 header;服务端还必须把 header 还原成父 Context,创建服务端 Span 时显式指定这个父 Context。这里要核对的是 Trace ID 是否继承、parent Span ID 究竟指向哪一个客户端 Span,以及 remote 位何时出现。
模型:线上是字段,线程里是 Context
1 | |
本篇使用 W3C Trace Context Level 1,2021-11-23 Recommendation:traceparent 的 version、Trace ID、parent ID、flags 用连字符分隔。parent ID 指向发起这次 HTTP 调用的客户端 Span,而不是订单入口 Span;tracestate 承载厂商状态,不自动变成 Span attribute。规范描述线上格式;Java 1.31.0 的具体容错分支必须以冻结源码为准。
| 边界 | 输入 | 输出 | 执行线程 |
|---|---|---|---|
| order handler | 新订单请求 | order、client Span | HTTP handler 线程 |
inject |
client 当前 Context、HttpRequest.Builder |
HTTP 请求头 | order handler 线程 |
extract |
库存请求头、root Context | 含 remote 父 SpanContext 的 Context | inventory handler 线程 |
startSpan |
显式父 Context | 库存 server Span | inventory handler 线程 |
1.31.0 源码中发生了什么
TextMapPropagator 定义 carrier/Getter/Setter 边界;接口不是 HTTP 客户端自动拦截器。本实验由 order handler 调用 W3CTraceContextPropagator.inject:从传入的 Context 取 SpanContext,无效则不注入;有效时写入 traceparent,只有非空 TraceState 才写 tracestate。服务端 handler 调用 extract:先读取 traceparent,合法时用 createFromRemoteParent 构造父上下文;不合法就返回传入的 Context,不会凭空继承订单服务的 ThreadLocal。SdkSpanBuilder.startSpan 根据父 Context 的有效性选继承 Trace ID 或新建根 Trace ID。
ContextPropagators.create 可注册 TextMapPropagator 的组合;OpenTelemetrySdkBuilder 初始使用 noop。本篇为观察调用边界直接调用单例 W3C propagator,不靠默认配置自动注入。上游 DefaultPropagatorsTest 核对 noop 的行为。
上游 extract_SampledContext 比较远端父上下文;extract_invalidDelimiters 验证无效头不替换输入 Context。静态源码阅读说明条件与返回值;下述运行只证明本实验选择的有效/无效输入与线程路径,不能推出所有代理都保留了头部。
两个 Java 进程的对照
HttpTraceServices 是教学实现,不是上游 HTTP 自动插桩。Lab06Test 分别启动 order 与 inventory JVM,读取 READY 端口后发送本地 HTTP 请求。关键代码是:
1 | |
在 examples/opentelemetry-java/ 运行 JAVA_HOME=/tmp/otel-20260930/jdk-extract/usr/lib/jvm/java-21-openjdk-amd64 ./mvnw -q -pl sdk-labs -Dtest=Lab06Test test。本次运行的一行原始摘要:
1 | |
测试另外比较库存 Span 的 Trace ID 与客户端 Trace ID,检查库存的新 Span ID 与远端父 ID 不同;向 inventory 直接送 00-not-a-valid-parent 后,父 ID 为全零、remote 为 false,新建根 Trace ID。随机生成的 ID 仅是这一次的观察值,不是稳定断言常量。进程间不共享 Scope;handler 的 try-with-resources 只管理本线程当前 Span,end() 才结束 Span。这里创建了各进程的内存 exporter,响应与断言只覆盖 header/Context 传播,没有跨进程读取 SpanData,更没有发送 OTLP 或验证 Collector 接收。
误解与边界
- “只要同 Trace ID,父 Span 一定是订单 Span”:实际远端 parent 是注入时当前的
inventory-clientSpan ID;少建这个 Span,父关系就会改变。 - “无效头会让服务端报错”:本版本实现返回原 Context;教学实验使用 root,因此库存 Span 成为新根。若传入已有有效 Context,不能直接推广成新根。
- “remote 表示网络导出成功”:remote 只说明该 SpanContext 是从传播头构造的远端父上下文,与 OTLP 发送、接收和存储无关。
练习一。 将 inject 移出 client Scope,重跑测试;预计 parent=client 的断言失败。指出实际 header 中是哪一个 Span ID,并说明为什么原测试需要把注入放在 client Scope 内。
练习二。 把非法 traceparent 改为 55 位但 Trace ID 全零的头,再重跑;观察 remote 和 parent,并用固定 SHA 的 extractContextFromTraceParent 以及 SpanContext.isValid() 解释结果。不要把“合法长度”误当作“有效父上下文”。
导航与参考资料
00 导读 · 04 Scope · 05 线程池传播 · 当前篇:06 W3C Trace Context。下一篇:07 Baggage 与 Span Link(待写)。
参考资料:W3C Trace Context Recommendation 2021-11-23;OpenTelemetry Java SDK c25c0a0ee0da01ab2f74ba83052d1c249ed57020 的上述源码及上游测试;本地运行记录 examples/opentelemetry-java/evidence/06/RUN.md。
