Spring Boot 内嵌 Web 服务器的原理是什么?如何替换和调优?
简化版
Spring Boot Servlet 应用通过 ServletWebServerApplicationContext 在容器刷新期间取得 ServletWebServerFactory,创建并启动内嵌服务器,再注册 DispatcherServlet 等 Servlet 组件。spring-boot-starter-web 默认使用 Tomcat,可以排除默认服务器依赖并引入受支持的替代实现,或通过属性和 WebServerFactoryCustomizer 调整端口、线程、连接等参数。
详细版
内嵌服务器把应用和服务器依赖打进同一个可执行归档,由 main 方法启动,不再要求先把 war 部署到外部容器。自动配置根据类路径提供相应 WebServerFactory,Web ApplicationContext 用它创建 WebServer,并把 Servlet、Filter、Listener 注册到服务器。
配置调优必须结合具体服务器和 Boot 版本:通用项可用 server.*,实现特有项位于 server.tomcat.* 等命名空间,更复杂的定制使用工厂 Customizer。线程数、连接数、accept 队列与数据库连接池是不同资源,不能只把服务器线程无限调大。
完整版教学
一、为什么能直接 java -jar 启动
传统部署由外部 Servlet 容器先启动,再加载 war。Boot 把服务器库作为应用依赖,由 SpringApplication 创建 Web ApplicationContext,应用上下文反过来创建服务器,因此进程入口属于应用自身。
java -jar app.jar
这种模式让服务器版本、应用依赖和启动参数随应用一起交付,环境一致性更高,也便于容器化部署。
二、服务器如何进入 Spring 生命周期
Servlet Web 应用的自动配置会提供一个 ServletWebServerFactory。ApplicationContext 刷新时创建 WebServer,并收集 ServletContextInitializer,把 DispatcherServlet、Filter、Servlet 和 Listener 等组件注册进去。
服务器不是一个普通 Controller Bean,但其创建、启动和停止受 ApplicationContext 生命周期协调。若端口占用或服务器初始化失败,上下文刷新会失败,应用不会进入 ready 状态。
三、如何替换默认服务器
Maven 中可以从 Web Starter 排除默认 Tomcat Starter,再引入 Boot 支持的另一种 Servlet 服务器 Starter。是否可替换以及支持哪些版本,要以当前 Spring Boot 版本的依赖清单为准,不能随意混搭任意服务器版本。
响应式 WebFlux 默认使用的服务器和线程模型又是另一套选择。项目同时存在 MVC 与 WebFlux 依赖时,应用类型判定也会影响最终启动哪种 Web 栈,不能把“引入一个服务器 jar”当作唯一决定因素。
四、属性配置与编程式定制
server:
port: 8081
shutdown: graceful
tomcat:
threads:
max: 200
常用配置优先写属性,便于不同环境覆盖。属性未覆盖的高级选项可注册 WebServerFactoryCustomizer,对具体工厂执行编程式设置;自定义时要避免无意覆盖 Boot 已绑定的其他配置。
测试中可设置 server.port=0 让操作系统分配空闲端口,并通过运行时提供的本地端口获取实际值,避免并行测试端口冲突。
五、线程与连接调优
服务器调优要观察请求是 CPU 密集、阻塞 I/O 还是长连接,并结合 CPU、内存和延迟目标。Servlet 工作线程增多可能提高并发,也可能增加上下文切换,并把压力推向小型数据库连接池。
需要一起观察最大连接、等待队列、keep-alive、请求体限制、超时和下游容量。任何默认值都可能随服务器与 Boot 版本变化,生产配置应通过指标和压测确定,不应照抄固定数字。
六、优雅停机与外部容器部署
启用 graceful shutdown 后,服务器停止接收新请求,并在生命周期超时范围内等待进行中的请求完成;超时后仍会强制结束。编排平台还应先摘除流量,再给应用足够的终止宽限期。
Boot 仍支持 war 方式部署到兼容的外部 Servlet 容器,通常需要调整打包、服务器依赖作用域并提供 Servlet 初始化入口。jar 内嵌模式更常见,但不是唯一部署形式。
七、用容量链路理解调优
假设 Tomcat 最大工作线程为 200,而数据库连接池只有 20 条,每个请求都同步占用 1 条数据库连接 100ms。即使把服务器线程从 200 调到 500,数据库阶段理论吞吐上限仍约为 20 / 0.1 = 200 次/秒;更多线程主要增加等待、栈内存与上下文切换。调优必须找最窄资源,而不是只改最显眼的线程参数。
| 资源 | 控制什么 | 调得过小 | 盲目调大 |
|---|---|---|---|
| 最大连接数 | 可保持的网络连接 | 高峰拒绝或排队 | 文件描述符、内存上升 |
| 工作线程 | 同时执行的阻塞请求 | CPU/下游未吃满 | 切换和等待增加 |
| accept 队列 | 暂未分配线程的连接 | 突发请求被拒 | 只把超时推迟 |
| 数据库连接池 | 同时访问数据库的请求 | 线程等待连接 | 压垮数据库 |
| 超时 | 资源占用上界 | 正常慢请求失败 | 故障请求长期占资源 |
socket connections
↓ max-connections / accept queue
servlet worker threads
↓
database pool / remote client pool
↓
downstream capacity
优雅停机也要结合版本核对默认值。无论默认是 graceful 还是 immediate,都应显式确认 server.shutdown 与 spring.lifecycle.timeout-per-shutdown-phase,并让编排平台的终止宽限期大于应用等待窗口,否则应用还在排空请求时进程已被强杀。
调优钩子:连接决定“能排多少”,线程决定“能同时做多少”,下游池决定“真正能推进多少”;三者必须连成一条容量链。
八、常见误区与追问
- 误区:内嵌 Tomcat 是 Spring 容器里的普通 Controller Bean。 Web ApplicationContext 通过 WebServerFactory 创建并管理服务器生命周期,Servlet 组件才被注册进服务器。
- 误区:把最大线程调大就一定提高吞吐。 当数据库、远程服务或 CPU 已饱和时,更多线程只会增加排队和资源消耗。
- 误区:引入 Jetty 依赖就一定替换成功。 需要排除默认服务器 Starter、引入兼容替代依赖,并确认 Web 栈和自动配置条件最终选择。
- 追问:为什么测试常用 server.port=0? 操作系统分配空闲端口,可避免并行测试争抢固定端口,实际端口需从运行上下文获取。
- 追问:Customizer 与 server. 属性怎样选择?* 常规、可外部化选项优先用属性;目标服务器的高级能力再用 WebServerFactoryCustomizer,并避免覆盖已绑定设置。
- 追问:端口占用为什么会让整个应用启动失败? 服务器创建发生在上下文初始化流程中,绑定端口失败意味着 Web 上下文无法成功刷新并进入 ready。
九、加强记忆
内嵌服务器的关键不是“把 Tomcat 放进 jar”这么简单,而是 Web ApplicationContext 通过 WebServerFactory 在刷新期间创建服务器并注册 Servlet 组件。替换靠依赖组合,常规配置走 server.*,高级配置走 Customizer,调优必须与下游资源和优雅停机一起考虑。