bigywater.tech
Spring · 58

WebFlux 线程模型:event loop 上做阻塞调用的后果

WebFlux 默认有几个线程?在控制器里 Thread.sleep 或查 JDBC 会怎样?block() 为什么会报错?subscribeOn(boundedElastic) 能解决什么?为什么 WebFlux 用很少的线程就能扛很多并发?这篇在 Spring Boot 4.0.8 的 WebFlux 上把 event loop 线程数调到 2,用真实请求逐个验证。

10
WebFlux 默认处理请求的 event loop 线程数(等于本机逻辑核心数)
1022 ms
2 个 event loop 线程都被 Thread.sleep 占住时,一个本来只要几毫秒的 /fast 的等待时间(offload 之后是 9 ms)
557 ms
只有 2 个线程时,200 个并发的非阻塞等待请求(各等 500 ms)全部完成的时间

线程数

实测 · 本机执行输出
  100 个请求(每个新连接)分布在 10 个线程上:{'reactor-http-nio-1': 10, 'reactor-http-nio-10': 10, 'reactor-http-nio-2': 10, 'reactor-http-nio-3': 10, 'reactor-http-nio-4': 10, 'reactor-http-nio-5': 10, 'reactor-http-nio-6': 10, 'reactor-http-nio-7': 10, 'reactor-http-nio-8': 10, 'reactor-http-nio-9': 10}

event loop 线程数等于核心数,连接轮流分配。

调小线程数

实测 · 本机执行输出
  reactor.netty.ioWorkerCount=2:40 个请求分布在 2 个线程上:{'reactor-http-nio-1': 20, 'reactor-http-nio-2': 20}

设成 2 让阻塞的后果可见。

阻塞的后果

实测 · 本机执行输出
  -- 在 Mono.fromCallable 里 Thread.sleep(500)(没有切线程)
  4 个并发请求的耗时(ms):[611, 611, 1120, 1125];响应里的线程:['blocked-500@reactor-http-nio-1', 'blocked-500@reactor-http-nio-2']
  期间每 40 ms 发一个 /fast(新连接)共 12 个:最慢 1022 ms,超过 100 ms 的有 1 个;耗时序列 [1022, 2, 2, 2, 5, 8, 4, 10, 6, 6, 7, 6]
  -- 控制器方法直接返回 String,里面 Thread.sleep(500)
  4 个并发请求的耗时(ms):[585, 585, 1096, 1099];响应里的线程:['direct-500@reactor-http-nio-1', 'direct-500@reactor-http-nio-2']
  期间每 40 ms 发一个 /fast(新连接)共 13 个:最慢 993 ms,超过 100 ms 的有 1 个;耗时序列 [993, 7, 8, 5, 2, 1, 2, 2, 2, 2, 2, 2, 7]
  -- Thread.sleep(500) 放到 subscribeOn(boundedElastic)
  4 个并发请求的耗时(ms):[595, 595, 595, 595];响应里的线程:['offload-500@boundedElastic-1', 'offload-500@boundedElastic-2', 'offload-500@boundedElastic-3', 'offload-500@boundedElastic-4']
  期间每 40 ms 发一个 /fast(新连接)共 32 个:最慢 9 ms,超过 100 ms 的有 0 个;耗时序列 [8, 2, 2, 2, 6, 9, 6, 7, 5, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 2, 3, 6, 6]
  -- Mono.delayElement(500 ms)(非阻塞等待)
  4 个并发请求的耗时(ms):[579, 579, 579, 579];响应里的线程:['delayed']
  期间每 40 ms 发一个 /fast(新连接)共 32 个:最慢 8 ms,超过 100 ms 的有 0 个;耗时序列 [8, 2, 2, 2, 2, 8, 7, 4, 2, 2, 2, 8, 7, 6, 7, 7, 5, 5, 8, 8, 3, 1, 1, 2, 2]

event loop 上阻塞会拖慢所有请求;offload 到 boundedElastic 则不影响。

block()

实测 · 本机执行输出
  GET /call-block → 500 [IllegalStateException] block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-2
  GET /call-block-offload → 200 x

Reactor 在非阻塞线程上直接拒绝 block()。

非阻塞等待

实测 · 本机执行输出
  50 个并发的 /delay?ms=500:全部完成用了 622 ms;结果统计 {200: 50};成功的里最慢 617 ms
  100 个并发的 /delay?ms=500:全部完成用了 552 ms;结果统计 {200: 100};成功的里最慢 526 ms
  200 个并发的 /delay?ms=500:全部完成用了 557 ms;结果统计 {200: 200};成功的里最慢 533 ms

等待不占线程,少量线程撑住大量并发。

分步演示

线程模型
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 Spring Boot 4.0.8 的 SpringApplication(spring-boot-starter-webflux,Reactor Netty 1.3.7,Netty 4.2.17);每个请求新建连接(Connection: close);event loop 线程数通过 -Dreactor.netty.ioWorkerCount 设置;机器 10 个逻辑核心。之前试跑过一次 200 个并发请求的实验,第一次冲击刚启动的进程时有个别请求失败,正式记录的这次全部成功,结果在刚启动的进程上并不稳定
    • 没有验证:BlockHound 对阻塞调用的检测;WebFlux 在 Tomcat / Undertow 等 Servlet 容器上的线程模型;spring.threads.virtual.enabled 对 WebFlux 的影响(见第 60 篇);WebClient 在 event loop 上的连接池行为(见第 59 篇);响应式 JDBC(R2DBC)的线程(见第 61 篇);ioWorkerCount 取值对吞吐的影响
    京ICP备2026067502号-1京公网安备11010502063487号