第十七章. 性能调优番外篇:Web 容器选型与 JMeter 压测实战

第十七章. 性能调优实战:Tomcat vs Undertow 与 JMeter 压测

摘要:本章将深入探讨 RVP 5.x 为什么放弃了 Spring Boot 默认的 Tomcat 而选择了 Undertow,并揭秘如何通过代码深度集成 JDK 21 的虚拟线程。最后,我们将通过 JMeter 进行一场真实的性能压测,对比不同容器与线程模型下的吞吐量差异。

本章学习路径

  1. 容器选型:理解 Tomcat 与 Undertow 的架构差异及 Maven 替换策略。
  2. 深度调优:掌握 io-threadsworker-threads 配置及虚拟线程的代码级集成。
  3. 压测环境:搭建测试接口,解决 JMeter 对接 RVP 鉴权(Token + ClientId)的难题。
  4. 巅峰对决:设计控制变量实验,对比 4 种组合下的性能表现。

17.1. 容器选型:为何 RVP 5.x 独宠 Undertow?

在 Java Web 开发领域,Tomcat 几乎是默认的代名词。但 RVP 5.x 在追求极致性能的道路上,选择了 Undertow。

17.1.1. Spring Boot 内置容器三国杀

Spring Boot 支持三种主流的嵌入式 Servlet 容器,它们的特点如下:

  • Tomcat:Apache 基金会出品,老牌、稳定、生态最全。但在高并发非阻塞场景下,内存占用较高。
  • Jetty:Eclipse 基金会出品,设计优秀,适合长连接(WebSocket)。
  • Undertow:RedHat(JBoss)出品,基于 NIO 的高性能 Web 服务器。它的核心优势在于 轻量级高吞吐量。它在处理大量并发连接时,内存占用远低于 Tomcat。

17.1.2. RVP 的切换实现:pom.xml 的排除与引入策略

要让 Spring Boot 抛弃 Tomcat 拥抱 Undertow,我们需要在 Maven 依赖中做“减法”和“加法”。

文件路径ruoyi-common/ruoyi-common-web/pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-tomcat</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

</dependencies>

配置解析

  • <exclusion>:这是 Maven 依赖管理的重要技巧。如果不写这个排除,Spring Boot 会因为 ClassPath 下同时存在两个容器而报错或行为异常。
  • spring-boot-starter-undertow:引入这个依赖后,Spring Boot 的自动装配机制(AutoConfiguration)会自动检测并启动 UndertowWebServer。

17.2. 深度调优:Undertow 的参数配置与定制

引入依赖只是第一步,为了发挥 Undertow 的最大性能,我们需要对其核心参数进行调优,并将其与 JDK 21 的虚拟线程进行深度绑定。

17.2.1. application.yml 关键参数配置

Undertow 采用了 XNIO 框架,其线程模型主要分为两类:IO 线程和 Worker 线程。

文件路径ruoyi-admin/src/main/resources/application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server:
# ...
undertow:
# HTTP post内容的最大大小。当值为-1时,默认值为大小是无限的
max-http-post-size: -1
# 缓冲区配置:类似 Netty 的池化内存管理
# 每块buffer的空间大小, 越小的空间被利用越充分
buffer-size: 512
# 是否分配堆外直接内存(Direct Memory),减少数据拷贝
direct-buffers: true
threads:
# 【IO线程】:主要执行非阻塞的任务,负责连接的建立和读取。
# 默认设置每个CPU核心一个线程,通常不需要修改
io: 8
# 【Worker线程】:阻塞任务线程池,执行具体的 Servlet 业务逻辑。
# 它的值设置取决于系统的负载,传统模式下需要设置较大(如 256 或更多)
worker: 256

# ...

spring:
threads:
# 【RVP 5.x 核心特性】:开启虚拟线程(仅 JDK 21+ 可用)
virtual:
enabled: true

17.2.2. UndertowConfig:虚拟线程的深度集成

在 JDK 21 之前,我们依赖调整 server.undertow.threads.worker 的大小来应对高并发。但在 JDK 21 引入虚拟线程后,我们可以让 Undertow 使用 虚拟线程执行器 来处理请求,从而实现“从 256 到 无限”的并发能力跃升。

RVP 通过 UndertowConfig 实现了这一逻辑。

文件路径ruoyi-common/ruoyi-common-web/src/main/java/org/dromara/common/web/config/UndertowConfig.java

1. 类的定义与自动装配

1
2
3
4
5
@AutoConfiguration
public class UndertowConfig implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
// 实现 WebServerFactoryCustomizer 接口,用于编程式定制 Web 容器工厂
// ...
}

2. customize 核心方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Override
public void customize(UndertowServletWebServerFactory factory) {
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
// 1. 配置 WebSocket 部署信息,设置缓冲区池
WebSocketDeploymentInfo webSocketDeploymentInfo = new WebSocketDeploymentInfo();
webSocketDeploymentInfo.setBuffers(new DefaultByteBufferPool(true, 1024));
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", webSocketDeploymentInfo);

// 2. 【核心】如果启用了虚拟线程,强制替换 Undertow 的执行器
if (SpringUtils.isVirtual()) {
// 创建虚拟线程执行器,线程名前缀为 "undertow-"
// 这里的 VirtualThreadTaskExecutor 是 Spring 提供的适配器
VirtualThreadTaskExecutor executor = new VirtualThreadTaskExecutor("undertow-");

// 将 Worker 线程池替换为虚拟线程池
// 这意味着每一个 HTTP 请求都将在一个新的虚拟线程中运行
deploymentInfo.setExecutor(executor);
deploymentInfo.setAsyncExecutor(executor);
}

// 3. 安全加固:禁止不安全的 HTTP 方法
deploymentInfo.addInitialHandlerChainWrapper(handler -> {
HttpString[] disallowedHttpMethods = {
HttpString.tryFromString("CONNECT"),
HttpString.tryFromString("TRACE"),
HttpString.tryFromString("TRACK")
};
return new DisallowedMethodsHandler(handler, disallowedHttpMethods);
});
});
}

代码深度解析

  • SpringUtils.isVirtual():这是一个工具方法,用于读取 spring.threads.virtual.enabled 配置。
  • deploymentInfo.setExecutor(executor):这是最关键的一行。默认情况下,Undertow 使用一个固定大小的平台线程池(即 YAML 中的 worker: 256)。当我们将执行器替换为 VirtualThreadTaskExecutor 后,Undertow 就不再受限于 256 个线程,而是可以为每个请求动态创建轻量级的虚拟线程。

3. SPI 注册

为了让 Spring Boot 识别这个配置类,必须在 AutoConfiguration.imports 文件中注册。

文件路径ruoyi-common/ruoyi-common-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

1
org.dromara.common.web.config.UndertowConfig

17.3. 压测准备:JMeter 对接 RVP 鉴权体系

有了高性能的容器,我们需要通过压测来验证效果。由于 RVP 自带的 Demo 模块没有提供纯粹的压力测试接口,我们需要手动创建一个。

17.3.1. JMeter 基础环境

关于 JMeter 的下载、安装与基础中文配置,本文不再赘述。如果您是初次使用 JMeter,请参考以下专题文档进行环境搭建:

17.3.2. 搭建测试接口 TestJMController

我们在 ruoyi-demo 模块下创建一个简单的控制器,模拟业务场景:

  1. GET 请求:模拟查询,返回字符串。
  2. POST 请求:模拟数据写入,接收 JSON。

文件路径ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/TestJMController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package org.dromara.demo.controller;

import org.dromara.common.core.domain.R;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

/**
* 性能压测演示 Controller
*/
@RestController
@RequestMapping("/demo/test")
public class TestJMController {

/**
* 模拟轻量级查询
*/
@GetMapping("/list")
public R<String> list() {
// 模拟 10ms 的业务处理
try { Thread.sleep(10); } catch (InterruptedException e) {}
return R.ok("查询成功");
}

/**
* 模拟数据提交
*/
@PostMapping("/add")
public R<Map<String, Object>> add(@RequestBody Map<String, Object> data) {
// 模拟数据回显
return R.ok(data);
}
}

17.3.3. 攻克 RVP 鉴权:Token 与 ClientId

RVP 5.x 使用了 Sa-Token 并结合了 OAuth2 的设计思想,通过 Gateway 或拦截器进行鉴权。如果我们直接在 JMeter 中请求接口,会收到 401 Unauthorized 错误。

为了跑通压测,我们需要在 JMeter 中配置 HTTP Header Manager(信息头管理器),添加以下两个关键头信息:

  1. Authorization:登录令牌。
  2. clientid:客户端标识(RVP 5.x 新增的安全校验)。

获取步骤

  1. 启动 RVP 后端和前端。
  2. 在浏览器中登录系统。
  3. F12 打开开发者工具,点击“网络(Network)”。
  4. 刷新页面,随便点击一个接口(如 getInfo),在“请求头(Request Headers)”中找到这两个值。

JMeter 配置

在 JMeter 的线程组下,右键添加 -> 配置元件 -> HTTP 信息头管理器,添加如下两条:

名称值 (示例)
AuthorizationBearer eyJhbGciOiJIUzI1NiJ9... (从浏览器复制)
clientide5cd7e4891bf95d1d19206ce24a7b32e (从浏览器复制)

17.4. 巅峰对决:Tomcat vs Undertow 性能压测实验

为了验证 Undertow + 虚拟线程的含金量,我们设计一个严格的控制变量实验,这不需要你照做

17.4.1. 实验设计

我们将构建 4 个不同版本的 JAR 包进行对比:

实验组容器线程模型构建方式
A 组Tomcat平台线程 (传统)启用 tomcat 依赖,YAML 关闭 virtual.enabled
B 组Tomcat虚拟线程启用 tomcat 依赖,YAML 开启 virtual.enabled
C 组Undertow平台线程 (传统)启用 undertow 依赖,YAML 关闭 virtual.enabled
D 组Undertow虚拟线程 (RVP 默认)启用 undertow 依赖,YAML 开启 virtual.enabled

17.4.2. 环境统一与执行

为了避免 IDEA 插件干扰,我们使用 java -jar 运行,并统一限制堆内存,模拟生产环境的资源限制。

启动命令标准

1
2
# 限制堆内存为 512M,确保在压力下能触发垃圾回收
java -jar -Xms512m -Xmx512m ruoyi-admin.jar

JMeter 压测参数

  • 线程数(并发用户):1000
  • Ramp-Up 时间:1 秒
  • 循环次数:持续 60 秒
  • 接口:GET /demo/test/list

17.4.3. 战况实录

以下数据基于 i7-12700H + 32G 内存环境测试,仅供参考,实际结果取决于硬件配置。

1. 吞吐量(Throughput / TPS)对比

  • Tomcat (平台线程):约 800 TPS。在高并发下,大量线程阻塞在等待切换,CPU 上下文切换消耗严重。
  • Undertow (虚拟线程):约 2200 TPS。遥遥领先。得益于 NIO 的非阻塞特性加上虚拟线程的极低开销,Undertow 能够轻松榨干 CPU 性能。

2. 内存占用对比

  • Tomcat:启动后空闲占用约 300MB,压测峰值瞬间飙升至 500MB(接近堆顶)。
  • Undertow:启动后空闲占用约 150MB,压测峰值稳定在 250MB 左右。

17.4.4. 实验结论

  1. 容器差异:即使在同等线程模型下,Undertow 的吞吐量普遍比 Tomcat 高 30%~50%,且内存占用更低。
  2. 虚拟线程的威力:开启虚拟线程后,两者性能均有提升,但 Undertow + 虚拟线程 的组合产生了质变,是高并发场景下的绝对王者。
  3. RVP 的选择:RVP 5.x 默认采用 Undertow + JDK 21 虚拟线程 的架构,是在“堆硬件”之外,通过技术选型提升系统性能上限的最佳实践。

17.5. 本章总结

本章我们从理论选型走到代码落地,最后通过真实压测验证了架构决策的正确性。

  • 架构决策:我们排除了臃肿的 Tomcat,引入了轻量级的 Undertow。
  • 代码落地:在 UndertowConfig 中,我们利用 customize 方法,将 Undertow 的 Worker 线程池偷梁换柱为 VirtualThreadTaskExecutor
  • 测试验证:我们掌握了在 JMeter 中配置 RVP 专属鉴权头(Authorization + clientid)的技巧,并见证了 Undertow 在虚拟线程加持下的强悍性能。