搞懂 Servlet 容器这一篇就够原理、Spring Boot 实战和坑点全记录我先说个结论绝大多数 Java Web 开发者每天打交道的东西里被误解最深的其实不是 Spring、不是 MyBatis而是那个默默蹲在底层干活的老哥——Servlet 容器。你点一个按钮、发一个请求、打开一个页面背后全是它在调度。但很少有人认真问一句它到底是什么为什么 Spring Boot 一跑起来它也跟着起来了为啥有时候接口明明没问题运行一段时间就假死这些问题的根源十有八九都落在 Servlet 容器的理解和配置上。这篇文章面向两类人一是刚接触 Java Web、搞不清楚 Tomcat 和 Spring Boot 关系的初学者二是已经写了一段业务代码、但遇到容器相关疑难杂症时只能来回搜资料的开发者。我会先讲清楚 Servlet 容器的底层原理再带你把 Spring Boot 内嵌容器的配置玩明白最后整理一份我自己这几年踩过的坑和排查思路尽量帮你绕开那些“明明能跑却不知道为什么”的玄学时刻。1. Servlet 容器到底是什么先拆开“Servlet”和“容器”两个词1.1 从 Servlet 说起Java Web 领域的老黄牛在很多人的认知里项目一启动就是 Spring Boot 在干活甚至有人觉得“JavaWeb Spring MVC”。但往前倒十年当时没有 Spring Boot甚至连 Spring MVC 都还没成气候大家写接口靠的是 JSP Servlet而 Servlet 就是 Java 处理 HTTP 请求的“根”。Servlet 本质上是一组接口规范由 Java EE后来改叫 Jakarta EE定义。它规定了一个 Java 类要具备哪些能力才能接收一个 HTTP 请求并返回响应。最核心的接口就叫Servlet里面定义了init()、service()、doGet()、doPost()、destroy()这几个生命周期方法。你写的每一个 Controller 方法最终被框架翻译成对 Servlet 的调用你注册的每一个 Filter、每一个 Listener本质上也是在 Servlet 规范之上做文章。但这里有个关键点Servlet 只是接口和规范它本身跑不起来。规范是一张图纸光有图纸盖不了楼你得有一个环境让它真正运行。这个环境就是 Servlet 容器。1.2 容器Servlet 的宿舍楼和物业公司如果把 Servlet 比作一个“干活的人”那 Servlet 容器就是这人的宿舍楼加物业公司。宿舍楼负责给人提供房间创建实例、配置水电初始化参数、安排门禁请求路由物业公司负责定期检查房屋安全生命周期管理、协调公共资源线程池、连接池、内存分配。更准确地说Servlet 容器负责的事情有四件生命周期管理什么时候创建 Servlet 实例、什么时候调用init()、什么时候回收全由容器说了算。你写的PostConstruct、PreDestroy方法其实都是在容器的生命周期回调链里挂着的。网络通信容器负责监听端口、接收 TCP 连接、解析 HTTP 请求报文再把请求封装成一个HttpServletRequest对象把响应封装成HttpServletResponse。这也是为什么你写 Controller 时根本不用关心 Socket、不用手动解析 HTTP 报文——脏活全被容器干了。多线程处理每个请求通常在独立的线程中执行而这个线程怎么来、怎么回收、怎么排队也由容器决定。我们经常讨论的“线程池耗尽”问题说的就是容器这层出了问题。请求路由根据 URL 和 Servlet 的映射关系找到对应的处理逻辑。在 Spring MVC 里所有请求先进DispatcherServlet再由它派发给各个RequestMapping方法但“把请求分发到 DispatcherServlet”这件事本身还是 Servlet 容器干的。1.3 区分三个“容器”Servlet 容器、Spring 容器、容器化里的容器热词里“容器”出现了很多次而且指向还不一样这点必须单独拎出来讲清楚不然很多人会懵。Servlet 容器本文的主角典型代表是 Tomcat、Jetty、Undertow负责运行 Servlet 规范下的 Web 应用。Spring 容器 / IoC 容器负责管理 Spring Bean 的创建、依赖注入、销毁。它运行在 Servlet 容器内部两者是嵌套关系。Spring Boot 启动时先是 Servlet 容器被初始化然后 Spring 容器被创建并装配最后DispatcherServlet被注册进 Servlet 容器。Docker 容器操作系统层面的进程隔离技术核心是 cgroup 和 namespace。它和前面两个完全不是一个层级的东西——你完全可以把一个跑着 Tomcat 的 JVM 进程再塞进 Docker 容器里。搜索词里的“容器资源隔离”“容器 centos 启动 sshd 失败”“宝塔内某个容器让他使用宿主机的网络环境”说的都是这一层的东西。这三者容易混是因为中文都叫“容器”但逻辑层次完全不同。你写 Java Web 时主要面对的是前两个你搞运维和部署时面对的是第三个。后面我会专门讲一下 Servlet 容器和 Docker 容器叠加使用时的一些资源分配陷阱。2. 主流 Servlet 容器选型对比Tomcat、Jetty、Undertow 各自的脾气2.1 三足鼎立的格局市面上符合 Servlet 规范的容器不少但真正在 Java 后端圈子形成广泛应用的主要是这三家Tomcat、Jetty、Undertow。Tomcat 是资历最老的由 Apache 软件基金会维护。它从 1999 年走到今天稳定性经过海量项目验证文档最多遇到问题搜到的解决方案也最多。它的结构相对“重”线程模型是经典的 BIO/NIO 混合默认配置下功能全、线程参数多对新手来说配置文件可能稍显得复杂。Jetty 出身 Eclipse 基金会设计哲学是“轻、快、嵌入友好”。它特别适合那些需要在一个 JVM 进程里动态启停 Web 服务的场景比如大数据领域的 Hadoop、Spark 生态里就大量使用 Jetty。启动速度快内存占用小但在极端高并发下的绝对吞吐能力一般比 Tomcat 要弱一些。Undertow 是红帽Red Hat主导开发的项目主打高性能非阻塞。它基于 JBoss 的 XNIO全异步 I/O在静态资源服务和低延迟场景下表现相当亮眼。WildFly 应用服务器默认使用 Undertow很多重视性能的新项目也喜欢把它塞进 Spring Boot 里替换默认容器。2.2 Spring Boot 为什么默认选 Tomcat很多人问过Spring Boot 默认内嵌容器是 Tomcat是不是因为它性能最好还真不完全是。Spring Boot 选 Tomcat 做默认最重要的原因是生态和兼容性。Tomcat 是最接近“标准实现”的 Servlet 容器绝大多数 Java Web 项目的生产环境原本就跑在 Tomcat 上。Spring Boot 横空出世的时候它的目标之一是让老项目平滑迁移默认选择 Tomcat 能最大程度降低迁移成本。再加上 Tomcat 对 Servlet 规范的实现最保守、最完整Spring Boot 团队只需要针对它做深度适配和自动化配置就能覆盖绝大多数使用场景。如果你不刻意改动Spring Boot 启动时会在spring-boot-starter-web里把tomcat-embed-core之类的一系列内嵌 Tomcat 依赖拉进来然后在ServletWebServerApplicationContext里自动创建TomcatServletWebServer。这个过程中你基本看不到任何配置文件端口、协议、线程池全都有默认值。2.3 实战把 Tomcat 换成 Undertow如果你的项目对性能和内存占用比较敏感想从 Tomcat 切到 Undertow其实非常简单只需要在 Maven 依赖里做点手脚。第一件事排除掉spring-boot-starter-web里的 Tomcat 依赖。第二件事引入 Undertow 的 starter。第三件事把原来的server.tomcat.*配置改成server.undertow.*。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId /exclusion /exclusions /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-undertow/artifactId /dependency配置上比较关键的有几个server: port: 8080 undertow: io-threads: 4 worker-threads: 32 direct-buffers: trueio-threads处理 I/O 事件的线程数一般按 CPU 核数乘以 2 设置。worker-threads处理业务逻辑的线程数相当于 Tomcat 里的max-threads。direct-buffers是否使用堆外内存做缓冲区。开启后能减少垃圾回收压力但如果系统物理内存紧张反而容易引发 OutOfMemory。实测下来纯静态文件或简单 JSON 接口下Undertow 的吞吐量比默认配置的 Tomcat 高出一些内存占用也更低。但要注意Undertow 的社区资料比 Tomcat 少不少遇到冷门问题搜索成本高。我的建议是新项目想尝鲜可以用 Undertow老项目稳定为主继续 Tomcat 没毛病。3. Spring Boot 实战内嵌容器到底怎么工作配置怎么调3.1 内嵌容器的原理“一键启动”的秘密以前做 SSM 项目时部署一个 Web 应用要经历写代码 - 打 WAR 包 - 把它丢进 Tomcat 的 webapps 目录 - 手动启动 Tomcat - 检查日志。每一步都容易出幺蛾子而且对新手特别不友好。Spring Boot 改变了这一切核心思路就是把 Servlet 容器从“外部依赖”变成“项目内部的一部分”。你执行的SpringApplication.run()方法内部会根据当前 classpath 里有哪些容器依赖自动创建一个对应的内嵌容器实例——有 Tomcat 依赖就创建 Tomcat有 Undertow 依赖就创建 Undertow。然后容器会启动在server.port指定的端口上而 Spring 容器和DispatcherServlet的装配也会全部自动完成。这背后的关键类叫ServletWebServerFactory。它是一个工厂接口Spring Boot 为每种容器都提供了一套自动装配实现TomcatServletWebServerFactoryJettyServletWebServerFactoryUndertowServletWebServerFactory当你在 application.properties 或 application.yml 里修改server.port8081实际上是在改变这个工厂的端口属性。容器启动后它负责监听网络端口把 HTTP 请求交给 Spring Web MVC 处理链。这个设计带来的直接好处是你不需要在自己的电脑上安装任何独立的 Web 服务器只要有 JDK 就能跑 Web 项目。生产环境里也只需要在服务器上装一个 JRE 然后运行 jar 包——部署流程从一个多小时缩短到几分钟。3.2 核心配置参数详解端口、线程池、连接超时Spring Boot 暴露的容器配置项非常多但真正跟高并发、稳定性强相关的其实就那么几个。我按重要程度给大家捋一遍。首先是端口和协议server: port: 8080 address: 0.0.0.0port没什么好说的需要注意的是port如果设成 0容器会自动选一个随机空闲端口启动这在测试环境里特别有用避免端口冲突。address一般保持默认如果你只希望内网访问可以绑成127.0.0.1。然后是 Tomcat 的关键线程池参数server: tomcat: threads: max: 200 min-spare: 10 accept-count: 100 max-connections: 8192 connection-timeout: 20000这几个参数的含义分别是max最大工作线程数。每个请求进来后如果目前没有空闲线程处理它就会排队等待直到有空闲线程。min-spare最小空闲线程数。Tomcat 启动时会预创建这么多线程用来应对突然的流量尖峰。accept-count等待队列长度。如果所有工作线程都忙着新来的请求会先进入这个队列等待队列满了之后连接才会被拒绝。max-connections最大连接数。这个表示服务器能同时接受的 TCP 连接数量超过之后的新连接会被拒绝或等待。connection-timeout连接超时时间单位是毫秒。如果一个连接建立后在这个时间内没有发送请求数据就会被关闭。这里有个常见的理解误区很多人以为max-connections越大越好max-threads配个几千就能扛住高并发。但实际上线程数太多反而会导致 CPU 频繁切换上下文性能直线下降。线程池大小应该结合 CPU 核数和服务的 I/O 密集程度来算。如果服务是 I/O 密集型的比如大部分时间在查数据库、调远程接口线程数可以适当大一些如果是 CPU 密集型比如做大量计算线程数接近 CPU 核数或略高于核数就够了。注意Spring Boot 2.3 之前Tomcat 的连接器参数用的是server.tomcat.max-threads注意没有threads层级2.3 之后调整为server.tomcat.threads.max。如果你在升级 Boot 版本后发现配置没生效先查这个命名差异。3.3 结合实战如何根据压测结果反过来调参数只看理论参数不落地没意义。我分享一下自己调优时的通用流程。假设你有一个订单查询接口单次请求平均耗时 80ms其中 60ms 花在数据库查询上。如果你的目标是支撑每秒 500 的 QPS那工作线程池其实不需要太大。简单估算一下每秒需要处理请求数 500每个请求占一个线程 80ms那么平均并发线程数 500 * 0.08 40。考虑到请求分布不可能是均匀的会有峰值波动留出一定的余量把max设为 80 到 100 是比较合理的。如果直接把max设成 500只会白白消耗内存和 CPU 切换开销。当然这只是粗略的估算方式真正上线前一定要做压测。拿 JMeter 或 wrk 压一轮观察线程池活跃度、CPU 利用率、请求百分位延迟再反过来微调参数。我见过很多团队把 Tomcat 线程数拉满 1000结果是接口平均延迟没降下来反而因为 CPU 耗尽导致 GC 频繁系统整体吞吐更低。压测时的另一个实用小技巧在 Spring Boot 里配置一个TomcatConnectorCustomizer把连接器的AcceptCount适当调大一点可以吸收突发流量。比如抢购场景瞬时来了 2000 个请求线程池只有 100如果把等待队列设成 500就能让大部分请求先排队而不是直接报连接拒绝错误。但注意队列不是越大越好因为排队的请求最终可能在等待中过期一旦客户端超时队列里的请求白占资源。3.4 WebSocket 场景下容器配置的额外注意点热词里出现了“Spring Boot 集成 web socket yml 配置”这块我简单提一个坑。Spring Boot 做 WebSocket 时如果你用的是 Tomcat配置上除了基本的 WebSocket 端点之外有几个跟容器强相关的参数要格外留意。server: tomcat: max-swallow-size: 2MB这是控制 Tomcat 垃圾回收已接收数据行的最大大小。在 WebSocket 文件上传或长连接传输大消息时如果这个值配小了会出现消息被截断或者连接异常关闭。更重要的是WebSocket 长连接会长期占用 Tomcat 的工作线程或 NIO 事件线程。如果你把线程池max设置得很小同时又有大量 WebSocket 长连接开着同样会造成线程池耗尽、普通 HTTP 请求全部排队。项目里如果 WebSocket 连接数很大建议单独把 Tomcat 的maxConnections调高同时把 WebSocket 的消息处理放在独立的线程池中执行避免阻塞容器的请求线程。4. 生产环境实战从内嵌容器到 Docker 容器再到资源隔离4.1 jar 包直接部署 vs WAR 包外置 Tomcat 部署内嵌容器的最大优势是简单但“简单”不代表“永远最佳”。在某些场景下你还是得考虑使用传统的外置 Tomcat 部署 WAR 包方案。什么时候更适合外置容器三类典型场景一是公司运维规范要求所有应用统一用同一个 Tomcat 实例管理便于集中处理证书和日志二是你需要在同一个 Tomcat 下部署多个彼此隔离的 Web 应用共享同一个端口三是某些遗留系统的监控脚本只认 Tomcat 的 catalina.out 日志格式。Spring Boot 要打 WAR 包也简单在pom.xml里把打包方式改成warpackagingwar/packaging让启动类继承SpringBootServletInitializerSpringBootApplication public class DemoApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(DemoApplication.class); } }然后正常mvn clean package生成的 WAR 包丢进 Tomcat 的webapps目录即可。这样做有一个明显的变化内嵌容器模式下的main方法启动逻辑不再起作用Tomcat 会按照标准的 Servlet 规范去加载应用。而且 WAR 包的 JSP 支持、类加载机制跟内嵌 jar 模式有明显差异如果项目里还在用 JSPWAR 部署是更稳妥的选择。注意Java 9 之后的模块化对 WAR 部署有一些影响尤其是涉及模块信息不完整或者非法反射访问时。我见过几次外部 Tomcat 部署 Boot 3 项目时出现IllegalAccessError基本都能通过添加--add-opens参数解决但比内嵌 jar 模式麻烦不少。4.2 Docker 部署 Servlet 容器JVM、容器和资源限制的三方拉扯现在生产上多是用 Docker 容器跑 Spring Boot 服务。这里有个非常隐蔽的问题默认情况下 JVM 不感知自己在 Docker 容器里它傻乎乎地拿宿主机的 CPU 核数和内存来配置自己的堆大小。你在宿主机上看到 32 核 128G 内存于是没做任何设置直接跑java -jar app.jarJVM 会默认把堆大小设置为物理内存的 1/432G。但 Docker 容器实际只能使用 2 核 4G结果就是容器频繁触发 Full GC甚至 OOM Killer 直接把 Java 进程干掉。解决方法是显式设置 JVM 参数。从 JDK 10 开始JVM 默认开启了UseContainerSupport会自动识别 cgroup 的 CPU 和内存限制。但如果你用的是 JDK 8特别注意JDK 8u191 之前的版本不识别 Docker 限定必须手动加-XX:MaxRAMPercentage50之类的参数。下面是我常用的容器启动参数java -XX:UseContainerSupport -XX:MaxRAMPercentage75.0 -XX:InitialRAMPercentage75.0 -XX:MaxMetaspaceSize256m -XX:UseG1GC -jar app.jar这里有两个跟 Servlet 容器强相关的点一是MaxRAMPercentage75给 JVM 堆留出容器内存的 75%剩下的 25% 留给堆外内存、线程栈、Metaspace。Tomcat 和 Netty 这类库会大量使用堆外内存如果直接开到 90很容易因为堆外内存溢出导致容器被内核杀掉。二是如果你发现请求吞吐不高但容器频繁被重启八成不是 Servlet 容器的线程数不够而是 JVM 堆或线程栈超限。可以先看dmesg或 Docker 的OOMKilled状态再决定是调线程池还是调内存参数。4.3 容器资源隔离、镜像安全和容器安全的实际联动搜索词里反复出现“容器资源隔离”“镜像安全”“容器安全”这些确实是从 Docker 角度来审视 Servlet 容器部署的重要话题。这里我不展开讲 Docker 底层原理就说几个跟 Spring Boot 服务直接相关的经验。资源隔离层面生产环境建议给每个 Spring Boot 容器加上明确的 CPU 和内存配额docker run -d --name orderservice \ --cpus2 \ --memory4g \ --memory-swap4g \ -p 8080:8080 \ orderservice:1.0.0--memory-swap要和--memory一样大目的是禁用 swap 使用。对 Java 应用来说swap 一旦启用JVM 的响应延迟会急剧恶化因为你不知道什么时候内存页被换到磁盘上去了。垃圾回收本应该在毫秒级完成结果变成几百毫秒甚至几秒。镜像安全层面有几个容易忽略的点基础镜像尽量选择官方精简版比如eclipse-temurin或amazoncorretto避免把所有依赖都打进最终镜像容器内运行用户不要用 root在 Dockerfile 里通过USER指令切换为低权限用户健康检查不要只探 TCP 端口应该探应用级的/actuator/health接口否则会出现容器看起来是活的但应用早就线程池耗尽的情况。镜像安全方面还有一个细节很多人做镜像时喜欢把application.yml直接打进镜像里里面带着数据库密码和其他密钥。这个习惯在生产环境中是致命的镜像仓库一旦泄露数据库就等于裸奔。正确的做法是通过环境变量或挂载 Volume 注入配置镜像本身保持无状态。5. 常见问题与排查技巧实录让 Servlet 容器“露出马脚”的瞬间5.1 端口被占用开发环境最经典的一天报错信息一般是Port 8080 was already in use。绝大多数人第一反应是“我其他进程占用了 8080”然后开始到处杀进程结果发现怎么杀都杀不掉。大概率是之前启动的 Spring Boot 进程没被完全终止只是在 IDE 的控制台里点了“停止”后台的 Java 进程还在跑。排查时先找进程再决定怎么处理lsof -i :8080 # 或者 netstat -anp | grep 8080找到 PID 之后确认是不是自己的 Java 进程再kill -9处理。如果你不想每次都处理这个麻烦事可以做一个“失败快速暴露”的设置在开发环境里使用随机端口server.port0启动后日志会直接打印实际的端口号从根上避开冲突。特别是同一台机器要同时跑多个微服务实例联调的时候这个方式几乎零成本。5.2 静态资源 404Servlet 容器映射规则没搞对很多次新项目跑起来前后端没联调前端人员问“为什么我放在src/main/resources/static里的图片直接访问不到”。Spring Boot 默认的静态资源映射路径是/**它会去这几个地方找资源classpath:/META-INF/resources/、classpath:/resources/、classpath:/static/、classpath:/public/。你的文件如果放在src/main/resources/static/images/a.png那么访问路径应该是http://localhost:8080/images/a.png不是http://localhost:8080/static/images/a.png。这个配置表面上是 Spring MVC 的东西其实底层跟 Servlet 容器的默认 Servlet 有关。当请求路径没有匹配到任何RequestMapping时请求会落到容器的默认 Servlet 上再由默认 Servlet 去解析静态资源。如果你自己写了WebServlet(urlPatterns /)覆盖了默认 Servlet 映射静态资源就会一片 404。这种情况在小团队里很常见因为前端工程师可能会把某些 SPA 路由处理交给后端解决。5.3 线程池耗尽慢接口拖垮整个应用这是线上最让人头疼的问题。典型症状是某一天某个接口突然变慢紧接着整个服务的其他接口也变慢了再后来健康检查都开始失败服务不得不重启。进程里看到的现象是 Tomcat 工作线程全部处于RUNNABLE状态或WAITING状态jstack里能看到大量线程卡在某个数据库查询或远程调用点。本质上就是因为某个下游服务的慢调用占满了所有容器工作线程导致新的请求无法处理。处理思路分几步第一步先找到占线程最多的代码点。用jstack pid连续抓几次线程快照看哪些线程堆积在同一个 Stack Trace 段。通常都是连接池等待或者第三方 HTTP 客户端的超时设置过长。第二步给下游调用设置超时。很多人设置 RestTemplate 的connectTimeout和readTimeout都是用默认值一下就是几十秒这等于主动把线程池让出去。一般建议connectTimeout2000readTimeout3000结合重试策略宁可牺牲少量请求也不能拖垮整个应用。第三步做线程池隔离。如果你的应用里有高优先级低延迟的接口同时也存在文件导出、报表查询这类慢接口可以考虑用自定义线程池把慢任务从 Tomcat 工作线程中剥离出去。比如将文件导出接口设置为异步执行Tomcat 线程立刻释放用户轮询导出进度。这个方法虽然不改变 Tomcat 的总线程数但避免了慢任务对常规接口的干扰。5.4 Spring Boot 3 和 Jakarta 命名空间迁移的坑Spring Boot 3 是一个大版本最核心的变化是从javax.servlet迁移到jakarta.servlet。很多人直接把老项目切换到 Boot 3结果编译报一堆找不到符号的错误第一反应是“依赖没拉全”其实问题出在导入路径上。以前你写import javax.servlet.http.HttpServletRequest;现在必须改成import jakarta.servlet.http.HttpServletRequest;看似只是改个前缀但影响面很大所有依赖 Servlet API 的第三方库都需要升级到支持 Jakarta 的版本自定义的 Filter、Interceptor、ServletContextInitializer 全部都要动如果你的项目里有老的web.xml描述符也要确认格式是否正确。还有一个很隐蔽的地方OpenAPI 文档库、文件上传库、验证框架里如果反射依赖了javax.servlet字符串运行时会直接抛NoClassDefFoundError。这种问题不像编译期报错那么显眼通常在启动或首次调用接口时才暴露。排查时先全面搜索代码里残留的javax.servlet再把所有用到javax.annotation之类的包也一起替换掉。5.5 常见问题速查表现象可能原因排查优先级启动报端口占用旧进程未关闭、端口冲突先用lsof确认占用进程接口偶发超时CPU 不高等待队列过长、下游调用慢抓jstack检查线程等待状态容器内内存溢出被重启JVM 未感知 cgroup 限制增加-XX:MaxRAMPercentage参数静态资源 404覆盖了默认 Servlet 映射、路径错检查静态资源路径和自定义WebServletWebSocket 连接频繁断开线程池耗尽、连接超时过短调大maxConnections或拆独立线程池上生产环境后随机性卡顿GC 频繁、堆分配过小观察 GC 日志调整堆大小升级 Boot 3 后接口 404Servlet 路径匹配策略变化检查spring.mvc.servlet.path和 Controller 映射6. 一些值得注意的实操心得关于容器配置的“反直觉”发现最后再聊几个我自己在长期实战里发现的不太直觉、但很实用的点希望能帮大家少走点弯路。第一个心得很反直觉默认的最大线程数不是越高越好。Spring Boot 的server.tomcat.threads.max默认值是 200。做性能调优时别一上来就翻倍很多场景下 200 完全够用甚至系统 CPU 只有 4 核时200 已经偏大了。你要是实在不确定先去压测而不是拍脑袋调线程池。第二个心得是尽量在开发和测试环境就用外置容器的模式跑一跑。虽然 Spring Boot 内嵌容器帮我们省了很多事但生产环境如果用 TOMCAT 外置部署开发环境一直用内嵌容器的话很多因为容器版本差异导致的问题比如 JSP 支持、Session 处理会拖到上线才爆出来那时候排查成本可就高了。至少在测试环境里保持和生产一致的部署形态。第三个心得健康检查不要只做 HTTP 端口探测。我见过很多项目挂了负载均衡的 HTTP 探活探的还是一个静态页面地址结果线程池都满了静态页面照样用极少的资源返回 200负载均衡器以为服务健康继续往里打流量最终彻底雪崩。后来我们统一改成探/actuator/health并且在这个接口里增加了线程池活跃度的判断——当活跃线程数超过最大线程数的 80% 时健康状态返回不健康负载均衡器自动摘除节点。这个改动在流量高峰期真的救过我们好几次。第四点更实际一点别忽略日志里的Tomcat started on port这类提示。很多新手看到这段日志以为只是一句普通输出但其实它说明整个 web 容器已经完成初始化。如果项目里需要做“容器启动完成后执行某些逻辑”正确的做法是实现ApplicationRunner或者监听ServletWebServerInitializedEvent事件而不是在main方法里直接写。第五点是给用 IntelliJ IDEA 社区版的读者提个醒社区版虽然没有 Spring 官方插件的全套功能但跑 Spring Boot 项目完全没问题。你在 Run Configuration 里直接选Application主类选xxxApplication加上 JVM 参数后启动即可。如果你遇到社区版启动后控制台没有彩色日志或者端口显示不出来多半是spring.output.ansi.enabled配置问题跟 IDEA 版本关系不大。写到这里关于 Servlet 容器、Spring Boot 实战和容器部署的核心内容基本都覆盖了。回想这几年带项目、排查线上故障的经历说实话很多让人熬夜的问题到最后都和容器理解不到位有关要么不知道请求一开始是怎么被分配的要么不清楚线程池的状态意味着什么要么把内嵌容器和外置容器混为一谈。搞懂 Servlet 容器不是一个可选加分项而是 Java Web 开发的底层基本功。希望这篇文章能让你在下次遇到“运行异常”“假死”“内存暴涨”时脑子里能第一时间浮现出容器的运作逻辑而不是对着日志干瞪眼。