Servlet 3 性能调优
Servlet 3 早在 2011 年推出,但很多 web 项目还是使用 Servlet 2.5。这些 web 项目业务相对较重、单机负载相对较低,Servlet 2.5 由于更少切换线程比 Servlet 3 更加稳定。
随着微服务的发展,web 层不断演进,涌现出越来越多的 API 网关。API 网关将 web 层的业务剥离开,专注于安全防护和路由转发。这种重 IO 轻 CPU 的场景非常适合使用 Servlet 3。
通常异步 Servlet 项目中有 3 类线程池:Tomcat IO 工作线程池、业务线程池、RPC 线程池。
![](https://img.haomeiwen.com/i3596970/d759129f37c0ea72.png)
下面介绍 Servlet 优化的五个阶段,这五个阶段大致分为三类优化手段:线程隔离、无锁处理和更换容器。
第一阶段:纯同步
第一阶段采用同步 Servlet,Tomcat 线程进入业务代码并执行预处理、RPC 调用、结果处理
压测到 1000 QPS 时 AVG(20)、99 线(70)、999 线(75),整体平稳
![](https://img.haomeiwen.com/i3596970/4fc2e6b616732723.png)
第二阶段:伪异步
异步 Servlet 中通过 servletRequest.startAsync 开启异步,asyncContext.dispatch 重新分发请求、asyncContext.complete 结束异步。Spring MVC 的异步是先开启异步、执行业务、重新分发请求。这种方式是为了兼容 Spring MVC 的同步实现,但是由于两次分发导致代码更复杂、并可能对 Servlet 带来更大的压力。
在第二阶段的时候采用类似的实现:
- Tomcat 线程进入业务代码并预处理请求
- Tomcat 线程开启异步并异步请求 RPC 服务
- RPC 回调后重新分发请求
- Tomcat 线程进入业务代码并处理请求结果
压测到 1000 QPS 时 AVG(18)、99 线(56)、999 线(60),整体平稳,异步占优
压测到 1400 QPS 时 AVG(23)、99 线(80)、999 线(85),整体平稳,异步占优
压测到 2000 QPS 时 AVG(32)、99 线(300)、999 线(320),波动较大
![](https://img.haomeiwen.com/i3596970/ecc8f8f5ef491a47.png)
第三阶段:全异步
项目在第三阶段的时候采用全异步:
- Tomcat 线程进入业务代码并开启异步
- Tomcat 线程提交业务线程并返回
- 业务线程预处理并调用 RPC 服务
- RPC 回调后提交业务线程
- 业务线程处理请求结果并结束异步
压测到 2500 QPS 时 AVG(44)、99 线(83)、999 线(85),整体平稳,时有抖动
压测到 3000 QPS 时 AVG(70)、99 线(180)、999 线(190),整体平稳,时有抖动
![](https://img.haomeiwen.com/i3596970/87932d96b3777560.png)
第四阶段:异步优化
业界 4 核 8 g 异步 Servlet 的机器吞吐量能够达到 1w+ QPS
1.观察机器在压测时的性能指标,QPS 3000 时出现 full gc
![](https://img.haomeiwen.com/i3596970/0a77e1c771a6e9f6.png)
2.压测结束后 Dump 机器内存后发现无大对象,但是有大量 byte [] 和 char [] 对象
![](https://img.haomeiwen.com/i3596970/dc881c07e5615864.png)
3.使用 JProfiler 分析 byte [] 的来源,可能来自 Tomcat 的 IO
![](https://img.haomeiwen.com/i3596970/93c2e4f84d87183b.png)
在对比 Tomcat 线程数后猜测,可能是 Tomcat 线程池打满所致。同时在 Tomcat 日志中也出现大量队列已满的报错
4.尝试调整 Tomcat 线程池配置:增加线程数量、采用 Http11Nio2Protocol 处理协议。调整后压测吞吐量没有提升。https://renwole.com/archives/357
<Executor
name="tomcatThreadPool"
namePrefix="catalina-exec-"
maxThreads="500"
minSpareThreads="30"
maxIdleTime="60000"
prestartminSpareThreads = "true"
maxQueueSize = "100"
/>
<Connector
executor="tomcatThreadPool"
port="8080"
protocol="org.apache.coyote.http11.Http11Nio2Protocol"
connectionTimeout="60000"
maxConnections="10000"
redirectPort="8443"
enableLookups="false"
acceptCount="100"
maxPostSize="10485760"
maxHttpHeaderSize="8192"
compression="on"
disableUploadTimeout="true"
compressionMinSize="2048"
acceptorThreadCount="2"
URIEncoding="utf-8"
processorCache="20000"
tcpNoDelay="true"
connectionLinger="5"
server="Server Version 11.0"
/>
5.修改业务线程池大小
压测到 5000 QPS 时 AVG(59)、99 线(130)、999 线(145),整体平稳
![](https://img.haomeiwen.com/i3596970/a5cecaf8a91ae970.png)
6.修改调用下游服务的方式,基于回调 => 同步
![](https://img.haomeiwen.com/i3596970/c4158419ac7a6634.png)
7.对比业务各阶段耗时,增加服务调用线程池线程
压测到 6000 QPS 时高点 AVG(348)、99 线(500)、999 线(1000),整体平稳
![](https://img.haomeiwen.com/i3596970/2b934d10030ffb6e.png)
8.异步 Servlet 开启异步后超时自动结束请求,RPC 在回调时如果已经超时会报状态异常,因此对 onTimeout 和 onComplete 加上同步锁
![](https://img.haomeiwen.com/i3596970/c613e025f4b1d897.png)
9.移除 RPC 调用,mock 服务返回结果
压测到 10000 QPS 时高点 AVG(12)、99 线(504)、999 线(1000),CPU 几乎打满
![](https://img.haomeiwen.com/i3596970/e6f4b15a03c9ae37.png)
10.调整业务线程组,预处理和调用服务使用同一线程、RPC 回调和处理返回结果使用同一线程,弱化 CPU 轮转。压测到高点时开始出现线程阻塞
压测到 6000 QPS 时高点 AVG(8)、99 线(35)、999 线(1000),CPU 几乎打满
![](https://img.haomeiwen.com/i3596970/65f22950309771ba.png)
11.缩短 RPC 服务超时时间 200 -> 50
压测到 4000 时开始出现线程阻塞
![](https://img.haomeiwen.com/i3596970/025941e60abbf50b.png)
12.移除 AsyncContext 监听中 onTimeout 和 onComplete 上的同步锁
![](https://img.haomeiwen.com/i3596970/8e82c144466819fa.png)
13.使用 servlet 3.1 nio,注册 reader listener 和 write listener 异步读写
![](https://img.haomeiwen.com/i3596970/e2c94579fe30e8db.png)
压测到 7000 QPS 时高点 AVG(10)、99 线(30)、999 线(1000)
![](https://img.haomeiwen.com/i3596970/8486ed1aa44fc2a2.png)
第四阶段总结:
- 异步 IO 能一定程度上提高并发,在传输量不大的情况下提高并不明显
- 高并发同步调用会阻塞 IO 线程
- Tomcat 内部没有复用 bytebuffer 导致请求量大时新生代 gc 频繁,最后引起服务超时,日志阻塞等一系列问题
第五阶段:Jetty 容器
Jetty 底层对 ByteBuffer 对象的复用和线程池的处理有更好的优化。
在第五阶段,更换 Servlet 容器为 Jetty 进行调优。
压测到 11000 QPS 时高点 AVG(40)、99 线(90)、999 线(140),整体平稳
![](https://img.haomeiwen.com/i3596970/6800a7f0946e50b2.png)
第五阶段总结:
- 在高并发的场景中,Jetty 的性能比 Tomcat 要强,耗时曲线非常平稳
- 使用 Jetty 容器后,10000 QPS 时 AVG(5)、99 线(20)、999 线(20