一、为什么我们要关心这件事

好多同学可能觉得,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 值得一试。换服务器不难,只需要改几行依赖和配置,但调优这件事需要耐心和实验。希望这篇文章能给你一个靠谱的起点,让你在生产环境里少踩几个坑。