夜雨聆风学习资料网

ARTICLE · 1111667

Tomcat 9 源码剖析(六):权衡和取舍

Tomcat 9 源码剖析(六):权衡和取舍

1. 很难做到十全十美

读优秀开源项目有一个常见陷阱:读着读着就跪下了,觉得每个设计都是对的。

Tomcat 的设计确实经得起推敲,但它不是没有代价。单 Poller 换来了稳定,却埋下了扩展性上限;对象池换来了低 GC,却引入了状态残留的风险;延迟转换换来了性能,却对调用方式有隐含要求。这些权衡和取舍在 Tomcat 的语境下是合理的,但如果不管场景照搬,就会变成问题。

1. NioEndpoint 单 Poller 是潜在瓶颈。 Tomcat 9.0.122 的 NIO 连接器只有一个 Poller 线程——setPollerThreadCount() 是空方法,getPollerThreadCount() 恒返回 1,startInternal() 里也只 new 了一个 Poller。这一个线程同时负责所有连接的 select() 和事件分发。在超多核机器 + 极高连接数场景下,它可能成为瓶颈。Tomcat 选择了单 Poller(稳定性优先),如果你确实撞上这个瓶颈,方向是横向扩展实例而不是调 pollerThreadCount(在这个版本上它已经无效)。

2. 对象池带来状态残留风险。 Tomcat 有三处对象池——PollerEvent(每轮循环都要创建的事件对象)、Processor(每个连接要用、内含各种缓冲区的重量级对象)、以及承载它们的 SynchronizedStack。它们都有一个隐含前提:reset() 被正确调用。一旦漏掉某个字段的重置,就会出现"上一个请求的数据泄漏到下一个请求"的问题——这类 bug 偶发、跟并发时序相关,极难排查。自己用对象池时,务必给 reset() 写单元测试,并在 CI 里跑。

3. MessageBytes 的延迟转换是单向的。 它用一个 type 字段记录头部字段当前以什么形态存在(T_NULL / T_STR / T_BYTES / T_CHARS),只在真正需要 String 时才解码——换来的是"用不到的头部一次都不解码"。但转换不可逆:一旦变成 T_STR 就不会自己变回 T_BYTES。所以如果代码里一会儿用字节、一会儿用字符串,反而会比直接解码更慢。这个优化的收益依赖于"使用模式符合设计预期"。

4. 大量历史兼容代码增加了阅读成本。 例如 NioEndpoint.cancelledKey() 里要按 JDK 版本分两条路径处理(JDK 11 之前必须先 cancel() 再 close(),否则 select() 与 close() 会互相死锁)、getPollerThreadCount() 这类为兼容而保留的空实现、AbstractProcessorLight 里针对 Servlet 3.0 异步与协议升级的各种特判。理解这些需要了解 Tomcat 的版本演进史。

5. 状态不落 static。 Tomcat 里每个 StandardContext、StandardWrapper 都持有自己的状态,没有全局静态可变状态。这是它支持多应用并存、热部署、独立类加载器的前提。反过来说,如果要自己写一个"只能跑一个应用"的服务器,不需要照抄这个设计,可以简单得多。


附录 A:关键类索引

想了解什么
看哪个类
连接是怎么被接受的
org.apache.tomcat.util.net.Acceptor
就绪事件是怎么分发的
org.apache.tomcat.util.net.NioEndpoint.Poller
连接的读写状态机
org.apache.tomcat.util.net.SocketWrapperBase
连接数上限
org.apache.tomcat.util.threads.LimitLatch
工作线程池的队列
org.apache.tomcat.util.threads.TaskQueue
对象池
org.apache.tomcat.util.collections.SynchronizedStack
HTTP 请求行/头解析
org.apache.coyote.http11.Http11InputBuffer
HTTP 请求处理主流程
org.apache.coyote.http11.Http11Processor
连接状态的分派中心
org.apache.coyote.AbstractProtocol.ConnectionHandler
异步与升级状态的循环
org.apache.coyote.AbstractProcessorLight
头部字段的延迟转换
org.apache.tomcat.util.buf.MessageBytes
组件生命周期
org.apache.catalina.LifecycleState
责任链(Pipeline/Valve)
org.apache.catalina.Valve
、org.apache.catalina.core.StandardPipeline
coyote → catalina 的桥
org.apache.catalina.connector.CoyoteAdapter
类加载器绑定时机
org.apache.catalina.core.StandardHostValve

附录 B:全系列引用的行号清单

以下行号对应 apache-tomcat-9.0.122-src,便于逐条打开核对。这张表覆盖全系列 6 篇里所有引用过的位置,可以当成一份独立的速查表使用——不需要读任何一篇正文也能按图索骥。

文件
行号
内容
util/net/NioEndpoint.java
108
eventCache
 字段声明
util/net/NioEndpoint.java
310-332
setPollerThreadCount
 空实现 / getPollerThreadCount 恒返回 1
util/net/NioEndpoint.java
337
selectorTimeout
 默认 1000
util/net/NioEndpoint.java
570
new Poller()
 —— 只创建一个
util/net/NioEndpoint.java
978-990
addEvent()
 的 wakeupCounter 判断
util/net/NioEndpoint.java
1019-1070
events()
 处理队列与对象池回收
util/net/NioEndpoint.java
1116-1174
Poller.run()
 主循环
util/net/NioEndpoint.java
1125-1132
getAndSet(-1)
 决定 selectNow / select
util/net/NioEndpoint.java
1182-1237
processKey()
util/net/NioEndpoint.java
1367-1370
unreg()
 —— 派发前摘除 readyOps
util/net/NioEndpoint.java
1379
reg()
util/net/NioEndpoint.java
1390-1401
timeout()
 的 nextExpiration 节流
util/net/NioEndpoint.java
1083-1108
cancelledKey()
 的 JDK 版本差异
util/net/Acceptor.java
104-125
分级 sleep
util/net/AbstractEndpoint.java
96-135
SocketState
 9 个取值
util/net/SocketEvent.java
22
SocketEvent
 枚举
util/net/SocketWrapperBase.java
1223-1330
OperationState
 抽象与 CompletionCheck
util/threads/LimitLatch.java
31-62
AQS 实现的连接闸门
util/threads/TaskQueue.java
98-117
重写 offer() 迫使线程池扩容
util/collections/SynchronizedStack.java
40-80
有上限的对象栈
coyote/AbstractProcessorLight.java
48-106
异步/升级的 dispatch 循环
coyote/AbstractProcessorLight.java
119-131
checkForPipelinedData()
coyote/AbstractProtocol.java
1250-1287
ConnectionHandler.process()
 入口与超时 NO-OP 判定
coyote/AbstractProtocol.java
1325-1337
recycledProcessors
 对象池
coyote/AbstractProtocol.java
1405-1452
按 SocketState 分派后续动作
coyote/http11/Http11Processor.java
261-313
service()
 的 keep-alive 主循环
coyote/http11/Http11Processor.java
275-283
循环条件与 UPGRADING 返回
coyote/http11/Http11InputBuffer.java
130
parsingRequestLinePhase
 字段
coyote/http11/Http11InputBuffer.java
344-579
parseRequestLine()
 分阶段推进
coyote/http11/Http11InputBuffer.java
405
setMethod(bytes, start, len)
 字节切片
coyote/http11/Http11InputBuffer.java
499-502
requestURI().setBytes(...)
util/buf/MessageBytes.java
45-62
四态常量
util/buf/MessageBytes.java
190-205
toString()
 按 type 分派
catalina/LifecycleState.java
全文件
11 个状态 + FAILED
catalina/Valve.java
109
invoke(Request, Response)
catalina/connector/CoyoteAdapter.java
350
进入容器的第一行
catalina/core/StandardHostValve.java
113、129
context.bind(...)
 与下钻 Pipeline

结语

六篇走下来,如果只留一句话,我想留这句:

好的服务端代码,大部分篇幅在描述"什么情况下什么都不用做"。

回头看看那些让人印象深刻的片段——timeout() 里那个提前 return、AbstractProcessorLight 里被刻意忽略的 OPEN_WRITE、超时事件里的 NO-OP 判定——它们的共同点是都在主动放弃工作。这跟"优化"的直觉相反:性能不是靠把每件事做得更快,而是靠让大部分事根本不发生。

另一个感受是,Tomcat 的代码里"防御"的味道很重。unreg() 那条注释、cancelledKey() 按 JDK 版本分两条路、checkForPipelinedData() 里那句"否则数据会被删除",防的都是概率很低、但一旦发生就很难查的问题。这类代码在 code review 时最容易被当成"冗余"删掉——而删掉它的人,通常要在半年后才知道代价。

最后需要说明的是,这套文档只覆盖了 Tomcat 的一小部分。为了不让篇幅铺得太开,有几块内容基本没展开:Servlet 规范的实现细节(Session 管理、Filter 链、请求分派)、JSP 编译(Jasper)、类加载器体系与热部署、HTTP/2 与 WebSocket 的协议实现、安全域与认证授权。如果读到某处觉得"怎么到这里就停了",多半就是这些地方之一。

找它们的方法在附录 A 里——先定位类,再顺着 Pipeline 往下读,通常比从入口一路跟要快得多。

相关学习资料