从 Server.xml 到字节码:Tomcat 内核全景与请求旅程 10 000 字深剖
(零)为什么要重读 Tomcat
· 面试最爱问:HTTP 请求在 Tomcat 里到底拐了多少弯?
· 生产最怕出:CPU 100 %、线程池打满、类加载泄漏、Session 爆炸。
· 云原生最尴尬:镜像个头 200 MB,启动 30 s,HPA 还来不及扩容。
本文不贴源码,用伪代码、示意图与运维日志,带你走完一次请求的“一生”。
(一)俯瞰:Tomcat 的 4 大版图
Server ‑> Service ‑> Engine ‑> Host ‑> Context ‑> Wrapper
一条 Server.xml 就是一颗树,节点各司其职,请勿随意增删。Connector:把“字节流”封装成“Request/Response 对象”。
BIO/NIO/NIO2/APR 四种实现,分别对应 Java 同步、Java NIO、Java AIO、OpenSSL。Container:Pipeline + Valve 责任链,像极了 Servlet Filter。
顶层组件:Jasper(JSP 编译器)、Catalina(Servlet 容器)、Coyote(HTTP 协议处理器)。
(二)一次 HTTP 请求的 12 站地
Acceptor 线程:监听 8080,accept() 得到 SocketChannel。
Poller 线程:NIO 模式下,把 Channel 注册到 Selector,等待 READ 事件。
Processor:解析 HTTP 报文,生成 Request Coyote → Request Catalina → HttpServletRequest。
Mapper:根据 URI 映射到 Host、Context、Wrapper,匹配原则“最长前缀”。
Pipeline.invoke():StandardEngineValve → StandardHostValve → StandardContextValve → StandardWrapperValve。
FilterChain:执行 web.xml 中配置的 Filters(顺序 = 声明顺序)。
Servlet.service():业务代码开始跑。
JSP 编译:如果请求 *.jsp,Jasper 把 JSP 转译成 .java,再编译成 .class,默认放在 $CATALINA_BASE/work。
响应阶段:WrapperValve → FilterChain → Connector → Poller → Socket 写回。
AsyncContext:若业务开启异步,Processor 归还线程给 Poller,等待业务线程回调。
keep-alive:Tomcat 10 默认 maxKeepAliveRequests=100,超出即关闭连接,防止“空轮询”。
日志:AccessLogValve 按 %h %l %u %t "%r" %s %b 格式刷盘。
(三)线程模型:Acceptor / Poller / Worker
· Acceptor:数量由 acceptorThreadCount 决定,默认 1。
· Poller:selectorPool.size = min(2, CPU 核数)。
· Worker:Executor 线程池,maxThreads 200 是“总阀门”,minSpareThreads 25 保证低峰期不反复创建。
CPU 100 % 排查模板:
top -H 找 nid 十六进制 → jstack 转线程名 → 定位哪个 Servlet 卡在 SQL。
jstat ‑gc 观察是否频繁 Full GC,导致线程长时间阻塞。
(四)类加载:打破双亲委派的三次场景
WebappClassLoader:每个 Context 一个实例,先自己加载 /WEB-INF/classes 与 /WEB-INF/lib,再向上委派。
Common ClassLoader:$CATALINA_BASE/lib 下的 JAR,被所有 Webapp 共享。
Shared ClassLoader:可选配置,解决多 Webapp 共用大体积 SDK(如 Hadoop)。
内存泄漏根因:ThreadLocal 引用 WebappClassLoader → 线程未销毁 → 元空间 OOM。
解决方案:
在 Context 的 stop() 中执行 ThreadLocal.remove()。
使用 Guava 的 FinalizableReferenceQueue 清理。
(五)Session 管理:内存、Redis、JDBC、JWT
· StandardManager:内存 Map,重启即失效,仅适合单体。
· PersistentManager + FileStore:序列化到 SESSIONS.ser,优雅停机时 dump。
· RedisSessionManager:通过 Valve 拦截 request,把 Session 序列化为 JSON,存在 Redis TTL=1800 s。
坑:Tomcat 10 以后包名从 org.apache.catalina.session.RedisSessionManager 换成非官方维护,注意兼容性。
· JWT:无状态会话,Tomcat 侧只负责 Filter 解析,不存储。
(六)JSP & Jasper:编译、热替换、预编译
· JspC:Maven 插件把 JSP 预编译为 Servlet,启动提速 50 %。
· development=true 时,Jasper 监听 .jsp 文件变更,重新生成源码。
· mappedfile=false 可关闭静态文本合并,调试时行号对应准确。
(七)HTTPS & APR:OpenSSL 的零拷贝
· NIO + JSSE:Java 层握手,堆内内存拷贝两次。
· APR + OpenSSL:Tomcat Native 库,使用 DirectBuffer,CPU 降低 30 %。
· HTTP/2:通过 upgrade 或 h2c direct,依赖 ALPN 与 openssl 1.0.2+。
(八)监控与诊断
JMX:Catalina MBean 暴露 Connector、ThreadPool、Session 指标。
psi-probe:老牌 Web UI,支持在线查看线程栈、数据源连接。
AccessLogValve:%{X-Forwarded-For}i 记录真实 IP。
GC Log:-Xlog:gc*:file=/tmp/gc.log:time,uptime,pid 观察 GC 与线程阻塞关联。
(九)云原生改造 6 步曲
镜像:alpine-jre + tomcat:10-jre17,剔除 examples、docs,瘦身至 80 MB。
启动脚本:catalina.sh 里 JAVA_OPTS 外置到 ConfigMap,热更新无需重建镜像。
优雅停机:
server.xml 开启 <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener"/>
k8s preStop exec sleep 15 让正在处理的请求完成。
探针:
livenessProbe 200 /healthz
readinessProbe 200 /ready
水平扩缩:
HPA 用 CPU 50 % + Tomcat 线程池利用率自定义指标。
Sidecar:
Envoy 做 mTLS、熔断、灰度。
(十)彩蛋:Tomcat 的“隐藏功能”
· JreMemoryLeakPreventionListener:GC 触发后显式调用 sun.misc.GC.requestLatency(),减少 RMI 带来的 full GC。
· ParallelWebappClassLoader:JDK9+ 模块化后,可并行加载类,启动提速 15 %。
· StuckThreadDetectionValve:监控超过 threshold 秒的线程,发邮件报警。