bigywater.tech
Spring · 60

MVC、WebFlux 与虚拟线程:同一负载下的吞吐与内存

虚拟线程能不能让普通的 Spring MVC 追上 WebFlux?吞吐、延迟、内存各是多少?线程池 200 个线程的吞吐上限怎么算?这篇在本机用真实的 HTTP 下游,把三种写法放在同一份负载下比较。

1997 → 9037
并发 1000 时,MVC 平台线程与虚拟线程的吞吐(请求/秒),代码一行不改
439 → 688 MB
并发 1000 之后进程 RSS:平台线程 vs 虚拟线程(WebFlux 是 375 MB)
9052
WebFlux 并发 1000 的吞吐(请求/秒),与虚拟线程版同一量级

平台线程

实测 · 本机执行输出
  启动后空闲 RSS 236 MB;单个请求 → 200 ok
  并发  100:两轮吞吐     914 /     924 请求/秒;p50  111.1 /  110.9 ms;p99   121.4 /   118.1 ms;错误 0 / 0;RSS  369 MB;peakPlatformThreads=221 livePlatformThreads=221 heapUsedMB=25
  并发 1000:两轮吞吐    1997 /    1946 请求/秒;p50  549.5 /  557.6 ms;p99   586.5 /   574.1 ms;错误 0 / 0;RSS  439 MB;peakPlatformThreads=414 livePlatformThreads=414 heapUsedMB=36
  并发 3000:两轮吞吐    2270 /    2312 请求/秒;p50 1670.0 / 1659.5 ms;p99  1772.4 /  1715.7 ms;错误 0 / 0;RSS  462 MB;peakPlatformThreads=436 livePlatformThreads=436 heapUsedMB=36

吞吐被线程数封顶(200 ÷ 100 ms ≈ 2000),并发超过线程数只排队。

虚拟线程

实测 · 本机执行输出
  启动后空闲 RSS 243 MB;单个请求 → 200 ok
  并发  100:两轮吞吐     922 /     933 请求/秒;p50  110.8 /  109.6 ms;p99   116.4 /   116.9 ms;错误 0 / 0;RSS  370 MB;peakPlatformThreads=31 livePlatformThreads=31 heapUsedMB=25
  并发 1000:两轮吞吐    9037 /    9229 请求/秒;p50  112.8 /  109.4 ms;p99   126.9 /   123.3 ms;错误 0 / 0;RSS  688 MB;peakPlatformThreads=31 livePlatformThreads=31 heapUsedMB=49
  并发 3000:两轮吞吐   14970 /   12465 请求/秒;p50  169.0 /  207.9 ms;p99  1319.4 /  2642.9 ms;错误 2 / 0;RSS 1161 MB;peakPlatformThreads=31 livePlatformThreads=31 heapUsedMB=84

阻塞代码不改就拿到接近 WebFlux 的吞吐,内存更高。

WebFlux

实测 · 本机执行输出
  启动后空闲 RSS 240 MB;单个请求 → 200 ok
  并发  100:两轮吞吐     922 /     927 请求/秒;p50  111.8 /  110.7 ms;p99   121.0 /   123.2 ms;错误 0 / 0;RSS  367 MB;peakPlatformThreads=18 livePlatformThreads=18 heapUsedMB=15
  并发 1000:两轮吞吐    9052 /    9179 请求/秒;p50  111.7 /  110.7 ms;p99   125.0 /   126.0 ms;错误 0 / 0;RSS  375 MB;peakPlatformThreads=18 livePlatformThreads=18 heapUsedMB=17
  并发 3000:两轮吞吐   19622 /   12919 请求/秒;p50  118.3 /  129.7 ms;p99  1631.1 /  4541.7 ms;错误 114 / 0;RSS  522 MB;peakPlatformThreads=18 livePlatformThreads=18 heapUsedMB=24 应用内的异常={WebClientRequestException <- SocketException: Connection reset=109}

吞吐与虚拟线程同量级,内存更低,代价是响应式写法。

分步演示

同一负载的对比
本步的对象

    本文用到的工具与边界

    • 本机 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(MVC 版用 RestClient,WebFlux 版用 WebClient 并把连接池上限调到 20000),JDK 21,-Xmx1g -Xms256m;下游是 JDK HttpServer + 虚拟线程(-Dsun.net.httpserver.maxIdleConnections=1000000,否则下游自己会掐断空闲 keep-alive 连接造成假错误,第一次试跑就因此出错);压测端是 JDK HttpClient + 虚拟线程;压测端、被测应用、下游都在同一台 10 核 macOS 上,互相抢 CPU;每个并发度测两轮,并发 3000 一档波动很大,只能当作趋势参考
    • 没有验证:Linux 上的表现(内核参数、调度);CPU 密集型负载;下游更慢或更快(只测了 100 ms);更长时间的稳定性与 GC 停顿;虚拟线程的 pinning(synchronized 里的阻塞调用);Tomcat 线程数调大到 1000 的平台线程对照(只测了默认 200);RSS 增加的具体原因(只记录了数字);JFR 或火焰图层面的分析;并发 3000 一档
    京ICP备2026067502号-1京公网安备11010502063487号