Project Reactor 与 Tokio:异步底座、Linux 内核共性,以及 Spring AI 流式 Advisor

最近在对照 JVM 上 Spring AI 的流式聊天,和 Rust 里 Tokio 网络服务时,很容易把两套 API 当成「两个平行世界」。它们确实不等价:一个是 Reactive Streams 库,一个是完整 async runtime。但在 Linux 上,两者最终都会落到同一组内核设施——epoll 事件通知、hrtimer 定时精度、futex 阻塞唤醒。下面把用户态库 / runtime、内核 syscall、Spring AI 应用串成一条链路,并说明 StreamAdvisor 如何用 Flux 承载 LLM token 流——不依赖 WebFlux

源码钉在本地核对过的 commit:reactor-core 95957bd8dtokio e77885a4spring-ai d71200317;内核引用本机 linux 树 6d35786de281fs/eventpoll.c


1. 分层地图:库 vs 运行时

Reactor 常被说成「JVM 的响应式编程库」,Tokio 则是「Rust 的异步运行时」。更精确地说:reactor-core 只定义 Publisher/Subscriber 与 Flux/Mono 组合子;真正碰网卡、调 epoll_wait 的是 Reactor Netty(Spring WebClient 默认传输)。Tokio 则把 I/O driver(mio)、scheduler、timer 打包进同一个 Runtime1。因此对比应写成「Reactor 生态 + Netty」对「Tokio 全家桶」,而不是 reactor-coretokio 一行 API。

flowchart TB
  subgraph jvm [JVM_Stack]
    AppJ[SpringAI_ChatClient]
    FluxJ[reactor_core_Flux]
    NettyJ[Reactor_Netty_EpollEventLoop]
    AppJ --> FluxJ
    FluxJ --> NettyJ
  end

  subgraph rust [Rust_Stack]
    AppR[Tokio_App]
    FutR[Future_poll]
    TokioR[Tokio_Runtime]
    MioR[mio_Poll]
    AppR --> FutR
    FutR --> TokioR
    TokioR --> MioR
  end

  subgraph kernel [Linux_Kernel]
    Epoll[epoll_wait]
    Hrtimer[hrtimer]
    Futex[futex]
    Sched[schedule]
  end

  NettyJ --> Epoll
  MioR --> Epoll
  NettyJ --> Hrtimer
  MioR --> Hrtimer
  TokioR --> Futex
  Epoll --> Sched
JVM(Spring AI 路径) Rust(Tokio 路径)
应用 ChatClient.stream() tokio::main + .await
流组合 reactor-core Flux Future + combinators
I/O 传输 Reactor Netty EpollEventLoop Tokio IoDrivermio::Poll
阻塞 / park LockSupport.park(→ futex) worker park(→ futex)
内核 epoll_wait、timed wait 同左

2. Linux 内核共性:epoll

Reactor Netty 与 Tokio mio 在 Linux 上共享 epoll 事件环。用户态 epoll_wait(2) 进入内核 fs/eventpoll.cSYSCALL_DEFINE4(epoll_wait)do_epoll_waitep_poll2。socket 就绪时,网络栈经 sk_wake_async 走到 wait queue 回调 ep_poll_callback,把就绪 fd 链进 ready list,唤醒卡在 epoll_wait 上的线程。Tokio 系列文已跟踪 park_timeout → epoll_wait3;这里补一句:JVM 侧经 Reactor Netty 也落在同一条 syscall

// fs/eventpoll.c L2442–(内核侧 epoll_wait)
static int do_epoll_wait(int epfd, struct epoll_event __user *events,
                         int maxevents, struct timespec64 *to)
{
    // ...
    return ep_poll(ep, events, maxevents, to);
}
// fs/eventpoll.c L1249–
static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
    struct epitem *epi = ep_item_from_wait(wait);
    struct eventpoll *ep = epi->ep;
    // ... 把 epitem 链入 ready list,唤醒 epoll waiter ...
}

定时:Tokio 时间轮只向内核登记「最近 deadline」,epoll_wait(timeout) 顺带睡;到期靠 hrtimer 叫醒(见 hrtimer 分析4)。Reactor Netty 的 HashedWheelTimer 在用户态批量收割,底层 timed wait 同样依赖 hrtimer。

阻塞:worker「无事可做」时的 park,以及 JVM LockSupport.park,慢路径落到 futex_wait / futex_wake(见 futex 文5)。协作式调度在用户态;内核只在 I/O 未就绪或显式阻塞时 schedule()

sequenceDiagram
    participant App as UserApp
    participant RT as Tokio_or_ReactorNetty
    participant Kernel as LinuxKernel
    participant NIC as NetworkStack

    App->>RT: poll_or_subscribe
    RT->>Kernel: epoll_wait(timeout)
    Note over Kernel: ep_poll blocks via wait_queue
    NIC->>Kernel: TCP segment arrives
    Kernel->>Kernel: sk_wake_async then ep_poll_callback
    Kernel-->>RT: epoll_wait returns readable
    RT-->>App: wake task or emit Flux chunk

边界:macOS/BSD 上 mio 用 kqueue;Tokio 的 io-uring feature6 非默认。reactor-core 本身不含 epoll——汇聚点在 Reactor Netty / WebClient 传输层


3. Project Reactor:订阅与背压

Project Reactor 实现 Reactive StreamsPublisher 发、Subscriber 收,用 Subscription.request(n) 做背压。Flux<T> 是 0..N,Mono<T> 是 0..1。组合子在用户态搭流水线,不绑死某一种 I/O——所以 Spring AI 能在 非 WebFlux 应用里用 Flux 读 SSE。

订阅入口在 Flux.subscribe:先把外部 Subscriber 收成 CoreSubscriber,再沿 operator 链下钻,最后落到具体 Publisher7

// reactor-core/.../Flux.java L8859–8894(95957bd8d)
@Override
public final void subscribe(Subscriber<? super T> actual) {
    CorePublisher publisher = Operators.onLastAssembly(this);
    CoreSubscriber subscriber = Operators.toCoreSubscriber(actual);
    try {
        if (publisher instanceof OptimizableOperator) {
            OptimizableOperator operator = (OptimizableOperator) publisher;
            while (true) {
                subscriber = operator.subscribeOrReturn(subscriber);
                if (subscriber == null) {
                    return;
                }
                // ... 继续沿 OptimizableOperator 链 ...
            }
        }
        subscriber = Operators.restoreContextOnSubscriberIfPublisherNonInternal(publisher, subscriber);
        publisher.subscribe(subscriber);
    }
    catch (Throwable e) {
        Operators.reportThrowInSubscribe(subscriber, e);
    }
}

日常写 flux.subscribe(x -> …) 时,默认 LambdaSubscriberonSubscribe 里若没自定义 subscription 回调,会直接 request(Long.MAX_VALUE)——也就是「无界请求」,背压靠下游自己消化:

// reactor-core/.../LambdaSubscriber.java L109–125(95957bd8d)
public void onSubscribe(Subscription s) {
    if (Operators.validate(subscription, s)) {
        this.subscription = s;
        if (subscriptionConsumer != null) {
            subscriptionConsumer.accept(s);
        }
        else {
            s.request(Long.MAX_VALUE);
        }
    }
}

需要有界拉取时,用 BaseSubscriber 显式 request(n)

// reactor-core/.../BaseSubscriber.java L212–227(95957bd8d)
@Override
public final void request(long n) {
    if (Operators.validate(n)) {
        Subscription s = this.subscription;
        if (s != null) {
            s.request(n);
        }
    }
}

public final void requestUnbounded() {
    request(Long.MAX_VALUE);
}

Operators.validate(n) 拒绝 n <= 0;非 CoreSubscriber 会被包成 StrictSubscriber,保证 onSubscribe 里先设状态再 request7

WebFlux 基于 Reactor,但 StreamAdvisor 只依赖 reactor-coreFlux,不必把 Controller 写成 Mono<ServerResponse>。HTTP 仍是 WebClient + Reactor Netty;Advisor 在 Flux 层组合。


4. Tokio:PollEvented → mio → park

Tokio 把 runtime 拆成 I/O driver、scheduler、timer1。I/O 类型经 PollEvented 注册到 reactor;真正挂 fd 的是 RegistrationIoDriver::add_sourcemio::Registry::register8

// tokio/src/io/poll_evented.rs L114–125(e77885a4)
pub(crate) fn new_with_interest_and_handle(
    mut io: E,
    interest: Interest,
    handle: scheduler::Handle,
) -> io::Result<Self> {
    let registration = Registration::new_with_interest_and_handle(&mut io, interest, handle)?;
    Ok(Self {
        io: Some(io),
        registration,
    })
}
// tokio/src/runtime/io/driver.rs L266–289(e77885a4)
pub(super) fn add_source(
    &self,
    source: &mut impl mio::event::Source,
    interest: Interest,
) -> io::Result<Arc<ScheduledIo>> {
    let scheduled_io = self.registrations.allocate(&mut self.synced.lock())?;
    let token = scheduled_io.token();
    self.registry.register(source, token, interest.to_mio())?;
    Ok(scheduled_io)
}

worker 无事可做时,IoDriver::park / park_timeout 进入 turn,核心是 self.poll.poll(events, max_wait)——Linux 上即 mio 封装的 epoll_wait:

// tokio/src/runtime/io/driver.rs L159–198(e77885a4)
pub(crate) fn park(&mut self, rt_handle: &driver::Handle) {
    let handle = rt_handle.io();
    self.turn(handle, None);
}

pub(crate) fn park_timeout(&mut self, rt_handle: &driver::Handle, duration: Duration) {
    let handle = rt_handle.io();
    self.turn(handle, Some(duration));
}

fn turn(&mut self, handle: &Handle, max_wait: Option<Duration>) {
    // ...
    match self.poll.poll(events, max_wait) {
        Ok(()) => {}
        Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
        Err(e) => panic!("unexpected error when polling the I/O driver: {e:?}"),
    }
    // ... 按 token 分发就绪事件,唤醒对应 task ...
}

和 Reactor 的 subscribe / request 不同,Tokio 用 Waker 把 task 重新入队——语义不同,但「等 I/O → epoll 叫醒 → 继续推进」一致。入门见 async/await 与 Tokio9;I/O 驱动细节见 Tokio I/O driver3


5. 「响应式」三层含义与对照

日常「响应式」至少三层,混用会吵不清楚:

依据 要点 Project Reactor Tokio
Reactive Systems Reactive Manifesto10 Responsive / Resilient / Elastic / Message Driven 生态目标;Flux 作消息流 事件驱动;mpsc
Reactive Streams reactive-streams.org11 Publisher/Subscriber/Subscription.request(n) 实现 JVM 规范;Flux/Mono 是 Publisher12 不实现该契约
异步 / 事件驱动 Tokio runtime1Future::poll 非阻塞 I/O、协作调度 靠 Netty + Schedulers 核心设计

Tokio 算不算「响应式」?

flowchart LR
  subgraph broad [Reactive_Systems_Manifesto]
    RS[Responsive_Resilient_Elastic_MessageDriven]
  end

  subgraph spec [Reactive_Streams_Spec]
    Pub[Publisher]
    Sub[Subscriber]
    Req[Subscription_request_n]
    Pub --> Sub
    Sub --> Req
  end

  subgraph impl [Implementations]
    Reactor[Project_Reactor_Flux]
    Tokio[Tokio_Future_mpsc]
  end

  broad --> Reactor
  broad --> Tokio
  spec --> Reactor
  Tokio -.->|no_spec_impl| spec
关注点 Project Reactor Tokio
组合 Flux operators Future + async/await
非阻塞 I/O Reactor Netty IoDriver + mio
调度 Schedulers.parallel() work-stealing / current-thread
流控 Reactive Streams 背压 channel、semaphore、Stream
运行时范围 库(+ 另配 Netty) 捆绑 Runtime
Linux 内核 epoll + hrtimer + futex 同左

把 Spring AI 的 Flux 说成「WebFlux 流式」容易误导——更准确是 Reactive Streams 流式;HTTP 层碰巧用了 Reactor Netty。


6. Spring AI:StreamAdvisor 双链

Spring AI 2.x 把 callstream 拆成两条链:DefaultAroundAdvisorChain 里有 callAdvisors / streamAdvisors 两个 Deque.call()nextCall().stream()nextStream()注册可合并、执行不串台1415

StreamAdvisor 约定返回 Flux<ChatClientResponse>16

// spring-ai-client-chat/.../StreamAdvisor.java(d71200317)
public interface StreamAdvisor extends Advisor {
    Flux<ChatClientResponse> adviseStream(
        ChatClientRequest chatClientRequest,
        StreamAdvisorChain streamAdvisorChain);
}

链尾常见实现是 ChatModelStreamAdvisor:直接 chatModel.stream(prompt),再 mapChatClientResponse,并 publishOn(Schedulers.boundedElastic())

// .../ChatModelStreamAdvisor.java L48–58(d71200317)
@Override
public Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest,
        StreamAdvisorChain streamAdvisorChain) {
    Assert.notNull(chatClientRequest, "the chatClientRequest cannot be null");

    return this.chatModel.stream(chatClientRequest.prompt())
        .map(chatResponse -> ChatClientResponse.builder()
            .chatResponse(chatResponse)
            .context(Map.copyOf(chatClientRequest.context()))
            .build())
        .publishOn(Schedulers.boundedElastic());
}

DefaultChatClient 的 stream 规格里,终止于这条链:

// .../DefaultChatClient.java L724–729(d71200317)
Flux<ChatClientResponse> chatClientResponse = this.advisorChain.nextStream(chatClientRequest)
    .doOnError(observation::error)
    .doFinally(s -> observation.stop())
    .contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));
flowchart LR
  subgraph callPath [Call_Path]
    CC[ChatClient.call]
    NC[nextCall]
    CA[CallAdvisors]
    CMA[ChatModelCallAdvisor]
    CC --> NC --> CA --> CMA
  end

  subgraph streamPath [Stream_Path]
    CS[ChatClient.stream]
    NS[nextStream]
    SA[StreamAdvisors]
    CMS[ChatModelStreamAdvisor]
    CS --> NS --> SA --> CMS
  end

DeepSeek 流式 HTTP 在 DeepSeekApi.chatCompletionStreamWebClient.post()bodyToFlux(String.class) 吃 SSE,过滤 [DONE],再 mapChatCompletionChunk;tool-calling 片段用 windowUntil + reduce 合并17

// models/spring-ai-deepseek/.../DeepSeekApi.java L168–212(d71200317)
return this.webClient.post()
    .uri(this.getEndpoint(chatRequest))
    .headers(headers -> headers.addAll(HttpHeaders.readOnlyHttpHeaders(additionalHttpHeader)))
    .body(Mono.just(chatRequest), ChatCompletionRequest.class)
    .retrieve()
    .bodyToFlux(String.class)
    .takeUntil(SSE_DONE_PREDICATE)
    .filter(SSE_DONE_PREDICATE.negate())
    .mapNotNull(content -> jsonHelper.fromJson(content, ChatCompletionChunk.class))
    .map(chunk -> {
        if (this.chunkMerger.isStreamingToolFunctionCall(chunk)) {
            isInsideTool.set(true);
        }
        return chunk;
    })
    .windowUntil(chunk -> {
        if (isInsideTool.get() && this.chunkMerger.isStreamingToolFunctionCallFinish(chunk)) {
            isInsideTool.set(false);
            return true;
        }
        return !isInsideTool.get();
    })
    .concatMapIterable(window -> {
        Mono<ChatCompletionChunk> monoChunk = window.reduce(this.chunkMerger::merge);
        return List.of(monoChunk);
    })
    .flatMap(mono -> mono);

可以看到:每个元素多半是 模型回复的 token 片段,不是把用户请求拆块发送;windowUntil 只在 tool-call 流式场景把碎片合成完整 chunk。

订票 Demo 对照:Demo 走 .call();流式场景见 springai_demo ADVISOR_API.md18

sequenceDiagram
    participant Client as ChatClient
    participant Chain as StreamAdvisorChain
    participant Model as DeepSeekChatModel
    participant API as DeepSeekApi
    participant WC as WebClient
    participant Netty as ReactorNetty
    participant Kern as epoll_wait

    Client->>Chain: stream(prompt)
    Chain->>Model: adviseStream
    Model->>API: chatCompletionStream
    API->>WC: POST SSE
    WC->>Netty: async read
    Netty->>Kern: epoll_wait
    Kern-->>Netty: readable
    Netty-->>API: Flux String chunk
    API-->>Model: Flux ChatCompletionChunk
    Model-->>Chain: Flux ChatResponse
    Chain-->>Client: Flux ChatClientResponse

7. 逐块消费与聚合

打字机 UI 可以直接 subscribe 每个 ChatResponse chunk;Tool Calling 往往要等完整 AssistantMessageMessageAggregator 在并行路径上把流式消息拼回去19

// spring-ai-model/.../MessageAggregator.java(d71200317)
/**
 * Helper that for streaming chat responses, aggregate the chat response messages
 * into a single AssistantMessage.
 */
public Flux<ChatResponse> aggregate(Flux<ChatResponse> fluxChatResponse,
        Consumer<ChatResponse> onAggregationComplete) {
    AtomicReference<StringBuilder> messageTextContentRef = new AtomicReference<>(new StringBuilder());
    // ... thoughts / toolCalls / usage 等 AtomicReference ...
    return fluxChatResponse.doOnSubscribe(subscription -> {
        // 重置缓冲
    }) /* doOnNext 追加文本,doOnComplete 回调聚合结果 */;
}

DefaultAroundAdvisorChain.nextStream 还会经 ChatClientMessageAggregator 做链级聚合14。业务可选:


小结

后续若继续写,可以单独拆 Reactor Netty EpollEventLoop 与 Tokio IoDriver 的线程模型对照。

References

  1. Tokio runtime 模块文档 — I/O driver、scheduler、timer:https://github.com/tokio-rs/tokio/blob/e77885a494d91baaaeeb9590436e555dea8dd1cb/tokio/src/runtime/mod.rs  2 3

  2. Linux fs/eventpoll.cdo_epoll_waitep_poll_callback6d35786de281):https://github.com/torvalds/linux/blob/6d35786de281/fs/eventpoll.c 

  3. 本站 Tokio I/O 驱动与 epoll:https://weinan.tech/2026/05/15/tokio-io-driver-mio-scheduledio.html  2

  4. 本站 hrtimer:https://weinan.tech/2026/05/16/hrtimer-nanosecond-precision-analysis.html 

  5. 本站 futex:https://weinan.tech/2026/03/02/userspace-locks-and-kernel-futex.html 

  6. Tokio optional io-uring feature:https://github.com/tokio-rs/tokio/blob/e77885a494d91baaaeeb9590436e555dea8dd1cb/tokio/Cargo.toml 

  7. Project Reactor Flux.subscribe / LambdaSubscriber / Operators95957bd8d):https://github.com/reactor/reactor-core/blob/95957bd8deb36234d8f76bd382207693c75218e7/reactor-core/src/main/java/reactor/core/publisher/Flux.java  2

  8. Tokio PollEvented / IoDriver::add_sourcehttps://github.com/tokio-rs/tokio/blob/e77885a494d91baaaeeb9590436e555dea8dd1cb/tokio/src/io/poll_evented.rs 

  9. 本站 Rust async/await 与 Tokio:https://weinan.tech/2026/04/30/rust-async-await-future-poll-tokio-runtime.html 

  10. The Reactive Manifesto:https://www.reactivemanifesto.org/ 

  11. Reactive Streams:https://www.reactive-streams.org/ 

  12. Project Reactor README:https://github.com/reactor/reactor-core/blob/95957bd8deb36234d8f76bd382207693c75218e7/README.md 

  13. Tokio sync::mpsc 背压说明:https://github.com/tokio-rs/tokio/blob/e77885a494d91baaaeeb9590436e555dea8dd1cb/tokio/src/sync/mpsc/mod.rs 

  14. Spring AI DefaultAroundAdvisorChaind71200317):https://github.com/spring-projects/spring-ai/blob/d71200317d4a6e2e30882ec2d448b7b8966e14f6/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/DefaultAroundAdvisorChain.java  2

  15. Spring AI DefaultChatClient stream 路径:https://github.com/spring-projects/spring-ai/blob/d71200317d4a6e2e30882ec2d448b7b8966e14f6/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/DefaultChatClient.java 

  16. Spring AI StreamAdvisorhttps://github.com/spring-projects/spring-ai/blob/d71200317d4a6e2e30882ec2d448b7b8966e14f6/spring-ai-client-chat/src/main/java/org/springframework/ai/chat/client/advisor/api/StreamAdvisor.java 

  17. Spring AI DeepSeekApi.chatCompletionStreamhttps://github.com/spring-projects/spring-ai/blob/d71200317d4a6e2e30882ec2d448b7b8966e14f6/models/spring-ai-deepseek/src/main/java/org/springframework/ai/deepseek/api/DeepSeekApi.java 

  18. springai_demo ADVISOR_API.mdhttps://github.com/liweinan/springai_demo/blob/main/docs/ADVISOR_API.md 

  19. Spring AI MessageAggregatorhttps://github.com/spring-projects/spring-ai/blob/d71200317d4a6e2e30882ec2d448b7b8966e14f6/spring-ai-model/src/main/java/org/springframework/ai/chat/model/MessageAggregator.java