深入 OpenTelemetry 05 - 线程池异步传播与跨请求隔离
订单请求在入口线程创建父 Span,库存查询提交到 Executor。同一线程池连续处理两个请求时,工作线程读到的“当前 Span”究竟是谁?默认 ThreadLocal 不跨线程,遗漏传播会断链;错误地留住 Context 则可能把后一个请求挂到前一个请求下面。需要分别验证两种风险。
捕获和执行发生在不同时间
1 | |
关键是提交之前捕获当前 Context,而不是在工作线程执行时才调用 Context.current()。未包装的任务在工作线程里创建新 Span 时,没有入口请求的父 Context;即使 JVM 线程 ID 与之前任务相同,也不能因此继承正确父亲。Context 可安全传递的对象与当前线程的激活状态是两回事。
源码中的包装范围
公开 Context.wrap(Callable) 返回一个新 Callable。包装器运行时用 makeCurrent() 在执行线程附加捕获的 Context,再通过 try-with-resources 关闭 Scope;即使任务抛异常也执行关闭。wrap(Runnable) 同理。默认 ThreadLocalContextStorage.attach 在当前线程保留进入前的上下文,Scope.close() 将它恢复。因此包装解决的是一个任务的动态作用域,而不是永久修改线程池的 ThreadLocal。
调用者是入口线程的业务代码,输入是它当前的 Context 与 Callable;输出是包装后的任务,随后由同一工作线程池执行。创建子 Span 的 SdkSpanBuilder.startSpan 默认从执行时的当前 Context 取父信息:未包装时生成新的 Trace ID,包装时与入口 Span 同 Trace ID、parent Span ID 指向入口 Span。上游 ContextTest.wrapCallable 与 wrapRunnable 检查包装执行;默认 storage 的非当前 Scope 关闭限制见 实现第 47 行。这不是 Reactor、CompletableFuture 或 Agent 自动传播的证据。
同一个 worker 的 A、B 对照
运行 ./mvnw -q -pl sdk-labs -Dtest=Lab05Test test。Lab05Test 使用专用单线程池和有期限的 Future.get(5, TimeUnit.SECONDS),不靠 sleep 猜任务是否完成。请求 A、B 各建一个入口 Span,分别提交未包装任务与在入口线程包装过的任务。测试检查工作线程 ID 相同;未包装任务的 parent ID 无效、Trace ID 不等于入口;包装任务的 parent ID 等于对应请求父 Span 的 ID。A 的包装任务还刻意抛异常,之后检查工作线程的当前 Context 无有效 Span;处理 B 时再次检查,不可串到 A。
1 | |
这个输出只是断言集合的摘要,六条 Span 的名字、Trace ID 与父 ID 的具体断言写在 Lab05Test。测试在关闭 provider 前读取内存列表;没有外部 RPC、OTLP exporter 或真实库存服务。若遗漏包装,断链发生在“工作线程读取当前上下文”这一边界,而不是 exporter 的网络层。
捕获的边界
包装任务时捕获的是当时的 Context。若在请求结束后错误地缓存同一个包装对象并拿给下一请求复用,它仍会恢复旧的上下文;wrap 不会识别“当前业务请求是谁”。未包装断链与错误包装串链,诊断方式不同:检查提交点的捕获时机、执行线程的父 ID、作用域关闭后是否恢复初始 Context。第 04 篇的隔离泄漏反例说明,忘关 Scope 会在同一个工作线程污染下一任务;本篇正常路径使用包装器的 try-with-resources 避免这种情况。
练习一。 将 Context.current().wrap(...) 移到工作线程内部,再执行 Lab05Test,解释包装为何无法恢复提交线程的父 Span。
练习二。 将 A 的包装任务保存在变量中,在 B 的 Scope 中再次提交它;观察父 ID 指向 A 还是 B,并在运行结束后确认 worker 上下文清理。该反例验证“捕获一次、用在另一请求”的串链风险。
系列导航与资料
00 导读 · 01 初始化 · 02 字段归属 · 03 生命周期 · 04 Scope · 当前篇:05 线程池异步传播。下一篇:06 W3C Trace Context(待写)。
参考资料:OpenTelemetry Java API、固定 SHA 的 Context 与 SDK 实现及测试(Apache-2.0);教学实验 Lab05Test 的本机运行记录见 examples/opentelemetry-java/evidence/05/。
