别忙着重构,用数据说话:Spring Boot 3 虚拟线程与 WebFlux 吞吐量实测对比
JDK 21 的正式发布以及 Spring Boot 3.2 对虚拟线程(Virtual Threads,Project Loom)的正式支持,在 Java 社区掀起了巨大的波澜。
一时间,“WebFlux 终结者”、“声明式异步已死”、“回归 Thread-per-request 阻塞模型”等言论充斥各大技术社区。许多同行甚至开始规划把现有的 WebFlux 项目重构成基于虚拟线程的 WebMVC 架构。
先别急着动手。技术选型从来不应该建立在情绪和 PPT 上,必须用真实的压测数据和底层运行机制来说话。本文将绕开理论空谈,通过一组接近生产环境的 I/O 密集型场景压测,实测 WebFlux 与 Spring Boot 3 虚拟线程在吞吐量、响应时间(RT)以及资源消耗上的真实差异。
压测场景设计
在高性能 Web 服务中,最核心的瓶颈往往不是 CPU 计算,而是网络 I/O 或数据库 I/O。因此,我们设计了一个典型的 I/O 密集型微服务接口:
- 接口逻辑:接收请求,调用一个模拟下游慢服务的 HTTP 接口(固定延迟 100ms),然后返回响应。
- 这种场景能极大地考验线程调度框架在应对高并发 I/O 阻塞时的资源利用效率。
压测环境
- 服务器配置:4 核 CPU,8G 内存(限制 JVM 堆内存
-Xms2g -Xmx2g,避免大内存掩盖垃圾回收开销)。 - JDK 版本:GraalVM JDK 21.0.2
- 框架版本:Spring Boot 3.2.4
- 压测工具:
wrk(在另一台同内网的机器上运行,排除压测端自身的 CPU 损耗影响)。
参测选手
选手 A:Spring Boot 3 + 虚拟线程 (Tomcat)
开启虚拟线程配置:
spring.threads.virtual.enabled=true
核心代码采用传统的同步阻塞写法:
@RestController
public class VirtualThreadController {
private final RestClient restClient = RestClient.create("http://mock-service:8080");
@GetMapping("/io-test")
public String ioTest() {
// RestClient 在虚拟线程启用时,其底层的阻塞网络 I/O 会自动挂起虚拟线程,释放 Carrier Thread
return restClient.get()
.uri("/delay/100")
.retrieve()
.body(String.class);
}
}
选手 B:Spring Boot 3 + WebFlux (Netty)
基于响应式编程范式,使用 WebClient 进行非阻塞调用:
@RestController
public class WebFluxController {
private final WebClient webClient = WebClient.create("http://mock-service:8080");
@GetMapping("/io-test")
public Mono<String> ioTest() {
return webClient.get()
.uri("/delay/100")
.retrieve()
.bodyToMono(String.class);
}
}
压测实测数据对比
我们使用 wrk 分别模拟了 500、2000、5000、10000 个并发连接,持续时间 60 秒,重点观测 QPS(每秒吞吐量)、P99 延迟 以及 系统资源占用。
1. 吞吐量(QPS)对比
| 并发连接数 | WebFlux 吞吐量 (QPS) | 虚拟线程 吞吐量 (QPS) |
|---|---|---|
| 500 | 4,890 | 4,872 |
| 2000 | 18,220 | 17,950 |
| 5000 | 35,600 | 32,100 |
| 10000 | 41,200 | 34,800 |
- 分析:
- 在 中低并发(500 - 2000) 阶段,两者的吞吐量几乎没有差距,基本逼近了下游 100ms 延迟下的物理极限。
- 当并发量飙升到 5000 及 10000 时,WebFlux 开始展现出微弱但明显的优势,比虚拟线程高出约 10% - 18% 的吞吐量。
2. 响应时间(P99 Latency)对比
| 并发连接数 | WebFlux P99 延迟 (ms) | 虚拟线程 P99 延迟 (ms) |
|---|---|---|
| 500 | 108 | 109 |
| 2000 | 118 | 124 |
| 5000 | 162 | 198 |
| 10000 | 245 | 312 |
- 分析:
在极端高并发下(10000 并发),虚拟线程的 P99 延迟漂移比 WebFlux 更严重。这意味着在高负荷下,虚拟线程的调度开销和内存抖动开始对实时性产生负面影响。
3. 内存与 CPU 表现
- 内存消耗:
- WebFlux 的内存曲线极其平滑。因为其底层的 Netty 采用固定数量的 EventLoop 线程,通过内存池(ByteBuf)复用,几乎没有因为并发连接增加而产生明显的内存暴涨。
- 虚拟线程 在 10000 并发时,JVM 堆内存占用明显偏高(多出约 400MB - 600MB)。虽然一个虚拟线程仅占数百字节到几 KB,但当并发高达数万时,海量的虚拟线程对象、调用栈帧(Stack Frames)在堆上的频繁分配与回收,给 JVM 垃圾回收器(Garbage Collector)带来了不小的压力。
- CPU 使用率:
- 两者都能充分榨干多核 CPU 的算力,但虚拟线程在极高并发下的 CPU 负载中,有较大一部分消耗在
ForkJoinPool的线程调度与 GC 回收上。
- 两者都能充分榨干多核 CPU 的算力,但虚拟线程在极高并发下的 CPU 负载中,有较大一部分消耗在
为什么 WebFlux 依然略胜一筹?
通过数据可以看到,虽然虚拟线程极大提升了传统阻塞代码的并发能力,但在极限场景下,WebFlux 依然保持着统治地位。原因在于两者的底层运行机制有着本质不同。
1. 线程调度模型的差异
- WebFlux (Reactor + Netty):
这是彻底的**非阻塞、事件驱动(Event-Driven)**架构。Netty 的 EventLoop 线程数通常与 CPU 核心数一致。当遇到 I/O 阻塞时,操作系统内核通过epoll机制通知 Netty 哪个连接的数据准备好了,EventLoop 才会去处理。整个过程没有线程切换,没有上下文保存,CPU 缓存行(Cache Line)极度友好。 - 虚拟线程 (Project Loom):
它本质上是在用户态实现的 M:N 协程调度。当你在虚拟线程中发起阻塞 I/O 时,JVM 捕获这一阻塞行为,把该虚拟线程从 Carrier Thread(承载它的平台线程)上卸载(Yield),并将当前的调用栈保存到堆内存中。等到 I/O 就绪,调度器(ForkJoinPool)再把调用栈装载回某个空闲的 Carrier Thread 继续运行。
虽然这比操作系统级别的内核线程切换轻量得多,但**“保存/恢复调用栈”以及“在堆上分配线程栈空间”依然存在物理开销**。
2. 无法避开的“Pinning(线程钉死)”问题
这是目前虚拟线程在实际落地中最容易踩雷的深坑。
在 Java 中,如果虚拟线程在 synchronized 块或 synchronized 方法内执行阻塞操作,或者调用了本地方法(Native Method),该虚拟线程就会被“钉死(Pinned)”在当前的平台线程上。此时,虚拟线程无法释放底层平台线程,导致 M:N 退化成 1:1 的传统阻塞模型。
在真实的 Spring 生产项目中,许多第三方库(例如旧版的 JDBC 驱动、部分 HttpClient、安全加密库)内部依然大量充斥着 synchronized。一旦这些代码在高并发下被触发,虚拟线程的优势将瞬间荡然无存。而 WebFlux 自诞生起,整个生态链(从 R2DBC 到 WebClient)都经过了严格的异步非阻塞重构,彻底杜绝了线程钉死。
架构选型建议:我们该怎么选?
基于上述实测与技术原理剖析,对于正在纠结如何选型的团队,建议如下:
1. 什么时候坚守或选择 WebFlux?
- 极限并发与资源敏感型系统:如网关(Spring Cloud Gateway)、高并发推送服务、中转代理等。WebFlux 在高负载下的极低内存占用和确定性延迟是虚拟线程无法替代的。
- 成熟的 Reactive 生态链:如果你的团队已经跨越了 Reactive 陡峭的学习曲线,且项目已经在使用 WebFlux + R2DBC 组合,绝对不要为了赶时髦去重构。
2. 什么时候应该果断拥抱虚拟线程?
- 传统的 WebMVC 业务系统:这是虚拟线程的绝对主场。如果项目充斥着复杂的业务逻辑、大量的同步第三方库,且团队玩不转 Mono/Flux 这种“ colored function(有色函数)”,直接在 Spring Boot 3 中开启虚拟线程是性价比最高的方案。你不需要修改一行业务代码,就能让 QPS 提升数倍。
- 开发效率优先于极致性能:虚拟线程保留了最符合人类直觉的顺序编程模型(Imperative Programming),不仅好写,而且极其便于调试(Debug)和打印堆栈日志(Stack Trace)。
总结
Spring Boot 3 虚拟线程的出现,不是为了消灭 WebFlux,而是为了解救那些饱受传统阻塞线程池枯竭折磨、却又无力重构为响应式架构的普通业务系统。
在生产实践中,不要盲信“新技术终结旧技术”的论调。了解两者的技术天花板与底层代价,根据团队技术栈、业务高并发的真实级别进行理性的架构权衡,才是架构师的核心价值所在。