该选 Tomcat 还是 Jetty?Undertow 还能用吗?两个都在 classpath 上谁赢?虚拟线程在两个容器上效果一样吗?同样的畸形请求两个容器怎么回应?这篇用同一份代码只换 classpath 上的容器,逐项实测。
Tomcat:classpath 共 39 个 jar,合计 18.7 MB;名字里带 tomcat / jetty 的 6 个:
spring-boot-starter-tomcat-4.0.8
spring-boot-starter-tomcat-runtime-4.0.8
spring-boot-tomcat-4.0.8
tomcat-embed-core-11.0.24
tomcat-embed-el-11.0.24
tomcat-embed-websocket-11.0.24
Jetty:classpath 共 77 个 jar,合计 19.9 MB;名字里带 tomcat / jetty 的 33 个:
jetty-alpn-client-12.1.12
jetty-annotations-12.1.12
jetty-client-12.1.12
jetty-compression-common-12.1.12
jetty-compression-gzip-12.1.12
jetty-compression-server-12.1.12
jetty-ee-webapp-12.1.12
jetty-ee11-annotations-12.1.12
jetty-ee11-plus-12.1.12
jetty-ee11-servlet-12.1.12
jetty-ee11-webapp-12.1.12
jetty-ee11-websocket-jakarta-client-12.1.12
jetty-ee11-websocket-jakarta-common-12.1.12
jetty-ee11-websocket-jakarta-server-12.1.12
jetty-ee11-websocket-jetty-server-12.1.12
jetty-ee11-websocket-servlet-12.1.12
jetty-http-12.1.12
jetty-io-12.1.12
jetty-plus-12.1.12
jetty-security-12.1.12
jetty-server-12.1.12
jetty-session-12.1.12
jetty-util-12.1.12
jetty-websocket-core-client-12.1.12
jetty-websocket-core-common-12.1.12
jetty-websocket-core-server-12.1.12
jetty-websocket-jetty-api-12.1.12
jetty-websocket-jetty-common-12.1.12
jetty-websocket-jetty-server-12.1.12
jetty-xml-12.1.12
spring-boot-jetty-4.0.8
spring-boot-starter-jetty-4.0.8
spring-boot-starter-jetty-runtime-4.0.8
Tomcat → factory Bean=[tomcatServletWebServerFactory, tomcatServletWebServerFactoryCustomizer];本次请求的处理线程=http-nio-9601-exec-1;classpath 上能加载到的容器类:Tomcat 11.0.24.0
Jetty → factory Bean=[jettyServletWebServerFactory];本次请求的处理线程=qtp1019009267-44;classpath 上能加载到的容器类:Jetty 12.1.12
Tomcat+Jetty 都在 classpath → factory Bean=[jettyServletWebServerFactory, tomcatServletWebServerFactoryCustomizer];本次请求的处理线程=qtp1385140107-45;classpath 上能加载到的容器类:Tomcat 11.0.24.0
Maven Central 上 spring-boot-starter-undertow 的最新版本:4.0.0-M1(查询时 4.0.8 的这个 pom 返回 404);spring-boot-starter-jetty 最新:4.2.0-M2Tomcat 方案 39 个 jar,Jetty 方案 77 个 jar;两个 starter 同时在时跑的是 Jetty;Undertow 在 Boot 4.0.8 里已经没有 starter。
Tomcat:端口可连接 中位 805 ms(范围 767~859);第 1 次请求 35.9 ms,第 2 次 1.4 ms;常驻内存 启动后 163 MB,请求 2 次后 169 MB Jetty:端口可连接 中位 860 ms(范围 840~900);第 1 次请求 47.0 ms,第 2 次 1.4 ms;常驻内存 启动后 170 MB,请求 2 次后 174 MB
两者相差几十毫秒、几个 MB,范围略有重叠。
-- Tomcat 平台线程:处理线程 → name=[http-nio-9618-exec-2] virtual=false
/fast,并发 64:两轮吞吐 65978 / 66323 请求/秒;p50 0.9 / 0.9 ms;p99 2.2 / 2.2 ms;错误 0 / 0
/fast,并发 256:两轮吞吐 70311 / 71894 请求/秒;p50 3.4 / 3.3 ms;p99 7.9 / 7.9 ms;错误 0 / 0
/slow50(sleep 50 ms),并发 400:两轮吞吐 3681 / 3689 请求/秒;p50 110.4 / 110.3 ms;p99 120.9 / 119.1 ms;错误 0 / 0
-- Jetty 平台线程:处理线程 → name=[qtp1477520324-44] virtual=false
/fast,并发 64:两轮吞吐 66206 / 63813 请求/秒;p50 0.9 / 0.9 ms;p99 2.1 / 2.2 ms;错误 0 / 0
/fast,并发 256:两轮吞吐 68239 / 70616 请求/秒;p50 3.5 / 3.4 ms;p99 8.1 / 8.0 ms;错误 0 / 0
/slow50(sleep 50 ms),并发 400:两轮吞吐 3568 / 3544 请求/秒;p50 58.3 / 58.6 ms;p99 375.2 / 369.3 ms;错误 0 / 0
-- Tomcat + spring.threads.virtual.enabled=true:处理线程 → name=[tomcat-handler-1] virtual=true
/fast,并发 64:两轮吞吐 69260 / 66429 请求/秒;p50 0.9 / 0.9 ms;p99 1.9 / 2.2 ms;错误 0 / 0
/fast,并发 256:两轮吞吐 70228 / 70483 请求/秒;p50 3.4 / 3.4 ms;p99 8.2 / 7.8 ms;错误 0 / 0
/slow50(sleep 50 ms),并发 400:两轮吞吐 6467 / 6478 请求/秒;p50 62.6 / 62.9 ms;p99 79.7 / 71.2 ms;错误 0 / 0
-- Jetty + spring.threads.virtual.enabled=true:处理线程 → name=[jetty-2] virtual=true
/fast,并发 64:两轮吞吐 70376 / 69378 请求/秒;p50 0.9 / 0.9 ms;p99 2.0 / 2.0 ms;错误 0 / 0
/fast,并发 256:两轮吞吐 77604 / 76076 请求/秒;p50 3.1 / 3.2 ms;p99 7.2 / 7.3 ms;错误 0 / 0
/slow50(sleep 50 ms),并发 400:两轮吞吐 3519 / 3579 请求/秒;p50 116.2 / 113.4 ms;p99 174.1 / 172.0 ms;错误 0 / 0
-- Jetty 平台线程 + server.jetty.threads.max=1000:处理线程 → name=[qtp1227903899-44] virtual=false
/fast,并发 64:两轮吞吐 62174 / 64547 请求/秒;p50 1.0 / 0.9 ms;p99 2.3 / 2.2 ms;错误 0 / 0
/fast,并发 256:两轮吞吐 67864 / 66823 请求/秒;p50 3.5 / 3.6 ms;p99 8.0 / 7.9 ms;错误 0 / 0
/slow50(sleep 50 ms),并发 400:两轮吞吐 7309 / 7326 请求/秒;p50 55.0 / 54.6 ms;p99 61.6 / 61.1 ms;错误 0 / 0
-- Jetty + 虚拟线程 + server.jetty.threads.max=1000:处理线程 → name=[jetty-2] virtual=true
/fast,并发 64:两轮吞吐 70437 / 70505 请求/秒;p50 0.9 / 0.9 ms;p99 1.9 / 2.0 ms;错误 0 / 0
/fast,并发 256:两轮吞吐 77987 / 78802 请求/秒;p50 3.1 / 3.1 ms;p99 7.1 / 6.7 ms;错误 0 / 0
/slow50(sleep 50 ms),并发 400:两轮吞吐 6600 / 6502 请求/秒;p50 60.8 / 62.1 ms;p99 72.8 / 73.6 ms;错误 0 / 0
-- Tomcat 平台线程 + server.tomcat.threads.max=1000:处理线程 → name=[http-nio-9624-exec-2] virtual=false
/fast,并发 64:两轮吞吐 65479 / 67622 请求/秒;p50 0.9 / 0.9 ms;p99 2.1 / 2.1 ms;错误 0 / 0
/fast,并发 256:两轮吞吐 68285 / 68943 请求/秒;p50 3.5 / 3.4 ms;p99 8.3 / 8.4 ms;错误 0 / 0
/slow50(sleep 50 ms),并发 400:两轮吞吐 7295 / 7308 请求/秒;p50 54.8 / 54.8 ms;p99 63.5 / 63.4 ms;错误 0 / 0快接口两者同一量级;阻塞接口取决于线程池上限,Jetty 开虚拟线程之后还要调大 server.jetty.threads.max。
-- 请求头 X-Big 的长度(默认的请求头上限):
X-Big 4000 字节:Tomcat → 200 ;Jetty → 200 OK
X-Big 7000 字节:Tomcat → 200 ;Jetty → 200 OK
X-Big 8000 字节:Tomcat → 200 ;Jetty → 200 OK
X-Big 9000 字节:Tomcat → 400 ;Jetty → 431 Request Header Fields Too Large
X-Big 70000 字节:Tomcat → 400 ;Jetty → 431 Request Header Fields Too Large
-- 路径写法(原样发送,不经过客户端规范化):
GET /echo/a/b
Tomcat:200 uri=/echo/a/b servletPath=/echo/a/b pathInfo=null
Jetty:200 OK uri=/echo/a/b servletPath=/echo/a/b pathInfo=null
GET /echo/a%2Fb
Tomcat:400 <!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style type="text/css">body {
Jetty:400 Bad Request <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 400 Ambigu
GET /echo/a//b
Tomcat:200 uri=/echo/a//b servletPath=/echo/a/b pathInfo=null
Jetty:400 Bad Request <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 400 Ambigu
GET /echo/a/./b
Tomcat:200 uri=/echo/a/./b servletPath=/echo/a/b pathInfo=null
Jetty:200 OK uri=/echo/a/./b servletPath=/echo/a/b pathInfo=null
GET /echo/a/../b
Tomcat:200 uri=/echo/a/../b servletPath=/echo/b pathInfo=null
Jetty:200 OK uri=/echo/a/../b servletPath=/echo/b pathInfo=null
GET /echo/%2e%2e/b
Tomcat:200 uri=/echo/%2e%2e/b servletPath=/b pathInfo=null
Jetty:400 Bad Request <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 400 Ambigu
GET /echo/a;x=1/b
Tomcat:200 uri=/echo/a;x=1/b servletPath=/echo/a/b pathInfo=null
Jetty:200 OK uri=/echo/a;x=1/b servletPath=/echo/a/b pathInfo=null
GET /echo/a%00b
Tomcat:400 <!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style type="text/css">body {
Jetty:400 Bad Request <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 400 Bad Re
GET /echo/a%5Cb
Tomcat:400 <!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style type="text/css">body {
Jetty:400 Bad Request <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 400 Suspic
GET /echo/a\b
Tomcat:400 <!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style type="text/css">body {
Jetty:400 Bad Request <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1"/> <title>Error 400 Suspic
-- 加上 server.max-http-request-header-size=16KB:
X-Big 9000 字节:Tomcat → 200 ;Jetty → 200 OK
X-Big 15000 字节:Tomcat → 200 ;Jetty → 200 OK
X-Big 20000 字节:Tomcat → 400 ;Jetty → 431 Request Header Fields Too Large
-- 连接空闲多久被服务端关掉(默认配置,发一个请求后什么都不发,最长等 120 秒):
Tomcat:60.1 秒后服务端关闭了连接
Jetty:30.0 秒后服务端关闭了连接超大请求头 400 与 431、// 与 %2e%2e 的路径、空闲连接超时 60 秒与 30 秒,两个容器都不一样。