3.2 IO模型演进:BIO、NIO与线程池


文档摘要

3.2 IO 模型演进:BIO、NIO 与线程池 本节摘要:三种 IO 模型的全部差异可以压缩成一句话——谁来等数据。BIO 派一个线程死等,NIO 由轮询器统一守望、数据到了才叫线程,NIO2 与 APR 是这一思想在不同底层的实现。本节用一张对比矩阵、一次线程数实测和一道容量算术,把 acceptCount、maxConnections、maxThreads 三道闸门的配合关系讲透,并给出连接器层面的调参方法论。 3.1 认清了连接器的岗位,本节讲这些岗位的"工作制度"——IO 模型决定了线程怎么花、连接怎么等。这里的结论直接服务第 7 章的调优实战,是全书参数讨论密度最高的一节。 分水岭:谁来等数据 网络 IO 里最冤的活是"等"。

3.2 IO 模型演进:BIO、NIO 与线程池

本节摘要:三种 IO 模型的全部差异可以压缩成一句话——谁来等数据。BIO 派一个线程死等,NIO 由轮询器统一守望、数据到了才叫线程,NIO2 与 APR 是这一思想在不同底层的实现。本节用一张对比矩阵、一次线程数实测和一道容量算术,把 acceptCount、maxConnections、maxThreads 三道闸门的配合关系讲透,并给出连接器层面的调参方法论。

3.1 认清了连接器的岗位,本节讲这些岗位的"工作制度"——IO 模型决定了线程怎么花、连接怎么等。这里的结论直接服务第 7 章的调优实战,是全书参数讨论密度最高的一节。

分水岭:谁来等数据

网络 IO 里最冤的活是"等"。客户端把请求头发到一半去喝口水,服务器这边读不到数据,总得有人守着。三种模型的差别就在守的方式。

BIO(阻塞 IO)的制度最笨:一条连接从进门到关闭,独占一个工作线程,线程在读不到数据时就阻塞干等。200 条慢连接就是 200 个线程在岗打盹——线程是 JVM 里最贵的资源之一,栈内存按 MB 计,上下文切换按微秒计,BIO 的并发天花板因此卡在几百连接的量级。Tomcat 7 及更早的默认就是它,那个年代"Tomcat 扛不住大并发"的口碑由此而来。

NIO(非阻塞 IO)换了制度:3.1 的 Poller 拿着 selector 统一守望成百上千条连接,谁的数据到了才把谁交给工作线程,线程只干"数据在手"的活。连接数与线程数解耦,一台普通服务器扛几万条空闲连接不再费力。Tomcat 8 起默认 NIO,老口碑从此翻案。NIO2(JDK 7 的异步 IO)与 APR(本地库实现)是同一思想的不同落地:前者把等待也交给操作系统回调,后者调用本地代码贴近内核,性能差距在日常负载下并不显著,选型看运维习惯而非 benchmark 数字。

图 3-2:三种工作制度对比矩阵

图 3-2:三种工作制度对比矩阵

实测:让数字说话

空口对比不过瘾,做一次小实验。先配一个 BIO 连接器(Tomcat 9 还保留着实现类),再压 300 条并发连接,观察线程行为。

<!-- server.xml:临时启用 BIO 以便对照(10.x 已移除请用 9 做实验) --> <Connector port="8081" protocol="org.apache.coyote.http11.Http11Protocol" maxThreads="600"/> <!-- 对照组:NIO --> <Connector port="8082" protocol="org.apache.coyote.http11.Http11NioProtocol" maxThreads="200" maxConnections="8192"/>
# 用连接保持工具制造 300 条空闲连接 分别打两个端口 # 观察两边工作线程数量差异 pid=$(ps -ef | grep catalina | grep -v grep | awk '{print $2}') jstack $pid | grep -c 'http-bio-8081-exec' jstack $pid | grep -c 'http-nio-8082-exec'
298 10

同样的 300 条连接,BIO 侧起了近 300 个 exec 线程(打盹也在岗),NIO 侧只用 10 个常备线程(minSpareThreads 的值)守望。这就是"谁来等数据"的代价差异,比任何文字描述都直观。实验后记得删掉 BIO 连接器。

三道闸门的配合算术

acceptCount、maxConnections、maxThreads 三道闸门从外到内串联,任何一道都可能成为瓶颈,配置它们要用同一套容量算术,而不是各抄各的"最佳实践"。

第一道:maxConnections 是城内容量,NIO 默认 8192,含义是同一时刻最多接纳这么多条连接,超出后新连接在操作系统队列里排队。第二道:acceptCount 是门外队列,默认 100,城内满了才用得上它。第三道:maxThreads 是办事窗口,默认 200,决定同时在处理的请求数。

一道算术把三者串起来:假设平均每个请求业务耗时 50 毫秒(含数据库与下游调用),单个线程每秒可完成 20 个请求,200 线程的理论吞吐是每秒 4000 请求。如果压测目标只有每秒 500,那 maxThreads 留 50 就够,多出来的线程只是占内存;反过来,若目标是每秒 8000,加线程之前先问下游数据库能不能扛——窗口开得再多,仓库发货跟不上,队伍只会排到城里。线程数的上限由下游最慢的一环决定,这是调参第一守则。

排队的代价也要算。城内满、门外排队的连接,客户端感受是连接建立慢;窗口满、任务排队的请求,感受是已连接但响应慢。前者调 maxConnections,后者调 maxThreads 或优化业务耗时——3.1 的"变慢与被拒"二分法在此落地成参数动作。

💡 关键直觉:三道闸门的默认值(8192、100、200)对多数应用是够用的。真正要按容量算术重算的场景是:请求耗时长(拉高线程需求)、长连接多(拉高连接容量)、下游有硬上限(限制线程数)。没有压测数据就别动它们,调参迷信比默认值危险。

参数速查表与联动关系

六个连接器参数一张表收齐,最后一列写清"什么时候才该动它"——没有对应的观测信号,就保持默认。

参数 默认值 作用岗位 动它的前提信号
maxThreads 200 工作线程池 线程全忙且下游还有余量
minSpareThreads 10 工作线程池 突发流量下首波请求排队
maxConnections 8192 城内容量 连接数曲线触顶且出现被拒
acceptCount 100 门外等待队列 峰值期连接被拒但城内容量未满
connectionTimeout 20000 毫秒 空闲回收 大量慢客户端长期占线
keepAliveTimeout 同连接超时 长连接空闲 空闲长连接数巨大

最后强调一次联动:这些参数不独立生效。maxThreads 提到 400,4.3 的连接池 maxTotal 还是 50,多出来的线程全在等连接——白忙。改任何一个参数前,把它上下游的闸门一起过一遍,这正是"参数有住址"在连接器一段的具体化。

本节要点回顾

  • 一句话分水岭:BIO 线程等数据,NIO 轮询器等数据、线程只干活;实测差距是 300 连接对 298 线程与 10 线程。
  • NIO2 与 APR 是同思想的异实现:日常负载差距小,选型看环境,默认 NIO 最稳。
  • 三道闸门各管一段:maxConnections 管城内容量,acceptCount 管门外排队,maxThreads 管办事窗口。
  • 线程数由下游决定:吞吐目标除以单请求处理能力等于所需线程,仓库发货力是隐藏上限。
  • 默认值多数时候够用:动参数前先有压测曲线,否则回滚都无从对证。

连接器会接客了,下一节把它接到真实网络:多端口、HTTPS 证书、以及那个叫 AJP 的内网专线。


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