深入 OpenTelemetry E01:Reactor 切线程后父 Span 去哪了
在订阅前 Span.current() 有效,不代表 Reactor 的操作符换到另一线程后仍能读到同一个父 Span。相反,某一次自动插桩确实恢复了父上下文,也不意味着所有 CompletableFuture、虚拟线程和取消回调都自动获得相同语义。E01 要回答的是:哪个边界由代码显式传播,哪个边界由这一次固定版本的 Agent 插桩恢复?
本篇固定完整 JDK 21.0.12、SDK 1.31.0(源码 SHA c25c0a0ee0da01ab2f74ba83052d1c249ed57020)、instrumentation/Agent 1.31.0(源码 SHA 97d87f3f3b2bc61aa396d71a98387a1e857618af)。新引入的仅测试依赖 io.projectreactor:reactor-core:3.6.5 与 POM 的 org.reactivestreams:reactive-streams:1.0.4;两个本机 JAR 的 SHA-256、字节码主版本及发行包信任限制见 writing-plans/opentelemetry-java/VERSIONS.md。源码 muzzle 覆盖版本号不是运行成功;只有 Agent 子 JVM 的断言通过才是这一个 Reactor/JDK 组合的运行证据。
从 Context 到 Reactor Hook
固定 SDK SHA 的 Context.java L200–225 将 makeCurrent 与 wrap(Callable) 分开:Scope 作用于当前线程;wrap 捕获提交时的 Context,在任务执行线程短暂 attach 并清理。没有给线程池提交任务时主动传播 Context,换线程不是“自动继承父 Span”。CompletableFuture 的异步 supplier 和 Thread.ofVirtual().start(...) 在这篇都显式套 wrap,不把 JDK 的线程类型当隐式传播承诺。
固定 instrumentation SHA 的 HooksInstrumentation.java L20–53 匹配 Reactor Hooks/Flux/Mono 并注册 ContextPropagationOperator;ContextPropagationOperatorInstrumentation.java L27–63 对存取 Reactor Context 及 runWithContext 的接口织入转换逻辑。模块 build.gradle.kts L5–18 标了 muzzle [3.1.0.RELEASE,),但本机没有跑 Gradle muzzle;上游 ContextPropagationOperatorInstrumentationTest.java L104 的 Mono 用例和 BaseMonoWithSpanTest.java L57 的嵌套用例仅作静态阅读,没有在本机执行它们。
三个进程,三种控制
E01Test 依次启动三个独立 JVM。unwrapped 和 explicit 不带 Agent,只有各进程自己的测试 provider 与内存出口;agent JVM 挂已冻结的 Agent 1.31.0,仅通过其全局 API 获取 tracer,不初始化第二套全局 SDK。三种场景都在 parent Scope 内订阅 Mono.fromCallable(...).subscribeOn(Schedulers.single()):成功读取当前 Trace ID;抛出 IllegalStateException 后经 onErrorResume 处理;Mono.create 在 scheduler 上注册取消回调,以闩锁确认订阅已进入,再 dispose 并等待回调。返回父 Scope 之后,在相同 scheduler/CompletableFuture worker 上再次读取 Current Span,断言已清理。
1 | |
| 模式 | Reactor 成功/异常/取消入口是否见父 Trace | Future/虚拟线程 | 后续 worker 清理 |
|---|---|---|---|
| unwrapped,无 Agent | false / false / false | 显式 wrap → true / true | true |
| explicit,无 Agent | true / true / true | 显式 wrap → true / true | true |
| agent,仅 Reactor 路径不显式 wrap | true / true / true | 仍显式 wrap → true / true | true |
工程目录执行 JAVA_HOME=/tmp/otel-20260930/jdk-extract/usr/lib/jvm/java-21-openjdk-amd64 ./mvnw -pl sdk-labs -Dtest=E01Test test,退出码 0;三行模式输出与 Agent parent 的教学 OTLP/HTTP protobuf 接收数 1 见 examples/opentelemetry-java/evidence/E01/RUN.md。异常分支核的是抛异常的 Callable 读取到的 Trace ID;取消分支核的是 scheduler 中 Mono.create 订阅入口读到的 ID 和 onCancel 已触发,不是说 onCancel 回调总在 scheduler 运行。所有异步等待有 5s/40s 期限,不靠睡眠。
不要把一个传播结果推广给另一种调度
常见误解是“Agent Reactor Hook 开启,就能保证任何 CompletableFuture 和虚拟线程也自动恢复父 Span”。表中 Future/虚拟线程的 parent=true 是显式 wrap 的结果,不能拿来证明 Agent 自动接管这两种路径;另一个误解是“cancel 无业务结果,所以无法验证 Context”,本篇通过订阅入口与取消信号/后续 worker 清理分开断言。此处只有三类受控任务、同一 Reactor single scheduler、单个 Agent 发行版本;没有验证任意 operator、异步取消竞态、生产线程池或后端完整性。Agent parent 的 OTLP 只到本篇教学端点,不是 Collector/Jaeger 查询。
两道练习:
- 修改
E01Probe,去掉 CompletableFuture 的显式Context.current().wrap,分别在无 Agent 与 Agent 子进程测试 parent 和后续 worker 清理;以这一次的结果定义有限范围内的自动传播,不预设所有 JDK Executor 都一样。 - 把 Reactor 的
Mono.create改成publishOn后链一个真正异步的可取消上游,用闩锁让取消与发出信号交错,记录发出、取消、Scope attach/close 的真实顺序;给出至少一种“先取消却仍回调”的反例,不能用任意 sleep 当时序保证。
参考资料:Java SDK Context(固定 SHA)、Reactor Agent Hook(固定 SHA)、上游 Mono 测试(固定 SHA)、examples/opentelemetry-java/evidence/E01/RUN.md。
导航:29 综合故障诊断 · 当前篇:E01 Reactor 与虚拟线程传播。
