ARTICLE · 1111667
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 | |
org.apache.coyote.http11.Http11InputBuffer | |
org.apache.coyote.http11.Http11Processor | |
org.apache.coyote.AbstractProtocol.ConnectionHandler | |
org.apache.coyote.AbstractProcessorLight | |
org.apache.tomcat.util.buf.MessageBytes | |
org.apache.catalina.LifecycleState | |
org.apache.catalina.Valveorg.apache.catalina.core.StandardPipeline | |
org.apache.catalina.connector.CoyoteAdapter | |
org.apache.catalina.core.StandardHostValve |
附录 B:全系列引用的行号清单
以下行号对应 apache-tomcat-9.0.122-src,便于逐条打开核对。这张表覆盖全系列 6 篇里所有引用过的位置,可以当成一份独立的速查表使用——不需要读任何一篇正文也能按图索骥。
util/net/NioEndpoint.java | eventCache | |
util/net/NioEndpoint.java | setPollerThreadCountgetPollerThreadCount 恒返回 1 | |
util/net/NioEndpoint.java | selectorTimeout | |
util/net/NioEndpoint.java | new Poller() | |
util/net/NioEndpoint.java | addEvent() | |
util/net/NioEndpoint.java | events() | |
util/net/NioEndpoint.java | Poller.run() | |
util/net/NioEndpoint.java | getAndSet(-1)selectNow / select | |
util/net/NioEndpoint.java | processKey() | |
util/net/NioEndpoint.java | unreg() | |
util/net/NioEndpoint.java | reg() | |
util/net/NioEndpoint.java | timeout()nextExpiration 节流 | |
util/net/NioEndpoint.java | cancelledKey() | |
util/net/Acceptor.java | ||
util/net/AbstractEndpoint.java | SocketState | |
util/net/SocketEvent.java | SocketEvent | |
util/net/SocketWrapperBase.java | OperationStateCompletionCheck | |
util/threads/LimitLatch.java | ||
util/threads/TaskQueue.java | offer() 迫使线程池扩容 | |
util/collections/SynchronizedStack.java | ||
coyote/AbstractProcessorLight.java | ||
coyote/AbstractProcessorLight.java | checkForPipelinedData() | |
coyote/AbstractProtocol.java | ConnectionHandler.process() | |
coyote/AbstractProtocol.java | recycledProcessors | |
coyote/AbstractProtocol.java | SocketState 分派后续动作 | |
coyote/http11/Http11Processor.java | service() | |
coyote/http11/Http11Processor.java | UPGRADING 返回 | |
coyote/http11/Http11InputBuffer.java | parsingRequestLinePhase | |
coyote/http11/Http11InputBuffer.java | parseRequestLine() | |
coyote/http11/Http11InputBuffer.java | setMethod(bytes, start, len) | |
coyote/http11/Http11InputBuffer.java | requestURI().setBytes(...) | |
util/buf/MessageBytes.java | ||
util/buf/MessageBytes.java | toString() | |
catalina/LifecycleState.java | ||
catalina/Valve.java | invoke(Request, Response) | |
catalina/connector/CoyoteAdapter.java | ||
catalina/core/StandardHostValve.java | context.bind(...) |
结语
六篇走下来,如果只留一句话,我想留这句:
好的服务端代码,大部分篇幅在描述"什么情况下什么都不用做"。
回头看看那些让人印象深刻的片段——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 往下读,通常比从入口一路跟要快得多。