Tomcat、Jetty、Undertow:Connector 与执行模型如何改变故障
“换成 Undertow 就是非阻塞”“Jetty 线程越少越快”“Tomcat 一个请求一条连接线程”都把实现压成了标签。现代容器普遍把连接 I/O 与阻塞应用执行分开,只是在组件模型、派发策略、线程池责任和协议扩展上选择不同。真正影响架构的是:阻塞代码在哪个线程运行,内部任务与业务任务是否竞争同一池,过载如何背压,升级时 Servlet/Java/协议矩阵是否闭合。
Tomcat:Connector 把协议处理送入容器调用链
Tomcat 的 Connector 负责端点、协议处理与适配,解析请求后进入 Engine、Host、Context、Wrapper 组成的容器管道,Valve 与 Filter 分别位于容器和 Web 应用层。NIO endpoint 使用 poller 观察 socket 事件,准备好的处理任务交给 executor;Servlet 阻塞代码运行在执行线程,不应把 poller 误称为每请求线程。
Tomcat 11 对齐 Servlet 6.1,要求 Java 17+。升级旧应用时要同时处理 javax 到 jakarta、容器 major、框架基线、JSP/WebSocket 与第三方 Valve/Filter。只把 WAR 放进新容器、看到端口起来,不代表所有组件已兼容。
Connector 的 max connections、accept backlog、executor max threads 和请求解析限制属于不同队列。连接能被接受不代表立即有线程执行;线程满时请求可停留在容器内部或网络层。排查需要同时看 connection、keep-alive、processor、executor active/queue 和应用下游池,而不是只看 maxThreads。
Jetty:任务生产与执行策略是核心
Jetty Server 连接 Connector 与 Handler,Servlet 是可选的 EE 环境能力;Jetty 12 Core Handler 与 Servlet API 不是一套编程模型。Jetty 的执行策略在 producer/consumer 之间选择直接消费、生产后异步执行、先执行再继续生产等模式,目标是在缓存局部性和阻塞隔离之间切换。
Jetty 线程架构说明 QueuedThreadPool 除应用任务外,还向 acceptor、selector 与 reserved executor 租赁线程。因此 maxThreads 不能全当业务并发;把队列简单设为很小来限流,可能阻止容器关键内部任务并形成锁死。并发保护应在请求入口、业务隔离池和下游配额上实施。
Jetty 12.0 是稳定线并支持多代 EE 环境模块;12.1 面向 EE 11 仍要按其实际状态评估。选择模块时必须锁定 Java、EE 环境与 Servlet namespace,避免 Core、ee8、ee9、ee10/ee11 依赖混装。Jetty 支持把应用代码调度到虚拟线程,但内部租赁线程、连接和下游容量仍需保留。
Undertow:I/O 线程绝不能被阻塞
Undertow 基于 XNIO worker。I/O 线程处理多个连接的非阻塞事件,阻塞 Servlet 或显式 blocking handler 应派发到 worker 的阻塞任务线程池。若在 I/O 线程执行数据库查询、文件读取或等待锁,同一线程负责的其他连接都会停顿,形成范围远大于单请求的队头阻塞。
Undertow Handler 链比完整 Servlet 部署更轻,但缺少的 Session、认证、部署与兼容能力要由组合的 handler 或 Servlet 模块补齐。原生 Handler 与 Servlet 混合时必须知道当前 exchange 是否已 dispatch、代码位于 I/O 还是 worker 线程,以及阻塞流 API 是否允许使用。
examples/backend-development/servlet/tomcat-jetty-undertow/DispatchBoundaryDemo.java 把不可阻塞的 I/O 任务派发到 worker 队列:
javac --release 17 -Xlint:all -Werror examples/backend-development/servlet/tomcat-jetty-undertow/ContainerCapacityModel.java examples/backend-development/servlet/tomcat-jetty-undertow/DispatchBoundaryDemo.java
java -cp examples/backend-development/servlet/tomcat-jetty-undertow DispatchBoundaryDemoioThreadBlocked=false dispatched=blocking-servlet workerQueue=0这是线程边界模型,不声称实现 Undertow dispatch。真实验证应在线程名、exchange 状态、worker queue 与受控阻塞实验中证明 I/O 线程仍能服务其他连接。
maxThreads 不是独立容量旋钮
ContainerCapacityModel.java 计算内部租赁与阻塞请求占用后的剩余线程:
java -cp examples/backend-development/servlet/tomcat-jetty-undertow ContainerCapacityModelmaxThreads=200 leasedInternal=12 blockedRequests=150 remaining=38 saturated=false稳定关系是 可用于新任务 = 总容量 - 内部保留/租赁 - 已占用。实际容器分类不同,但任何线程池都不能把名义上限直接当业务并发。还要扣除长轮询、异步回调、管理任务和响应写出,结合数据库连接、CPU 与内存决定入口限制。
平台线程常有显著 native stack 成本;虚拟线程降低线程驻留成本,却允许更快地把压力送到数据库和远程服务。容器若启用虚拟线程,仍应对每路依赖设置 semaphore/bulkhead、连接池、deadline 与拒绝,观察 pinned、carrier 利用和任务年龄。
容器选型应比较责任,而不是跑一次 hello world
需要标准 WAR 生态、成熟运维与广泛框架兼容时,Tomcat 通常路径直接;需要以库方式组合多协议、精细 Handler 与执行策略时,Jetty 提供更显式组件模型;需要轻量 Handler 链和明确 I/O/worker 分层时,Undertow 有相应优势。最终选择还受框架官方支持、补丁维护、协议、JSP、WebSocket、管理、团队经验和漏洞响应约束。
基准测试必须使用同一 JDK、堆、GC、TLS、协议、响应大小、依赖延迟和容器 warmup,报告 CPU、内存、吞吐、p95/p99、拒绝、队列年龄与连接数。纯内存 hello world 主要测框架固定开销,无法推导真实阻塞业务的容量。
迁移验证要覆盖路径/参数编码、错误派发、异步 timeout、multipart 临时文件、Session 序列化、WebSocket、HTTP/2、优雅停机和代理 Header。容器默认值不同可能改变合法 Header 大小、URI 字符、表单参数上限和 SameSite 行为;这些都可能变成兼容事故。
统一观测模型消除产品标签
无论实现,都应暴露:连接与协议分布、accept/listen overflow、I/O 线程繁忙、executor active/queue/reject、请求 active/age、async active/age、响应已提交后失败、Session 与临时文件、部署状态。容器专有指标映射到这套语义后,才能跨实现比较。
故障注入至少包括慢依赖占满 worker、慢客户端写、上传中断、异步漏 complete、连接突发与优雅下线。验收目标是关键任务仍获得执行机会、队列有界或可观测、超时后资源收敛、旧部署可卸载。容器差异最终必须回到这些可证明结果,而不是“异步”“轻量”或“企业级”的宣传词。
配置翻译必须基于语义
迁移容器时不能把 maxThreads=200 原样翻译成同名参数。Tomcat executor 主要承担请求处理,Jetty pool 还可能租赁内部线程,Undertow worker 同时包含 I/O 与 blocking task 概念。先定义最大活动业务请求、内部关键任务余量、队列/拒绝行为和每线程成本,再映射到实现配置。
连接上限、请求上限和协议流上限也不可互换。HTTP/2 一条连接可复用多流,限制 connection 不等于限制并发 Servlet;异步请求释放 worker 后仍占请求与连接状态。迁移基准必须以逻辑请求阶段计量,避免表面线程数下降、实际在途状态暴涨。
安全默认值要语义对齐:请求目标字符、Header/参数数量、multipart 阈值、错误页、目录列表、代理地址恢复和 TLS Connector。新容器更严格导致拒绝时,应先定位非规范客户端并做最小例外,而不是直接放宽全部解析器。
