3.1 Connector如何接住第一个字节


文档摘要

3.1 Connector 如何接住第一个字节 本节摘要:连接器是 Tomcat 的城门与海关。本节解剖它的内部构造——Acceptor 接受连接、Poller 轮询读写事件、工作线程池执行任务、协议处理器解析报文、适配器完成翻译交接,并用一次线程名观察实验把这套分工"看见"。学完你能解释一个请求从 TCP 握手到进入容器之间的每一步,以及每一步对应哪个可调参数。 旅程正式上路。1.1 的旅程图里"进城"一段,在这里放大成完整的内部构造;本节认识的所有部件名,3.2 与 3.3 会反复使用,也是第 7 章性能排障时的"嫌疑人名单"。 城门与海关:连接器的两副面孔 老手嘴里常说的"城门口那点事",指的是连接器的两副面孔。对外它是城门:监听端口、接受连接、收发字节,纯粹的網絡活。

3.1 Connector 如何接住第一个字节

本节摘要:连接器是 Tomcat 的城门与海关。本节解剖它的内部构造——Acceptor 接受连接、Poller 轮询读写事件、工作线程池执行任务、协议处理器解析报文、适配器完成翻译交接,并用一次线程名观察实验把这套分工"看见"。学完你能解释一个请求从 TCP 握手到进入容器之间的每一步,以及每一步对应哪个可调参数。

旅程正式上路。1.1 的旅程图里"进城"一段,在这里放大成完整的内部构造;本节认识的所有部件名,3.2 与 3.3 会反复使用,也是第 7 章性能排障时的"嫌疑人名单"。

城门与海关:连接器的两副面孔

老手嘴里常说的"城门口那点事",指的是连接器的两副面孔。对外它是城门:监听端口、接受连接、收发字节,纯粹的網絡活。对内它是海关:把 HTTP 报文这种"外文"翻译成容器能懂的内部队列,把网络世界的套接字包装成 Java 世界请求对象。两副面孔对应两套技能,Tomcat 把它们分别交给了不同的内部角色。

先看全貌,再逐个上岗。

图 3-1:连接器内部构造标注图

图 3-1:连接器内部构造标注图

四个岗位的交接班

Acceptor 是唯一直接碰监听套接字的岗位。它在端口上调用 accept,每接到一条 TCP 连接,就把连接包装登记给 Poller,自己立刻回去等下一位。操作系统在它身后维护着等待队列,队列长度就是 acceptCount——城门再宽,排队的地方也有限,队伍塞满后新来的客户端会收到连接拒绝,这是"服务没挂但用户连不上"的第一嫌疑人。

Poller 是 NIO 时代的主角。它拿着一份连接清单反复问内核"谁的数据到了",只有就绪的连接才会被派给工作线程。有了它,线程不再干"守着连接等数据"的傻事——这正是 3.2 要展开的 IO 模型分水岭,此处记住分工即可:Acceptor 管进门,Poller 管叫号

工作线程池承接叫到号的连接,把后续所有活干完:协议处理器解析报文、适配器翻译交接、容器处理、响应写回。maxThreads 是这个池的天花板,池满时任务在队列里等,表现为请求时延上升而非报错——瓶颈定位时,"变慢"与"被拒"是两种完全不同的线索,前者查线程池,后者查等待队列。

协议处理器与适配器是翻译岗。Http11Processor 按语法把字节切成请求行、头部、体;CoyoteAdapter 把内部对象适配成 Servlet 规范的请求与响应,并顺手把主机名、路径等映射信息补齐,然后把请求"点燃"给 Engine——从这一刻起,请求离开连接器,进入第 4 章的指路系统。

用线程名把分工"看见"

这套分工不是纸上谈兵,线程名字就是证据。启动一个默认配置的 Tomcat,对进程做一次线程转储。

# 找到 Tomcat 进程号 并打印线程概览 pid=$(ps -ef | grep catalina | grep -v grep | awk '{print $2}') jstack $pid | grep -E 'catalina-exec|Poller|Acceptor' | head -n 12
"catalina-exec-1" #14 daemon prio=5 ... waiting on condition "catalina-exec-2" #15 daemon prio=5 ... waiting on condition "http-nio-8080-Poller" #18 daemon prio=5 ... RUNNABLE "http-nio-8080-Acceptor" #19 daemon prio=5 ... RUNNABLE "http-nio-8080-ClientPoller" #20 daemon prio=5 RUNNABLE

三个细节值得驻足。第一,线程名自带结构信息:http-nio-8080 前缀同时暴露了协议处理器的 IO 模型与端口——看到 bio 字样就知道这套配置活在旧世界。第二,exec 是工作线程的编号,数量上限就是 maxThreads;压测时反复抓几次转储,若池里全部线程都 RUNNABLE 且持续增长到 200(默认值),瓶颈大概率在应用处理而非连接器。第三,Poller 与 Acceptor 常年只有一两个线程,它们是"叫号员"不是"办事员",不参与业务处理,不要按工作线程的思路去调它们。

对应到配置,一份带注释的连接器定义把岗位与参数对上号。

<!-- server.xml:岗位与参数的对应关系 --> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" <!-- 空闲连接等待数据的上限 毫秒 --> maxThreads="200" <!-- 工作线程池天花板 --> minSpareThreads="10" <!-- 池里常备的空闲线程 --> acceptCount="100" <!-- 城门外的等待队列长度 --> maxConnections="8192" <!-- 同一时刻接纳的连接总数 --> redirectPort="8443"/> <!-- 收到需加密请求时转投的端口 -->

⚠️ 常见坑:把 maxThreads 调到几千去"提升并发"。工作线程是真线程,每个都要占栈内存,几千个线程先压垮 JVM 再谈服务。NIO 模式下连接容量由 maxConnections 扛,maxThreads 只需匹配业务处理时长的倒数——这在 3.2 用一道算术讲清。

本节要点回顾

  • 两副面孔:对外收发字节的城门(Acceptor、Poller),对内翻译交接的海关(Processor、Adapter)。
  • 叫号与办事分离:Poller 只报告"数据到了",工作线程才真正处理,这是 NIO 高并发的根基。
  • 参数认岗:acceptCount 管门外排队,maxConnections 管门内容量,maxThreads 管办事窗口数。
  • 变慢与被拒是两回事:变慢查线程池与下游,被拒查等待队列与 maxConnections。
  • 线程名是免费仪表:jstack 里的前缀同时暴露 IO 模型、端口与岗位,排障先看它。

岗位认齐了,下一节回答那个分水岭问题:BIO 与 NIO 到底差在哪,三道闸门参数怎么配才不自相矛盾。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U