WEBKT

别忙着重构,用数据说话:Spring Boot 3 虚拟线程与 WebFlux 吞吐量实测对比

3 0 0 0

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 回收上。

为什么 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,而是为了解救那些饱受传统阻塞线程池枯竭折磨、却又无力重构为响应式架构的普通业务系统

在生产实践中,不要盲信“新技术终结旧技术”的论调。了解两者的技术天花板与底层代价,根据团队技术栈、业务高并发的真实级别进行理性的架构权衡,才是架构师的核心价值所在。

技术探路者 虚拟线程WebFlux

评论点评