# WebFlux 与虚拟线程:每秒 2000 请求的性能对比
在高负载下,Spring WebFlux 的响应式模型与 Project Loom 的新型虚拟线程范式之间的选择,将决定 Java 应用的性能和可扩展性。本研究在示例微服务项目上,对两种方法进行了每秒 2000 请求的负载测试,结果如下。
Spring WebFlux 架构:事件循环与非阻塞 I/O
Spring WebFlux 采用带有事件循环的非阻塞 I/O 模型。与传统的“一个请求—一个线程”模型不同,WebFlux 使用少量线程即可处理数千个连接。其核心原则是:线程在等待 I/O 时不会阻塞,而是注册对事件的兴趣(例如,“套接字上数据到达”),然后切换到其他任务。
WebFlux 默认使用的核心组件是 Netty。Netty 通过 EventLoopGroup 管理事件循环。事件循环线程数由以下公式决定:
int DEFAULT_IO_WORKER_COUNT = Integer.parseInt(System.getProperty(
ReactorNetty.IO_WORKER_COUNT,
"" + Math.max(Runtime.getRuntime().availableProcessors(), 4)));
换言之,线程数等于可用处理器数,但不少于 4。在容器化环境中,可以调整此值以匹配分配的资源。
每个网络连接(Channel)都绑定到特定事件循环(通道亲和性)。这确保了无同步开销和 CPU 缓存效率。然而,事件循环中的任何阻塞操作都会使绑定到该循环的所有连接处理停滞。
虚拟线程:Project Loom 的工作原理
虚拟线程保留了“一个请求—一个线程”模型,但大幅降低了开销成本。每个请求都有自己的虚拟线程,该线程挂载在载体线程(平台线程)上。载体线程由基于 ForkJoinPool 的调度器管理。
默认载体线程数等于可用处理器数。例如,在具有 2 个 CPU 的容器中,将有 2 个载体线程,能够运行数千个虚拟线程。
默认调度器创建方法:
private static ForkJoinPool createDefaultScheduler() {
ForkJoinWorkerThreadFactory factory = pool -> new CarrierThread(pool);
int parallelism, maxPoolSize, minRunnable;
String parallelismValue = System.getProperty("jdk.virtualThreadScheduler.parallelism");
String maxPoolSiz
虚拟线程的优势:
- 保留命令式编程风格
- 可读的堆栈跟踪
- 支持 ThreadLocal
- 简化与现有阻塞库的集成
每秒 2000 请求的负载测试结果
在示例微服务项目上的测试显示了以下结果:
- WebFlux 只要事件循环中没有阻塞操作,就能保持稳定性能。然而,集成阻塞库(例如 JDBC)需要将这些操作卸载到单独的线程池(Schedulers.boundedElastic()),这增加了复杂性。
- 虚拟线程简化了开发和调试,同时提供了高性能。在每秒 2000 请求下,Spring MVC 和 Tomcat 上的虚拟线程吞吐量与 WebFlux 相当,且代码对大多数 Java 开发者来说都很熟悉。
WebFlux 的关键因素是正确使用响应式链并避免在事件循环中进行阻塞调用。对于虚拟线程,需要注意它们无法解决 CPU 密集型操作——高 CPU 负载可能导致载体线程争用。
关键差异与建议
在 WebFlux 和虚拟线程之间选择时,需要考虑以下方面:
- 工作负载类型:
- 两种方法都适用于 I/O 密集型应用(大量 I/O 等待)
- 对于 CPU 密集型任务,由于载体线程争用,虚拟线程可能落后
- 库生态:
- 如果项目依赖阻塞库(JDBC),虚拟线程便于迁移
- 对于支持响应式驱动程序(R2DBC)的新项目,WebFlux 仍然可行
- 团队专业知识:
- 响应式编程需要深入的异步模式知识
- 虚拟线程支持熟悉的命令式风格
关键要点
- 每秒 2000 请求的性能:两者都能处理负载,但虚拟线程通过坚持命令式风格简化了开发
- 关键限制:WebFlux 事件循环中的一个阻塞操作会使该循环上的所有请求瘫痪;虚拟线程无法解决 CPU 密集型问题
- 生态:采用 WebFlux 意味着用响应式库替换阻塞库,这可能代价高昂
- 调试:由于堆栈跟踪混乱,响应式应用的诊断更困难,而虚拟线程提供熟悉的性能分析工具
— Editorial Team
暂无评论。