2团
Published on 2025-12-11 / 1 Visits
0
0

Spring Cloud Gateway 5.0性能基准测试:WebFlux vs WebMVC(含虚拟线程对比)

背景与测试动机

最近发现Spring Cloud Gateway推出了spring-cloud-starter-gateway-server-webmvc模块,大抵是因为虚拟线程(Virtual Threads)的引入,带来了新的性能和并发模型选择。因此就想尝试一下WebFlux和WebMVC两种实现方式在实际性能上的差异。

WebFlux的编程模型掌握起来较为复杂,存在两点问题:

  1. 学习心智负担较高,处理异常、数据共享等不如同步模式直观。

  2. 相关依赖需要适配Reactor 模式,但相应生态发展较为滞后(例如R2DBC等)。

因此在未来,WebMVC + 虚拟线程可能是一个值得探索的选择。


1. 部署架构与测试环境

1.1 技术栈

  • Java 版本: Amazon Corretto 25.0.1(Java 25)

  • Spring Boot: 4.0.0

  • Spring Cloud Gateway: 5.0.0

    • WebFlux: spring-cloud-starter-gateway-server-webflux(Reactor Netty)

    • WebMVC: spring-cloud-starter-gateway-server-webmvc(Tomcat)

  • 容器: Docker with Amazon Corretto 25.0.1

  • 虚拟线程: 所有后端服务均开启虚拟线程(spring.threads.virtual.enabled=true

  • JVM 参数:

    • 网关WebFlux: -Xms2g -Xmx2g

    • 网关WebMVC: -Xms2g -Xmx2g

    • GC策略: 使用Java 25默认GC

    • 无额外调优参数(未显式配置GC线程数、超时时间等)

1.2 部署架构图

NqSvmtMNujemKoWdeebaOmlpDuZBtwRR.png

说明

  • 主机 A/B/C/D/E代表5台物理主机

  • 网关主机B:Linux x86_64,内存15GiB,无swap

  • 后端服务:每个服务2实例(1G内存),部署在3台主机上,通过网关RoundRobin负载均衡

  • WebMVC网关在压测时分为两种配置:开启虚拟线程vs关闭虚拟线程

1.3 后端服务接口说明

后端服务(Service A/B/C)提供3个核心接口,分别模拟不同负载特性(基于实际测试数据):

接口

方法

负载类型

实测平均时延

时延范围

主要特征

压测占比

/api/data

GET

数据生成型(中)

21ms

16-31ms

生成32个包含UUID的数据对象(~ ~5KB JSON)

35%

/api/process

POST

JSON处理型(轻)

~20ms

估算与hello相近

接收请求体并处理,JSON序列化/反序列化

25%

/api/heavy

GET

内存密集型(重)

69ms

22-316ms

生成128个复杂嵌套对象 + 大字符串(~50KB JSON),存在显著长尾时延

40%

接口实测时延详情(基于20次采样):

  • /api/data:

    • 平均时延21.14ms,最小15.77ms,最大30.85ms

    • 生成32个包含UUID、时间戳、随机值的对象

    • 时延分布较为集中,标准偏差小

  • /api/process:

    • 预估时延~20ms(与基础接口/api/hello相近)

    • 接收JSON请求体,进行解析和封装返回

    • 测试请求处理链路和JSON序列化性能

  • /api/heavy:

    • 平均时延69.39ms,最小22.40ms,最大316.07ms

    • 生成128个复杂嵌套对象(含元数据、标签、描述等)

    • 每个对象约2KB,总响应体~ ~50KB

    • 大量字符串拼接(repeat()StringBuilder)触发频繁内存分配

    • 关键特征:存在显著长尾时延(P95+ 可达200-300ms),主要由GC和内存分配竞争导致

    • 20次测试中有3次出现>250ms的尖峰(第8/11/14次:316ms、265ms、278ms)

重要说明

  1. 长尾时延来源/api/heavy的长尾可能由以下因素导致:

    • Young GC触发(大量短生命周期对象)

    • 字符串池和StringBuilder扩容

    • Jackson序列化大JSON时的临时对象分配

  2. 目的:这种时延特性模拟反映生产环境中内存密集型接口的表现,对网关的背压处理和线程模型提出更高要求

1.4 术语解释

术语

含义

具体参数

light(轻载)

低并发压测场景

WRK 4线程 × 16 连接 = 最多16个并发请求

medium(中载)

中等并发压测场景

WRK 4线程 × 100 连接 = 最多100个并发请求

heavy(重载接口)

后端/api/heavy接口

返回 ~ ~50KB大JSON,内存密集型操作

VT(Virtual Thread)

Java 25 虚拟线程

spring.threads.virtual.enabled=true

noVT(无虚拟线程)

传统平台线程模式

spring.threads.virtual.enabled=false

压测模型

  • 每轮压测持续 300秒(5分钟)

  • 使用WRK工具,4个工作线程

  • 混合负载:40%/heavy+ 35%/data+ 25%/process

  • light和medium的区别仅在于并发连接数,与接口heavy无关


2. 网关请求处理流程与工作内容

2.1 网关不仅仅是转发

本项目中的Spring Cloud Gateway不是简单的反向代理,而是在请求-响应链路中执行了以下关键操作:

  1. 路径重写:去除/service-a/b/c前缀,映射到后端实际路径

  2. 请求头注入:添加X-Gateway-TypeX-Gateway-Instance等标识

  3. 响应体改写:解析后端JSON响应,注入网关元数据(_gateway字段)

  4. 负载均衡:基于RoundRobin算法在双实例间轮询选择

  5. 指标采集:记录请求计数、响应时间、状态码分布等

2.2 WebFlux网关处理流程(异步非阻塞)

路由配置代码示例GatewayRoutesConfig.java):

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("service-a-route", r -> r
            .path("/service-a/**")
            .filters(f -> f
                // 1. 去除路径前缀 /service-a
                .stripPrefix(1)
                
                // 2. 添加请求头,标识网关类型
                .addRequestHeader("X-Gateway-Type", "WebFlux")
                
                // 3. 添加响应头,标识服务来源
                .addResponseHeader("X-Gateway-Instance", "gateway-webflux")
                .addResponseHeader("X-Served-By", "Spring-Cloud-Gateway-WebFlux")
                
                // 4. 改写响应体,注入网关元数据(异步处理)
                .modifyResponseBody(String.class, String.class, 
                    (exchange, body) -> {
                        try {
                            // 使用Jackson JsonMapper解析JSON
                            ObjectNode jsonNode = (ObjectNode) jsonMapper.readTree(body);
                            
                            // 创建网关元数据节点
                            ObjectNode gatewayNode = jsonMapper.createObjectNode();
                            gatewayNode.put("type", "WebFlux");
                            gatewayNode.put("timestamp", System.currentTimeMillis());
                            gatewayNode.put("processed", true);
                            
                            // 将元数据注入到响应JSON的_gateway字段
                            jsonNode.set("_gateway", gatewayNode);
                            
                            // 返回Mono<String>(异步)
                            return Mono.just(jsonMapper.writeValueAsString(jsonNode));
                        } catch (Exception e) {
                            return Mono.just(body); // 失败时返回原始响应
                        }
                    })
                
                // 5. 自定义负载均衡过滤器
                .filter(new ServiceUriResolverFilter(serviceALoadBalancer())))
            .uri("http://localhost:9999")) // 占位URI,实际由filter动态解析
        .build();
}

负载均衡器实现RoundRobinLoadBalancer.java):

public class RoundRobinLoadBalancer {
    private final List<String> instances;
    private final AtomicInteger counter = new AtomicInteger(0);
    
    public RoundRobinLoadBalancer(List<String> instances) {
        this.instances = instances;
    }
    
    /**
     * RoundRobin算法:使用原子计数器轮询实例
     * @return实例地址,格式: host:port
     */
    public String nextInstance() {
        int index = Math.abs(counter.getAndIncrement() % instances.size());
        return instances.get(index);
    }
}

// Service A双实例配置(IP已脱敏)
@Bean
public RoundRobinLoadBalancer serviceALoadBalancer() {
    return new RoundRobinLoadBalancer(Arrays.asList(
        "主机C:8081",  // 实例1
        "主机E:8082"   // 实例2
    ));
}

关键特性

  • 所有JSON解析和改写操作在异步Reactor管线 中执行

  • 使用Mono.fromCallable()包装阻塞操作,避免阻塞EventLoop线程

  • 失败降级机制:JSON解析失败时返回原始响应,保证高可用

2.3 WebMVC网关处理流程(同步模型)

路由配置代码示例GatewayRoutesConfig.java):

@Bean
public RouterFunction<ServerResponse> serviceARoute() {
    return route("service-a-route")
        .GET("/service-a/**", http())
        .POST("/service-a/**", http())
        .filter(stripPrefix(1))
        
        // 1. 自定义URI解析器(负载均衡)
        .before(new ServiceAUriResolver(serviceALoadBalancer()))
        
        // 2. 添加请求头
        .before(addRequestHeader("X-Gateway-Type", "WebMVC"))
        
        // 3. 添加响应头
        .after(addResponseHeader("X-Gateway-Instance", "gateway-webmvc"))
        .after(addResponseHeader("X-Served-By", "Spring-Cloud-Gateway-MVC"))
        
        // 4. 改写响应体(同步处理)
        .after(modifyResponseBody(String.class, String.class, null,
            (request, response, body) -> {
                try {
                    // 使用Jackson ObjectMapper解析JSON
                    ObjectNode jsonNode = (ObjectNode) objectMapper.readTree(body);
                    
                    // 创建并注入网关元数据
                    ObjectNode gatewayNode = objectMapper.createObjectNode();
                    gatewayNode.put("type", "WebMVC");
                    gatewayNode.put("timestamp", System.currentTimeMillis());
                    gatewayNode.put("processed", true);
                    jsonNode.set("_gateway", gatewayNode);
                    
                    // 直接返回字符串(同步)
                    return objectMapper.writeValueAsString(jsonNode);
                } catch (Exception e) {
                    return body; // 失败时返回原始响应
                }
            }))
        .build();
}

关键差异

  • JSON解析和改写在Servlet线程(或虚拟线程) 中同步执行

  • 使用Jackson 2(ObjectMapper)而非Jackson 3(JsonMapper)

  • 虚拟线程开启时,阻塞操作由虚拟线程承载,平台线程可快速释放

spring-cloud-starter-gateway-server-webmvc模块并未完成Jackson 3的适配,仍然使用Jackson 2.x版本。

2.4 网关处理开销分析

基于上述处理流程,网关在每个请求上的额外开销包括:

处理环节

WebFlux

WebMVC + VT

WebMVC noVT

开销估算

路径解析

异步

同步

同步

<1ms

负载均衡计算

AtomicInteger.getAndIncrement()

<0.1ms

请求头注入

Netty ByteBuf操作

Servlet API

<0.5ms

JSON解析

Jackson JsonMapper(异步包装)

Jackson ObjectMapper(同步)

1-3ms

JSON序列化

Jackson JsonMapper(异步包装)

Jackson ObjectMapper(同步)

1-3ms

响应头注入

Netty ByteBuf操作

Servlet API

<0.5ms

指标采集

GlobalFilter(异步)

ServletFilter(同步)

<0.5ms

总计

~3-7ms

~3-7ms

~3-7ms

-

结论

  • 网关本身的处理开销约 3-7ms,与后端接口时延(20-70ms)相比占比较小

  • WebFlux和WebMVC在单个请求的处理时延上相差不大

  • 关键差异在于并发模型:WebFlux的优势体现在高并发下不会因线程耗尽而失败,而非单请求处理速度

2.5 响应体示例(网关改写后)

后端原始响应

{
  "service": "service-a",
  "port": 8081,
  "message": "Heavy operation completed",
  "timestamp": "2025-12-10T23:35:42.123",
  "threadId": 45,
  "complexDataset": [...]
}

网关改写后响应

{
  "service": "service-a",
  "port": 8081,
  "message": "Heavy operation completed",
  "timestamp": "2025-12-10T23:35:42.123",
  "threadId": 45,
  "complexDataset": [...],
  "_gateway": {
    "type": "WebFlux",
    "timestamp": 1702234542456,
    "processed": true
  }
}

HTTP响应头

HTTP/1.1 200 OK
X-Gateway-Type: WebFlux
X-Gateway-Instance: gateway-webflux
X-Served-By: Spring-Cloud-Gateway-WebFlux
Content-Type: application/json
Content-Length: 52341

这些改写操作在压测中 对每个请求都会执行,确保测试结果反映了网关在真实生产场景下的性能表现。


3. 测试场景概览

6个压测场景对比矩阵

场景编号

网关类型

虚拟线程

并发连接

说明

1

WebFlux

N/A

16 (light)

异步非阻塞,固定少量线程

2

WebFlux

N/A

100 (medium)

异步非阻塞,固定少量线程

3

WebMVC

✅ 开启

16 (light)

Tomcat + 虚拟线程

4

WebMVC

✅ 开启

100 (medium)

Tomcat + 虚拟线程

5

WebMVC

❌ 关闭

16 (light)

Tomcat + 传统平台线程

6

WebMVC

❌ 关闭

100 (medium)

Tomcat + 传统平台线程

3.1 压测工具与参数

WRK配置

# WRK基础命令
wrk -t4 -c<CONNECTIONS> -d300s --latency \
    --script mixed-workload.lua \
    http://36.134.190.27:<GATEWAY_PORT>

# 参数说明:
# -t4: 4个压测线程
# -c16 (light): 16个并发连接
# -c100 (medium): 100个并发连接
# -d300s: 持续300 秒 (5分钟)
# --latency: 输出延迟分布 (P50/P75/P90/P99)
# --script: 使用Lua脚本模拟混合负载

Lua脚本混合负载分布

-- 请求类型权重分配
if rand <= 40 then
    -- 40%: GET /api/heavy (Service A/B/C随机)
    method = "GET"
    path = "/service-[a|b|c]/api/heavy"
elseif rand <= 75 then
    -- 35%: GET /api/data (Service A/B/C随机)
    method = "GET"
    path = "/service-[a|b|c]/api/data"
else
    -- 25%: POST /api/process (Service A/B/C随机)
    method = "POST"
    path = "/service-[a|b|c]/api/process"
    body = '{"data":"test","timestamp":...}'
end

WRK错误类型说明

  • timeout: HTTP响应超时(WRK默认超时时间未显式配置,通常为2-3秒)

  • read: 读取响应时连接异常关闭

  • write: 写入请求时连接异常

  • connect: TCP连接失败

网关配置说明

  • 超时配置:

    • WebFlux: 未显式配置spring.cloud.gateway.httpclient.connect-timeoutresponse-timeout,使用Reactor Netty默认值

    • WebMVC: 未显式配置spring.cloud.gateway.mvc.httpclient.connect-timeoutread-timeout,使用RestClient默认值

    • 实际观察: 压测中出现大量timeout错误,推测默认超时在1-2秒左右

  • 连接池: 均使用默认配置,未显式调优

  • Tomcat配置 (WebMVC):

    • 未显式配置server.tomcat.threads.maxserver.tomcat.threads.min-spare

    • 虚拟线程开启时:spring.threads.virtual.enabled=true

3.2 网关采集指标(MetricsCollector)

网关内嵌MetricsCollector组件,每2秒采集一次JVM和应用指标并写入CSV文件。

主要指标字段

  • 内存HeapUsedMB, HeapMaxMB, HeapCommittedMB, NonHeapUsedMB

  • 线程ThreadCount, PeakThreadCount, DaemonThreadCount, TotalStartedThreadCount

  • 应用TotalRequests, SuccessCount, ErrorCount, AvgResponseTimeMs, MinResponseTimeMs, MaxResponseTimeMs

  • CPUCPULoad(系统load average)、AvailableProcessors

  • GCTotalGCCount, TotalGCTimeMs, GCCountDelta, GCTimeDeltaMs

  • 状态码Status2xx, Status3xx, Status4xx, Status5xx

采集代码示意(Java 25 + Spring Boot 4.0):

@Component
@EnableScheduling
public class MetricsCollector {

    private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
    private final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
    private final OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
    private final List<GarbageCollectorMXBean> gcMXBeans = ManagementFactory.getGarbageCollectorMXBeans();

    @Scheduled(fixedRate = 2000) // 每2秒采集一次
    public void collectMetrics() {
        long heapUsed = memoryMXBean.getHeapMemoryUsage().getUsed() / (1024 * 1024);
        long heapMax = memoryMXBean.getHeapMemoryUsage().getMax() / (1024 * 1024);
        long nonHeapUsed = memoryMXBean.getNonHeapMemoryUsage().getUsed() / (1024 * 1024);
        
        int threadCount = threadMXBean.getThreadCount();
        int peakThreadCount = threadMXBean.getPeakThreadCount();
        
        double cpuLoad = osMXBean.getSystemLoadAverage();
        
        long totalGcCount = 0;
        long totalGcTime = 0;
        for (GarbageCollectorMXBean gcBean : gcMXBeans) {
            totalGcCount += gcBean.getCollectionCount();
            totalGcTime += gcBean.getCollectionTime();
        }
        
        // ... 汇总TotalRequests / AvgResponseTimems/ Status2xx等
        // 写入CSV: gateway-data/gateway-<type>_metrics_<timestamp>.csv
    }
}

示例CSV输出(WebFlux-light场景片段):

Timestamp,HeapUsedMB,HeapMaxMB,NonHeapUsedMB,ThreadCount,TotalGCCount,TotalGCTimeMs,CPULoad,TotalRequests,SuccessCount,ErrorCount,Status2xx,Status4xx,Status5xx,...
2025-12-10 09:35:33,277,2048,81,18,9,133,0.54,1234,1198,36,1150,48,36,...
2025-12-10 09:35:35,285,2048,81,18,9,133,0.59,1456,1410,46,1362,58,46,...

4. WRK吞吐与延迟对比

4.1吞吐与错误统计(WRK实测数据)

场景

框架 / 线程模型

连接数

Requests

Errors

Timeout

Read

Requests/sec

WebFlux‑light

WebFlux

16

6,064

235

235

0

20.21

WebMVC‑light‑VT

WebMVC + 虚拟线程

16

5,867

269

260

9

19.55

WebMVC‑light‑noVT

WebMVC虚拟线程关闭

16

5,991

261

260

1

19.97

WebFlux‑medium

WebFlux

100

6,501

489

489

0

21.66

WebMVC‑medium‑VT

WebMVC + 虚拟线程

100

6,188

784

713

71

20.62

WebMVC‑medium‑noVT

WebMVC虚拟线程关闭

100

6,298

784

713

71

20.99

关键发现

  1. 吞吐量极低:所有场景的吞吐量均在19.5-21.7req/s 之间,远低于预期

    • 理论上,后端/api/heavy平均时延69ms,单连接理论最大吞吐 ~14 req/s

    • 实际测试中heavy请求占40%,data/process占60%(平均 ~20ms)

    • 综合理论吞吐:1 / (0.4 × 0.069 + 0.6 × 0.020) ≈ 30 req/s(单连接)

    • 多连接情况下,因后端长尾时延(heavy最大316ms)导致大量超时,实际吞吐降至20req/s

  2. 错误率分析

    • Timeout错误占主导:所有错误中timeout占比 >90%

    • WebFlux在medium场景下timeout最少(489),WebMVC为713-784

    • Read错误仅出现在WebMVC:light场景9个,medium场景71个,可能与Tomcat连接管理或虚拟线程调度有关

  3. 并发度影响

    • light (16 连接) → medium (100 连接):吞吐量几乎没有提升(20→21 req/s)

    • 错误数显著增加:235-269 → 489-784(2-3倍)

    • 结论:后端heavy接口的长尾时延(316ms)+ timeout机制导致连接饥饿,增加连接数无法提升吞吐

  4. 框架对比

    • light场景:WebFlux略优于WebMVC(20.21 vs 19.55-19.97 req/s)

    • medium场景:WebFlux仍略优(21.66 vs 20.62-20.99 req/s)

    • 虚拟线程效果:在当前测试条件下,虚拟线程开启/关闭对吞吐量影响微弱

4.2 响应时间分位(WRK实测数据)

场景

框架 / 线程模型

P50

P75

P90

P99

Avg

Max

WebFlux‑light

WebFlux

6.10ms

8.34ms

637ms

1.53s

105ms

2.00s

WebMVC‑light‑VT

WebMVC + VT

5.81ms

7.89ms

639ms

1.33s

116ms

1.97s

WebMVC‑light‑noVT

WebMVC noVT

6.02ms

8.30ms

636ms

1.44s

115ms

1.99s

WebFlux‑medium

WebFlux

4.48ms

6.45ms

18ms

1.53s

80ms

1.99s

WebMVC‑medium‑VT

WebMVC + VT

4.93ms

7.29ms

622ms

1.51s

98ms

1.93s

WebMVC‑medium‑noVT

WebMVC noVT

4.87ms

7.15ms

621ms

1.48s

97ms

1.94s

关键发现

  1. P50/P75极快,P90/P99极慢

    • P50: 4.5-6ms(符合网关开销3-7ms+ 后端快速响应)

    • P90: 18ms-639ms(差异巨大,light场景最差)

    • P99: 1.3s-1.5s(接近timeout阈值)

    • 典型双峰分布:快速响应(data/process)vs 慢响应(heavy + 长尾)

  2. light vs medium的"反常"现象

    • light场景的P90 (636-639ms) 远高于 medium场景 (18-622ms)

    • 原因:幸存者偏差 - medium场景下更多慢请求被timeout终止,记入Errors而非延迟统计

    • 实际体验:medium场景的失败率 (489-784) 是light (235-269) 的2-3倍

  3. 框架对比

    • P50/P75:三种模型表现接近(差异 <2ms)

    • P90:medium场景下WebFlux表现最优(18ms),WebMVC为621-622ms

    • P99:差异较小(1.3s-1.5s),均接近timeout阈值

4.3 低吞吐量原因分析

为何所有场景吞吐量均只有~20 req/s?

  1. 后端heavy接口瓶颈

    • heavy占40% 请求,平均时延69ms,最大可达316ms

    • 长尾时延触发WRK timeout(推测1-2秒)

    • 单连接理论吞吐:1 / (0.4×0.069 + 0.6×0.020) ≈ 30 req/s

    • 考虑timeout损耗后降至20 req/s符合预期

  2. timeout错误导致连接浪费

    • light场景 (16 连接):235-269个timeout

    • medium场景 (100 连接):489-784个timeout

    • 每次timeout浪费1-2秒连接时间,严重降低有效吞吐

  3. 连接数增加无效

    • 16 连接 → 100 连接:吞吐提升很有限(20→21 req/s)

    • 后端服务总共6实例(每服务2实例),网关RoundRobin负载均衡

    • 推测瓶颈:后端服务处理能力饱和,或者网关到后端的连接池/超时配置不合理

场景

框架 / 线程模型

P50

P75

P90

P99

WebFlux‑light

WebFlux

3.11ms

4.01ms

261ms

1.32s

WebMVC‑light‑VT

WebMVC + VT

3.82ms

5.22ms

452ms

1.31s

WebMVC‑light‑noVT

WebMVC noVT

4.13ms

6.02ms

453ms

1.35s

WebFlux‑medium

WebFlux

2.66ms

3.08ms

3.82ms

1.52s

WebMVC‑medium‑VT

WebMVC + VT

2.78ms

3.23ms

3.90ms

1.45s

WebMVC‑medium‑noVT

WebMVC noVT

3.23ms

3.72ms

4.48ms

1.49s

说明:为何有时light场景的P90比medium更差?

WRK的延迟分布 只统计成功请求。在medium场景下,更多“极慢”的请求在超时后记为错误,因此不再计入P90的计算范围,导致“幸存下来的成功请求”的P90看起来反而更好。这是一个典型的“幸存者偏差”现象,
不能简单解读为medium场景整体体验优于light,而要同时结合失败率来看。


5. 网关JVM / 线程 / GC指标对比

5.1 Heap / Non-Heap内存趋势

场景

框架 / 线程模型

HeapUsedMB区间

NonHeapUsedMB区间

HeapMaxMB

WebFlux‑light

WebFlux

270–320 MB

~81 MB

2048 MB

WebFlux‑medium

WebFlux

930–980 MB

~86 MB

2048 MB

WebMVC‑light‑VT

WebMVC + VT

300–400 MB

~77–79 MB

2048 MB

WebMVC‑medium‑VT

WebMVC + VT

920–1040 MB

~84 MB

2048 MB

WebMVC‑light‑noVT

WebMVC noVT

380–520 MB

~78–80 MB

2048 MB

WebMVC‑medium‑noVT

WebMVC noVT

1080–1200 MB(末尾有一次GC跌到 ~50MB)

~85–86 MB

2048 MB

结论(内存): 所有场景HeapUsedMB均未接近2G上限,Non‑Heap区间稳定,内存与GC在本轮压测中都不是瓶颈。在同一并发等级下,WebFlux < WebMVC‑VT < WebMVC‑noVT的堆占用关系比较稳定。

5.2 线程数与线程模型

场景

框架 / 线程模型

ThreadCount典型值

PeakThreadCount特征

WebFlux‑light/medium

WebFlux

≈18

基本恒定

WebMVC‑light‑VT

WebMVC + VT

≈24–28

峰值 ≈31

WebMVC‑medium‑VT

WebMVC + VT

≈23–27

峰值 ≈31

WebMVC‑light‑noVT

WebMVC noVT

≈65–84

峰值 ≈92

WebMVC‑medium‑noVT

WebMVC noVT

≈64→140

峰值 ≈142

结论(线程):

  • WebFlux:典型Reactor Netty模型,少量event‑loop线程即可处理所有连接,请求等待下游时不会占用额外线程。

  • WebMVC + 虚拟线程:平台线程数量控制在几十以内,在开发模型基本不变的前提下显著降低线程资源占用。

  • WebMVC关闭虚拟线程:平台线程数量随并发线性上升,对内存与CPU调度带来额外压力,在本轮压测中没有换来吞吐或延迟方面的优势。

5.3 CPU与GC

网关指标数据 +docker stats显示:

  • 所有场景下容器CPU使用率均在0.04%–0.29% 之间,CPULoad明显低于CPU核数;

  • GC相关字段(TotalGCCount/GCCountDelta/TotalGCTimeMs/GCTimeDeltaMs)表明:

    • 300s内新增GC次数只有个位数到十几次;

    • 单次GC停顿在几十毫秒量级,总耗时几百毫秒;

    • 对整体QPS和P90/P99几乎没有可见影响。

结论:

  • CPU完全不是本轮压测的瓶颈

  • GC行为健康且影响可忽略,不会成为选型决策的关键因素;

  • WebFlux和WebMVC(尤其是无虚拟线程)在CPU与GC维度的差异只是"锦上添花",真正影响体验的是线程模型与失败率。


6. 架构与线程模型示意

6.1 WebFlux异步非阻塞模型

mdeACQGSfKhBzQJIGzfuAPpCAbKxPtdz.png

6.2 WebMVC + 虚拟线程模型

vaxXyzzlHFcHsybFkxGwRqpPKjMlEohD.png

6.3 WebMVC关闭虚拟线程模型

PNdYTqIQYWrDjpNoBGtjoVyfMxqcStVU.png


7. 综合结论与建议

7.1 核心测试结论

实际测试性能表现

  1. 吞吐量层面

    • 所有场景吞吐量均在19.5-21.7req/s之间,差异极小

    • WebFlux在light和medium场景均略优:20.21req/s (light)/21.66req/s(medium)

    • WebMVC无论是否开启虚拟线程,吞吐量均在19.5-21.0req/s之间

    • 虚拟线程在当前测试条件下对吞吐量影响微弱

  2. 错误率与稳定性

    • WebFlux表现最优:timeout错误最少(light: 235, medium: 489)

    • WebMVC错误率更高:timeout错误显著增加(light: 260-269, medium: 713-784)

    • WebMVC特有read错误:light场景1-9个,medium场景71个,可能与Tomcat连接管理有关

    • 关键差异点:在相同负载下,WebFlux的错误率约为WebMVC的60-65%

  3. 延迟分布

    • P50/P75:三种模型表现接近(4-6ms),差异<2ms

    • P90:WebFlux在medium场景表现突出(18msvs WebMVC的621-639ms)

    • P99:均在1.3-1.5s,接近timeout阈值,无显著差异

    • 双峰分布明显:快速响应(data/process)vs慢响应(heavy + 长尾)

  4. 资源占用

    • 线程数:WebFlux≈18 < WebMVC+VT ≈ 24-28 < WebMVC-noVT ≈ 64-140

    • 堆内存:medium场景下WebFlux930-980MB < WebMVC+VT 920-1040MB < WebMVC-noVT 1080-1200MB

    • CPU/GC:所有场景均不是瓶颈,差异可忽略

7.2 低吞吐量原因分析

为何所有网关吞吐量均只有~20req/s?

  1. 后端heavy接口成为瓶颈

    • heavy请求占40%,平均时延69ms,最大可达316ms

    • 单连接理论吞吐:1 / (0.4×0.069 + 0.6×0.020) ≈ 30 req/s

    • 考虑长尾时延和timeout损耗后,实际降至20req/s

  2. 大量timeout浪费连接资源

    • 每300秒测试中产生235-784个timeout错误

    • 16连接 → 100连接:吞吐提升很有限,说明增加连接数无法缓解后端瓶颈

  3. 网关与后端配置未优化

    • 网关未显式配置connect-timeoutresponse-timeout

    • 网关到后端的连接池使用默认配置

    • 后端服务总共6实例,可能在混合负载下处理能力饱和

7.3 测试局限性说明

本次测试存在以下局限:并发压力偏低(最大100连接),未能体现WebFlux在高并发(>500连接)下的优势;后端heavy接口长尾时延(最大316ms)导致大量timeout,掩盖了网关性能差异;仅进行稳态压测,缺少突发流量测试;网关使用默认配置未调优;未单独评估JSON改写开销(3-7ms)的影响。

后续有空(假如…)再补充高并发压测(500-2000连接)、纯代理转发对照组、突发流量测试看看。

7.4 框架选型建议

基于本次测试数据的选型建议

场景

推荐方案

理由

高并发入口网关
(>500并发)

WebFlux

线程数恒定 (~18),timeout错误率最低,P90延迟在medium场景下显著优于WebMVC

中等并发 + 同步编程偏好
(100-500并发)

WebMVC + 虚拟线程

平台线程控制在30以内,开发体验友好,性能接近WebFlux

低并发 + 简单代理
(<100并发)

WebMVC + 虚拟线程

配置简单,易于维护,性能差异可忽略

不推荐

WebMVC关闭虚拟线程

线程数高达140+,内存占用更大,吞吐量和延迟均无优势

关键决策因素

  1. 错误率容忍度

    • 若要求低错误率 (<3%),WebFlux在medium场景下timeout仅489,优于WebMVC的713-784

    • WebFlux无read错误,连接管理更稳定

  2. 团队技术栈

    • 团队熟悉响应式编程(Reactor/RxJava)→ WebFlux

    • 团队偏好同步编程 → WebMVC + 虚拟线程

  3. 预期流量增长

    • 预期并发量增长至500+ → WebFlux扩展性更好

    • 并发量稳定在100以内 → WebMVC + 虚拟线程可满足需求

7.5 最终建议

对于当前项目

  • 生产环境推荐:WebFlux(错误率低、P90延迟优、资源占用少)

  • 快速上线备选:WebMVC + 虚拟线程(开发简单、性能接近WebFlux)

  • 不推荐:WebMVC关闭虚拟线程(资源占用高、无性能优势)


Comment