跳到主要内容

上下文传播支持

Doubao 中英对照 Context-Propagation Support

目录

自 3.5.0 起,Reactor-Core 内置了对 io.micrometer:context-propagation SPI 的支持。该库旨在作为一种便捷手段,在上下文这一概念的各种实现之间(ContextView/Context 即为一例)以及 ThreadLocal 变量之间进行适配。

ReactorContextAccessor 让上下文传播库能够理解 Reactor 的 Context 与 ContextView。它实现了该 SPI,并通过 java.util.ServiceLoader 加载。除了同时依赖 reactor-core 与 io.micrometer:context-propagation 之外,无需用户进行任何操作。ReactorContextAccessor 类是公开的,但用户代码通常不应直接访问它。

Reactor-Core 支持两种与 io.micrometer:context-propagation 协作的运行模式:

  • default(受限)模式,

  • 以及通过 Hooks.enableAutomaticContextPropagation() 启用的 automatic 模式。请注意,该模式仅适用于新的订阅,因此建议在应用启动时启用此 hook。

下文将在两种场景下讨论它们的关键差异:一是向 Reactor Context 写入数据,二是访问 ThreadLocal 状态,该状态反映了当前已附加的 Subscriber 的 Context 内容,供读取使用。

1. 写入 Context​

根据具体应用的不同,您可能需要将已填充的 ThreadLocal 状态作为条目存入 Context,也可能只需要直接填充 Context。

1.1. contextWrite 操作符​

当需要作为 ThreadLocal 访问的值在订阅时尚不存在(或不需要存在)时,可以立即将其存入 Context:

// assuming TL is known to Context-Propagation as key TLKEY.
static final ThreadLocal<String> TL = new ThreadLocal<>();

// in the main Thread, TL is not set

Mono.deferContextual(ctx ->
Mono.delay(Duration.ofSeconds(1))
// we're now in another thread, TL is not explicitly set
.map(v -> "delayed ctx[" + TLKEY + "]=" + ctx.getOrDefault(TLKEY, "not found") + ", TL=" + TL.get()))
.contextWrite(ctx -> ctx.put(TLKEY, "HELLO"))
.block(); // returns "delayed ctx[TLKEY]=HELLO, TL=null" in default mode
// returns "delayed ctx[TLKEY]=HELLO, TL=HELLO" in automatic mode

1.2. contextCapture 操作符​

当需要在订阅时捕获 ThreadLocal 值,并将这些值反映到 Reactor Context 中以供上游操作符使用时,可以使用此操作符。

与手动的 contextWrite 操作符不同,contextCapture 使用 context-propagation API 获取 ContextSnapshot,然后用该快照来填充 Reactor Context。

因此,如果订阅阶段存在任何 ThreadLocal 值,且存在与其对应的已注册 ThreadLocalAccessor,那么这些值现在就会被存入 Reactor Context,并在运行时对上游操作符可见。

// assuming TL is known to Context-Propagation as key TLKEY.
static final ThreadLocal<String> TL = new ThreadLocal<>();

// in the main Thread, TL is set to "HELLO"
TL.set("HELLO");

Mono.deferContextual(ctx ->
Mono.delay(Duration.ofSeconds(1))
// we're now in another thread, TL is not explicitly set
.map(v -> "delayed ctx[" + TLKEY + "]=" + ctx.getOrDefault(TLKEY, "not found") + ", TL=" + TL.get()))
.contextCapture() // can be skipped in automatic mode when a blocking operator follows
.block(); // returns "delayed ctx[TLKEY]=HELLO, TL=null" in default mode
// returns "delayed ctx[TLKEY]=HELLO, TL=HELLO" in automatic mode
备注

在 automatic 模式下,阻塞操作符(如 Flux#blockFirst()、Flux#blockLast()、Flux#toIterable()、Mono#block()、Mono#blockOptional() 及相关重载)都会透明地执行 contextCapture(),因此在大多数情况下无需再手动添加。

2. 访问 ThreadLocal 状态​

从 Reactor-Core 3.5.0 开始,ThreadLocal 状态会在一小组操作符中被恢复。我们称这种行为为 default(受限)模式。在 3.5.3 中新增了一种模式,即 automatic 模式,它在整个响应式链中都可以访问 ThreadLocal 值。

Reactor-Core 使用存储在 Context 中的值,以及在 ContextRegistry 中按键匹配注册的 ThreadLocalAccessor 实例,来执行 ThreadLocal 状态恢复。

2.1. 用于快照恢复的默认模式操作符:handle 与 tap​

在 default 模式下,如果运行时存在上下文传播库,handle 和 tap 的 Flux 与 Mono 变体的行为都会略有改变。

具体而言,如果它们下游的 ContextView 非空,就会假定已经发生过上下文捕获(无论是手动捕获还是通过 contextCapture() 操作符),并尝试从该快照中透明地恢复 ThreadLocal。对于 ContextView 中缺失的键对应的 ThreadLocal,则保持原样不动。

这些操作符会确保分别围绕用户提供的代码执行恢复:

  • handle 会将 BiConsumer 包装为一个能够恢复 `ThreadLocal`s 的 BiConsumer

  • tap 变体则会将 SignalListener 包装为一个在每个方法周围都有同样包装的 SignalListener(这包括 addToContext 方法)

其意图是让一组极简的操作符透明地执行恢复。因此我们选择了应用场景相当通用、广泛的操作符(一个具备转换能力,一个具备副作用能力)。

//assuming TL is known to Context-Propagation.
static final ThreadLocal<String> TL = new ThreadLocal<>();

//in the main thread, TL is set to "HELLO"
TL.set("HELLO");

Mono.delay(Duration.ofSeconds(1))
//we're now in another thread, TL is not set yet
.doOnNext(v -> System.out.println(TL.get()))
//inside the handler however, TL _is_ restored
.handle((v, sink) -> sink.next("handled delayed TL=" + TL.get()))
.contextCapture()
.block(); // prints "null" and returns "handled delayed TL=HELLO"

2.2. 自动模式​

在 automatic 模式下,所有操作符都会跨 Thread 边界恢复 ThreadLocal 状态。相比之下,在 default 模式下只有部分选定的操作符这样做。

可以在应用启动时调用 Hooks.enableAutomaticContextPropagation() 来启用 automatic 模式。请注意,该模式仅适用于新的订阅,因此建议在应用启动时启用此 hook。

这并非易事,因为 Reactive Streams 规范使响应式链与 Thread 无关。尽管如此,Reactor-Core 仍尽力控制 Thread 切换的来源,并基于 Reactor Context 执行快照恢复,而 Context 被视为 ThreadLocal 状态的事实来源。

注意

default 模式仅将 ThreadLocal 状态限定在作为所选操作符参数执行的用户代码中,而 automatic 模式则允许 ThreadLocal 状态跨越操作符边界。这就需要妥善清理,以避免状态泄漏到复用同一 Thread 的无关代码中。这要求将 Context 中已注册 ThreadLocalAccessor 实例所缺失的键视为清除对应 ThreadLocal 状态的信号。这一点对于空 Context 尤为重要,因为空 Context 会清除所有已注册 ThreadLocalAccessor 实例的状态。

3. 我该选择哪种模式?​

default 与 automatic 两种模式都会影响性能。访问 ThreadLocal 变量会显著影响响应式流水线。如果目标是最高的可扩展性和性能,可以考虑改用更冗长的日志记录方式和显式参数传递,而不是依赖 ThreadLocal 状态。如果您理解并接受在可观测性领域(如 Micrometer、SLF4J)这些成熟库中为了提供有意义的生产级特性而借助 ThreadLocal 状态所做的妥协,那么模式的选择就是另一项需要权衡的决策。automatic 模式相比 default 模式可能更好,也可能更差,这取决于您应用的流程和所使用的操作符数量。唯一能给出的建议是:在您预期的负载下,测量应用的实际表现以及所能获得的可扩展性与性能特征。