一、为什么我们要关心这件事
好多同学可能觉得,Spring Boot 自带服务器,直接跑起来不就行了吗?确实,对于一个小项目或者本地演示,什么都不用改,默认的 Tomcat 足够用。但是一旦上了生产环境,情况就变了。你可能遇到并发上不去、响应变慢、连接被重置、日志太多没地方看……这些问题很多都和服务器本身的配置有关系。
我们平时用 Spring Boot 时,最直观的感受就是启动一个带 main 方法的 jar,然后内嵌的服务器就起来了。它不像以前那样把 war 包丢到外置 Tomcat 的 webapps 里,而是把服务器当成一个库,和业务代码打包在一起。这件事带来了很大的方便,但也让我们必须明白:这个“服务器”其实是可以用代码或配置去改的,而且不同服务器底子不一样,适合的场景也不一样。
这篇文章想聊的,就是怎么在 Spring Boot 里换掉默认的 Tomcat,换成 Jetty 或 Undertow,以及无论是哪一个,我们都可以通过一些参数和代码来调优。我会尽量用大白话讲,给你一些能直接抄的配置。
二、三个选手的基本印象
2.1 Tomcat:老大哥
Tomcat 是 Java 世界最经典的 Web 容器,很多老项目从 Servlet 时代就在用。Spring Boot 默认选它,图的就是稳定、成熟、资料多。你在网上搜任何一个关于线程池、连接数的问题,基本上都能找到 Tomcat 的答案。
Tomcat 的默认配置比较“保守”,它更看重稳定性,而不是极限性能。默认的最大线程数是 200,连接超时是 20 秒。如果你的项目是传统企业应用,长事务比较多,数据库查询有点慢,Tomcat 这种稳重的性格会很合适。
2.2 Jetty:轻量小钢炮
Jetty 最大的特点是可以被“裁剪”。它的架构是模块化的,需要哪些功能就加载哪些功能,所以内存占用可以控制得很低。很多嵌入式场景、物联网设备、或者对启动速度特别敏感的服务,喜欢用 Jetty。
Jetty 在处理大量短连接、长连接混合的场景时,表现得非常灵活。它的线程模型和 Tomcat 不太一样,后者更偏向于一个线程处理一个连接,而 Jetty 在 NIO 方面做得更细致。如果你需要在一个很小的内存限制下跑起来,Jetty 往往是个好选择。
2.3 Undertow:默默无闻的优等生
Undertow 是红帽公司搞出来的,被用在 WildFly 应用服务器里。它有一些非常亮眼的指标:内存占用比 Tomcat 低,吞吐量在某些场景下比 Tomcat 高,而且对高并发连接的支持很好。它的名字直译是“底层”,也暗示了它的追求:更接近底层,更高效。
Undertow 比较适合那些需要处理大量并发连接、比如 WebSocket 聊天服务、消息推送服务、以及微服务网关。它的配置方式也很有趣,很多选项可以通过环境变量或者 YAML 文件直接设置。
三、选型思路:场景决定一切
3.1 什么时候优先选 Tomcat
如果你的团队里大部分人只熟悉 Tomcat,那别犹豫,就用 Tomcat。不要为了“性能好”去换一个大家都不了解的服务器,因为稳定运行的关键不是服务器有多快,而是出了问题谁能快速定位。Tomcat 的监控指标丰富,JMX 支持完善,而且网上有海量踩坑记录,这是非常大的无形资产。
另外,如果你的应用里有大量同步阻塞式 Servlet、或者使用了很多老牌 JavaEE 库,Tomcat 的兼容性最稳妥。
3.2 什么时候优先选 Jetty
如果你部署环境是 Docker 容器,内存限制只有 256MB,启动还要快;或者你要在同一个 JVM 里启动多个 Web 服务实例,那么 Jetty 会很合适。它占的体积小,模块定制能力强。
比如你做一个小型设备管理后台,只提供几个接口,Tomcat 有点“杀鸡用牛刀”的感觉,而 Jetty 可以让你把不用的模块全部关掉,把浪费降到最低。
3.3 什么时候优先选 Undertow
如果你的业务是长连接密集型,比如在线聊天、实时协作、股票行情推送,Undertow 的表现通常会让你惊喜。它的 IO 处理机制能够承受大量的空闲连接,不会因为连接数量多而让内存暴涨。
另外,如果你的系统已经用 Spring WebFlux 这种响应式编程,那 Undertow 是一个很自然的搭配。因为它原生支持异步,和响应式模型配合得更流畅。
3.4 它们之间的直观对比
| 特性 | Tomcat | Jetty | Undertow |
|---|---|---|---|
| 默认成熟度 | 最高 | 中等 | 中等 |
| 内存占用 | 偏大 | 最小 | 较小 |
| 高并发支持 | 好 | 较好 | 很出色 |
| 配置复杂度 | 低 | 中等 | 中等 |
| 常见场景 | 企业级 | 嵌入式 | 微服务/长连接 |
这个表只是提供一个感觉,实际数据会受版本、配置、系统资源的影响。真要做选型,建议写个小压测脚本,模拟你的真实流量,再决定。
四、动手定制:以 Tomcat 为例
我们先看看怎么把 Tomcat 调一调。假设你用的是 Spring Boot 2.x,依赖里已经默认引入了 Tomcat。那么你只需要在 application.yml 里写配置,很多参数就能生效。
4.1 配置连接池和线程池
连接池的参数决定了能同时接受多少 TCP 连接,线程池的参数决定了有多少线程能同时处理请求。传统的 Tomcat 模型下,连接和线程是联系在一起的。下面这段配置表示:最多 800 个连接,最大工作线程 400,队列容量 200。
# 技术栈:Spring Boot 2.x + Spring MVC + Tomcat(默认)
server:
port: 8080 # 服务端口
tomcat:
# 最大工作线程数,不是越大越好,一般设置为 CPU 核数的 2~4 倍
max-threads: 400
# 最少空闲线程数,提前准备一些线程,避免突发流量时创建线程的耗时
min-spare-threads: 40
# 最大连接数,超过了就会排队等待
max-connections: 800
# 等待队列的长度,如果队列也满了,新的请求会被拒绝
accept-count: 200
# 连接超时时间,单位毫秒,这里设置为 20 秒
connection-timeout: 20000
注意,如果你的接口都是快速返回的,可以适当调大 max-connections,线程数可以少一点;如果你的接口要花好几秒调用远程服务,那线程数才是瓶颈,连接数反而没那么关键。
4.2 配置访问日志
默认情况下,Tomcat 不打印访问日志。但生产环境最好打开它,因为你需要统计请求量、排查问题、分析用户行为。
# 技术栈:Spring Boot 2.x + Tomcat 访问日志配置
server:
tomcat:
accesslog:
enabled: true # 开启访问日志
directory: /data/logs/tomcat # 日志存放目录
pattern: "%h %l %u %t \"%r\" %s %b %D" # 格式:IP 时间 请求 状态 字节 耗时(ms)
prefix: access_log # 日志文件前缀
suffix: .log # 日志文件后缀
rotate: true # 每天切分一个文件
这里的 pattern 格式跟你以前在外置 Tomcat 里看到的是一样的,复制过去就能用。
4.3 配置 HTTPS
在微服务架构里,网关外面一般会挂一层负载均衡来处理证书,但内网服务之间也可以自己启用 HTTPS 来加密。Spring Boot 让你可以用一个很简单的配置加上证书文件就完成。
# 技术栈:Spring Boot 2.x + Tomcat HTTPS 配置
server:
port: 8443
ssl:
enabled: true
key-store: classpath:server.p12 # 证书文件放到 resources 目录下
key-store-type: PKCS12
key-store-password: changeit # 密码,别用这个,记得换
如果证书有别名,还需要加 key-alias。这段配置在 Jetty 和 Undertow 下也是类似的,因为 Spring Boot 帮你做了抽象。
五、再讲 Jetty 和 Undertow 的调优重点
5.1 Jetty 的配置方式
要使用 Jetty,先要把 Tomcat 排除掉,然后引入 Jetty 依赖。这一步骤是必须的,不然会出现两个服务器同时启动的冲突。在你的 pom.xml 里,大概是这样:
<!-- 技术栈:Spring Boot 2.x + Maven + Jetty -->
<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>
<!-- 引入 Jetty 服务器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
改完依赖之后,启动项目就会发现端口监听来自 Jetty。接下来可以对 Jetty 的线程池做一些调整。在 Spring Boot 2.x 中,Jetty 的线程配置是通过 server.jetty 开头的属性控制的。
# 技术栈:Spring Boot 2.x + Jetty
server:
port: 8080
jetty:
threads:
# 最少线程数,和 Tomcat 的 min-spare-threads 类似
min: 20
# 最大线程数
max: 300
# 空闲线程存活时间,单位毫秒
idle-timeout: 60000
# 连接空闲超时时间,超过这个时间没动静就关闭连接
connection-idle-timeout: 30000
Jetty 还有一个很实用的地方是可以直接通过 Java 代码来添加 Connector。比如我们想额外开一个端口来跑健康检查,可用下面这个自定义配置类:
// 技术栈:Spring Boot 2.x + Java 8 + Jetty
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JettyConfig {
@Bean
public WebServerFactoryCustomizer<JettyServletWebServerFactory> jettyCustomizer() {
// 这个类型是 Spring Boot 专门提供给 Jetty 的定制入口
return factory -> {
// 给 Jetty 添加一个额外的连接器,监听 9090 端口
factory.addServerCustomizers(server -> {
ServerConnector extraConnector = new ServerConnector(server);
extraConnector.setPort(9090);
// 把这个连接器挂到 server 上
server.addConnector(extraConnector);
});
};
}
}
这段代码虽然看起来有点绕,但其实就是“找到 Jetty 的 server 对象,再往上面插一个端口”。
5.2 Undertow 的配置方式
换 Undertow 一样要先排除 Tomcat,再引入依赖。在 pom.xml 里:
<!-- 技术栈:Spring Boot 2.x + Maven + Undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
Undertow 的配置项里,最重要的两个是 io-threads 和 worker-threads。前者负责网络 IO 的读写,数量一般设置为 CPU 核数;后者负责业务处理,可以稍微大一点。下面是一个例子:
# 技术栈:Spring Boot 2.x + Undertow
server:
port: 8080
undertow:
# IO 线程数,通常设置为 CPU 核心数
io-threads: 4
# 工作线程数,类似 Tomcat 的 max-threads
worker-threads: 256
# 每个缓冲区的大小,单位字节
buffer-size: 1024
# 是否直接分配堆外内存
direct-buffers: true
# 访问日志配置
accesslog:
enabled: true
dir: /data/logs/undertow
pattern: "%h %l %u %t \"%r\" %s %b %D"
如果你用的是 Spring Boot 2.3 以上的版本,Undertow 的访问日志配置会有一点变化。但上面这种写法在大多数场景依然能用。假如你想用代码来设置更多细节,比如响应头里加个服务器标识,也可以写定制器。
// 技术栈:Spring Boot 2.x + Java 8 + Undertow
import io.undertow.Undertow;
import io.undertow.UndertowOptions;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class UndertowConfig {
@Bean
public WebServerFactoryCustomizer<UndertowServletWebServerFactory> undertowCustomizer() {
return factory -> {
// 给 Undertow 设置全局参数
factory.addBuilderCustomizers(builder -> {
// 设置 HTTP 请求头大小上限为 32KB
builder.setServerOption(UndertowOptions.MAX_HEADER_SIZE, 32 * 1024);
// 开启 HTTP/2 功能(需要额外依赖支持)
builder.setServerOption(UndertowOptions.ENABLE_HTTP2, true);
});
};
}
}
这里设置 MAX_HEADER_SIZE 对处理那些在请求头里塞了很多自定义信息的客户端来说很管用,不然默认值可能不够。
六、注意事项:容易踩的坑
6.1 不要盲目调大线程数
很多同学一遇到性能问题,就疯狂调大最大线程数,结果内存直接爆掉,或者线程上下文切换把 CPU 耗光了。正确的做法是先压测,观察线程使用率、请求响应时间、CPU 利用率,再决定调整。一般来说,线程数不是越大越好,而是刚好够用最好。
6.2 换服务器之后一定要验证功能
从 Tomcat 切到 Jetty 或 Undertow,虽然 Spring Boot 封装得很好,但某些底层行为还是不一样的。比如 ServletRequest 的某些实现方法,上传文件大小的解析,以及异步请求的细节,都可能出现微小的差异。所以换完服务器之后,至少要跑一遍完整的冒烟测试,尤其是文件上传、下载、WebSocket 这些场景。
6.3 别忘了设置超时时间
无论是 Tomcat 还是 Undertow,默认的连接超时时间都可能比你想象的长。如果上游服务经常出现连接堆积,你应该把 connection-timeout 调短一点,比如 5 秒,这样一旦客户端不主动发数据,连接能尽早释放。但也不要调太短,否则一些慢速网络环境下的正常请求会被误杀。
6.4 访问日志和监控不能少
你选再好的服务器,如果没有日志和监控,出了问题一样抓瞎。建议至少把访问日志打开,并接入你现有的日志收集系统。还可以通过 Spring Boot Actuator 的 server 相关端点来查看当前连接数、线程数。记住,性能优化是一个迭代过程,不是一锤子买卖。
6.5 版本升级要谨慎
Spring Boot 从 2.x 升级到 3.x 的时候,内嵌服务器的配置发生了不小的变化。比如 Tomcat 的很多参数还是保留,但 Jetty 和 Undertow 的某些选项被改名或者移动了位置。如果你在网上搜到一篇几年前的配置文章,千万别直接复制,先查一下你当前版本的官方文档。最好的做法是升级之后,看一下启动日志里有没有提示废弃的配置项,再结合测试来逐步调整。
6.6 容器环境中要合理设置内存参数
在 Docker 里跑 Spring Boot 应用时,很多人只设置了容器的内存上限,却没有告诉 JVM。这会导致容器认为可用内存很多,给线程池和堆分配过大空间,最后被操作系统强制杀掉。这时候你需要使用 -XX:MaxRAMPercentage 这样的参数来让 JVM 根据容器限制自动调整。合理的堆大小与线程数搭配,比盲目堆内存更有效。
七、文章总结
今天我们聊了 Spring Boot 里三兄弟的故事。Tomcat 是那个最让人放心的老大哥,适合大多数企业应用;Jetty 是轻巧灵活的瘦身专家,适合内存紧张、需要快速启动的场景;Undertow 是后来崛起的高并发好手,特别适合长连接和响应式应用。
选型没有绝对的对错,关键是看你的业务长什么样。如果你追求稳定和资料丰富,就用 Tomcat;如果你需要极致的空间利用,就看看 Jetty;如果你要支撑大量并发连接,Undertow 值得一试。换服务器不难,只需要改几行依赖和配置,但调优这件事需要耐心和实验。希望这篇文章能给你一个靠谱的起点,让你在生产环境里少踩几个坑。
评论
围绕“Spring Boot内嵌服务器定制优化:Tomcat、Jetty、Undertow的选型与调优策略”参与讨论