Spring Boot 性能优化:Undertow 嵌入式服务器配置实战指南
1. 为什么选择 Undertow?聊聊我的实战感受
大家好,我是老张,一个在Java后端领域摸爬滚打了十多年的老码农。这些年,从早期的SSH到现在的Spring Boot全家桶,各种技术栈都折腾过。今天想和大家深入聊聊一个在Spring Boot性能优化中经常被提及,但可能很多朋友还没真正用起来的“利器”——Undertow嵌入式服务器。
我记得几年前,我们团队接手了一个高并发的API网关项目。初期为了图省事,直接用了Spring Boot默认的Tomcat。项目上线后,平时流量不大时一切安好,可一到促销活动,流量瞬间暴涨,服务就开始出现响应延迟、内存飙升,甚至偶发OOM(内存溢出)的情况。当时我们紧急扩容了机器,但成本压力巨大。后来经过一番性能压测和排查,发现Tomcat在高并发短连接场景下的线程模型和内存管理,确实成了瓶颈。也就是在那时,我们把目光投向了Undertow。
简单来说,Undertow是Red Hat(红帽)公司出品的一款高性能、轻量级的Web服务器。它天生就是为嵌入式和现代云原生应用设计的。和Tomcat、Jetty这些“老大哥”相比,Undertow就像一个身手敏捷的“特种兵”。它最大的特点就是采用了非阻塞(NIO)的I/O模型,并且设计上追求极致的资源利用效率。我实测下来,在相同的硬件配置下,处理大量并发请求时,Undertow的内存占用能比Tomcat低30%左右,而吞吐量(QPS)能有明显的提升,尤其是在处理大量短连接请求(比如微服务间的HTTP调用、API请求)时,优势非常明显。
所以,如果你的Spring Boot应用面临以下场景,我强烈建议你考虑切换到Undertow:
- 高并发、低延迟:应用需要处理大量的瞬时并发请求,对响应时间要求苛刻。
- 资源敏感型部署:比如在容器化(Docker/K8s)环境中,希望单个Pod占用更少的内存,从而在有限的资源下部署更多的实例。
- 需要HTTP/2支持:Undertow开箱即用地支持HTTP/2,而Tomcat需要额外的配置。
- 追求极致的性能调优:你希望对Web服务器的线程、缓冲池等底层参数有更精细的控制。
当然,Undertow也不是银弹。它的社区生态和资料丰富度确实不如Tomcat,一些非常冷门的问题可能需要你深入源码去排查。但就我个人的经验来看,对于绝大多数追求性能的Java Web应用,切换到Undertow带来的收益是远大于这点学习成本的。下面,我就手把手带你完成从引入到深度配置的实战。
2. 第一步:在Spring Boot中引入Undertow
Spring Boot的“约定大于配置”理念在这里同样适用。默认情况下,spring-boot-starter-web 依赖会自动引入Tomcat。所以,我们要做的就是把Tomcat排除掉,然后引入Undertow的starter。这个过程非常简单。
2.1 Maven项目配置
在你的 pom.xml 文件中,找到 spring-boot-starter-web 依赖,添加一个 <exclusions> 标签将其内部的Tomcat排除,然后单独引入 spring-boot-starter-undertow。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- 排除默认的Tomcat -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入Undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
改完依赖后,直接启动你的应用。如果控制台看到类似下面的日志,恭喜你,切换成功!
2024-01-24 22:14:03.204 INFO 41412 --- [ main] io.undertow : starting server: Undertow - 2.0.33.Final
2024-01-24 22:14:04.029 INFO 41412 --- [ main] o.s.b.w.e.u.UndertowServletWebServer : Undertow started on port(s) 8080 (http) with context path ''
看到 Undertow started on port(s) 8080 这一行,就说明Undertow已经成功替代Tomcat,在8080端口监听了。整个过程无需修改任何业务代码,这就是Spring Boot的魅力。但是,这只是“能用”。要想“好用”,发挥出Undertow的性能优势,我们必须进行针对性的配置。这里我踩过一个坑:直接切换后不做任何配置,在某些极端压力测试下,性能提升并不明显,甚至因为缓冲区设置不合理,出现了频繁的GC。所以,接下来的配置环节才是重头戏。
2.2 解决一个常见的启动警告
切换后启动,你很可能会在日志里看到一个WARN信息:
WARN 9608 --- [main] io.undertow.websockets.jsr : UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
这个警告的意思是WebSocket的缓冲区池没有显式设置,使用了默认值。如果你根本用不到WebSocket功能,最简单的办法是直接把相关依赖排除掉,一劳永逸。在引入 spring-boot-starter-undertow 时再加一层排除:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
<exclusions>
<!-- 排除WebSocket JSR依赖,如果你不需要WebSocket功能 -->
<exclusion>
<groupId>io.undertow</groupId>
<artifactId>undertow-websockets-jsr</artifactId>
</exclusion>
</exclusions>
</dependency>
如果你需要用到WebSocket,那么我建议你通过编程式配置来显式设置这个缓冲区池,这样既能消除警告,也能让你更精确地控制资源。具体做法我们会在后面的编程式配置部分详细讲。先把这个警告记在心里,我们继续看更核心的性能参数配置。
3. 核心性能调优:配置文件参数详解
Undertow的性能调优,大部分工作都可以通过 application.yml 或 application.properties 文件来完成。Spring Boot为我们封装了非常丰富的配置项,全都以 server.undertow 开头。我结合自己的实战经验,把这些参数分为几个关键类别,并告诉你每个参数调优背后的“为什么”。
3.1 线程池配置:控制并发处理的核心
Undertow的线程模型是其高性能的基石之一。它主要包含两种线程:I/O线程(IO Threads)和工作线程(Worker Threads)。
- I/O线程:负责非阻塞地处理所有入站和出站的连接。它们数量很少,通常设置为CPU核心数的一到两倍。这些线程非常“忙碌”,只做最轻量的I/O事件分发。
- 工作线程:负责执行实际的业务逻辑,比如运行你的Servlet、Filter和Controller中的代码。它们是阻塞的,所以需要更大的池子。
在配置文件中,我们可以这样设置:
server:
undertow:
threads:
io: 4 # I/O线程数,通常设置为CPU核心数。我的机器是8核,这里设4或8都可以。
worker: 256 # 工作线程数。这是处理业务请求的主力军。
经验之谈:io线程数不建议设置得过高,因为它不处理业务,够用就行。worker线程数则需要根据你的应用类型来定。对于计算密集型(CPU Bound)应用,设置太大反而会因为线程切换导致性能下降,可以参考 CPU核心数 * (1 + 等待时间/计算时间) 这个公式来估算。对于I/O密集型(IO Bound)应用,比如大量调用数据库或外部API,可以适当调高。我一般会从200开始,通过压测工具(如JMeter)观察系统负载和响应时间,逐步调整到一个最优值。默认值是 io 线程数的8倍。
3.2 缓冲区配置:影响内存和吞吐量的关键
缓冲区(Buffer)是Undertow处理请求和响应的临时内存区域。配置得当,能极大提升吞吐量;配置不当,则会引起频繁GC。
server:
undertow:
buffer-size: 1024 # 每个缓冲区的大小,单位字节。默认根据JVM内存自动判定。
direct-buffers: true # 是否使用堆外内存(Direct Buffer)。强烈建议设为true。
buffer-size:这个值设置多大?这取决于你典型的请求和响应体大小。如果你的API主要是传输小JSON(几KB),那么设置1024或2048就足够了。如果涉及文件上传下载,可能需要设置为16384(16KB)甚至更大。设置太小,会导致一个请求被分割到多个Buffer,增加处理开销;设置太大,会浪费内存。我通常先设为1024,观察压测时的性能。direct-buffers:这是Undertow的一个性能利器。设为true意味着使用堆外内存(Direct Buffer)。堆外内存的分配和回收虽然比堆内内存稍慢,但它有一个巨大优势:在进行网络I/O操作(如Socket读写)时,数据可以直接在堆外内存和网卡之间传输,省去了从JVM堆内内存复制到系统内核缓冲区的开销。对于高吞吐量的网络应用,开启这个选项能显著降低CPU使用率。这也是Undertow内存占用看起来比Tomcat低的原因之一——Tomcat大量使用堆内内存。
3.3 连接与请求限制:安全与稳定的护栏
这些配置是防止恶意攻击和应用过载的重要防线。
server:
undertow:
max-http-post-size: 10MB # HTTP POST请求体的最大大小。防止超大请求体攻击。
max-parameters: 1000 # 允许的查询参数或路径参数的最大数量。
max-headers: 200 # 允许的请求头最大数量。
max-cookies: 200 # 允许的Cookie最大数量。
no-request-timeout: 60000 # 连接空闲超时时间(毫秒)。客户端建立连接后,如果在60秒内没发送任何请求,服务器会关闭连接。
这些限制主要是为了防御基于哈夫曼编码的DoS攻击,攻击者会发送大量、畸形的头信息或参数来耗尽服务器资源。根据你的业务实际情况调整,比如你的API需要接收大的表单提交,就把 max-http-post-size 调大;如果你的应用通过URL传递大量参数,就需要调整 max-parameters。no-request-timeout 对于管理长连接池非常有用,避免空闲连接占用资源。
4. 高级定制:编程式配置满足复杂需求
虽然配置文件能解决大部分问题,但有些更精细的控制,或者需要根据运行时条件动态调整的配置,就必须通过编程式配置来实现了。这也是Undertow灵活性的一大体现。
4.1 实现 WebServerFactoryCustomizer 接口
我们可以创建一个配置类,实现 WebServerFactoryCustomizer<UndertowServletWebServerFactory> 接口。在这个类的 customize 方法里,我们可以为所欲为地对Undertow工厂进行定制。
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Configuration;
import io.undertow.Undertow;
@Configuration
public class MyUndertowCustomizer implements WebServerFactoryCustomizer<UndertowServletWebServerFactory> {
@Override
public void customize(UndertowServletWebServerFactory factory) {
// 1. 设置监听端口和主机(虽然通常用配置文件,这里展示编程能力)
// factory.setPort(9090);
// factory.setAddress(InetAddress.getByName("0.0.0.0"));
// 2. 更精细的Buffer Pool配置
factory.setBufferSize(1024 * 16); // 设置16KB的缓冲区
factory.setDirectBuffers(true); // 使用堆外内存
// 3. 添加一个自定义的HttpHandler(底层处理器)
factory.addBuilderCustomizers((Undertow.Builder builder) -> {
// 例如,添加一个自定义的访问日志处理器
// builder.setHandler(new AccessLogHandler(builder.getHandler()));
});
// 4. 解决之前提到的WebSocket Buffer Pool警告(如果需要WebSocket)
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
WebSocketDeploymentInfo info = new WebSocketDeploymentInfo();
// 创建一个直接的ByteBuffer池,参数:是否使用DirectBuffer, 每个Buffer大小,池的最大大小
info.setBuffers(new DefaultByteBufferPool(true, 1024, 200, 20));
deploymentInfo.addServletContextAttribute("io.undertow.websockets.jsr.WebSocketDeploymentInfo", info);
});
}
}
通过编程方式,我们可以直接操作Undertow的底层Builder,这带来了无限的可能性。比如,你可以添加自定义的Handler来处理特定路径的请求,在进入Spring MVC之前就完成一些操作(比如静态资源处理、基础认证),这比通过Filter链效率更高。
4.2 配置Undertow原生选项
Undertow和其底层XNIO框架提供了大量高级选项(Options),这些选项在Spring Boot的配置文件中可能没有直接映射,但可以通过编程方式设置。
factory.addBuilderCustomizers(builder -> {
// 设置Undertow原生选项
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true); // 确保HTTP/2开启
builder.setServerOption(UndertowOptions.MAX_ENTITY_SIZE, 10485760L); // 限制请求实体大小为10M
builder.setServerOption(UndertowOptions.IDLE_TIMEOUT, 30000); // 连接空闲超时30秒
// 设置底层XNIO的Socket选项
builder.setSocketOption(Options.BACKLOG, 1000); // 设置Accept队列长度
builder.setSocketOption(Options.READ_TIMEOUT, 30000); // 读超时
builder.setSocketOption(Options.WRITE_TIMEOUT, 30000); // 写超时
builder.setSocketOption(Options.TCP_NODELAY, true); // 启用Nagle算法,减少小包延迟,对高性能API至关重要
});
这里特别提一下 TCP_NODELAY。默认情况下,TCP协议会使用Nagle算法来合并小数据包,减少网络报文数量,但这会引入延迟。对于要求低延迟的交互式应用(如游戏、实时API),将其设为 true 可以禁用该算法,让数据包立即发送,虽然会增加一些网络开销,但能显著降低延迟。这是我做高频交易系统调优时必改的一个参数。
5. 实战压测与参数调优经验分享
配置写好了,不代表性能就最优了。所有参数的最终值,都应该基于压测结果来定。我分享一下我们当时做网关性能调优的流程和发现。
我们使用JMeter对一个简单的健康检查接口(/actuator/health)进行压测,模拟1000个并发用户持续请求5分钟。初始配置是Undertow默认参数。
第一轮压测:发现吞吐量上不去,CPU使用率也不高。查看监控,发现工作线程池经常满负荷,而I/O线程很闲。这说明瓶颈在业务处理线程上。我们逐步将 server.undertow.threads.worker 从默认的(io*8)提升到256,吞吐量有了线性增长。但增加到512后,吞吐量不再上升,反而因为线程切换开销略有下降。所以最终将worker线程数锁定在256。
第二轮压测:调整线程后,吞吐量上来了,但发现GC(垃圾回收)频率异常高,特别是“年轻代GC”。我们怀疑是Buffer设置不合理。默认的Buffer池策略可能产生了大量小对象。我们通过编程式配置,显式地创建了一个 DefaultByteBufferPool,并设置了合理的池化参数(池大小、每个Buffer的大小),同时确保 direct-buffers=true。这一轮调整后,年轻代GC频率下降了约70%,吞吐量又提升了15%。
第三轮压测:处理大量小请求时表现良好,但模拟大文件上传时,响应时间变长。我们调整了 server.undertow.buffer-size 到 16384(16KB),并适当增大了 max-http-post-size。同时,我们发现 no-request-timeout 默认值有点大,对于我们的短连接场景,将其从60秒下调到30秒,释放了部分连接资源。
经过这三轮调优,最终我们的API网关在同样的4核8G容器实例上,QPS从最初的约8000提升到了接近15000,平均响应时间从35ms降低到了12ms,而且内存使用更加平稳。这个优化过程让我深刻体会到,没有放之四海而皆准的最优配置,只有最适合你业务场景的配置。最好的方法就是:大胆假设,小心验证,用压测数据说话。
最后,再给大家提个醒,切换到Undertow后,一些依赖特定Tomcat行为特性的代码可能需要调整,比如某些与Tomcat HttpSession 或 ServletContext 相关的边缘API。但在标准的Spring MVC和Spring WebFlux使用方式下,兼容性非常好,基本可以做到无缝切换。希望我的这些实战经验能帮你绕过一些坑,更快地让Undertow为你所用。如果你在切换过程中遇到任何具体问题,也欢迎随时交流。
更多推荐



所有评论(0)