keepalive连接复用对tomcat线程池的影响
本文来源于看到的一篇文章: tomcat的acceptCount、maxThreads、connectionTimeout参数调整
- 这篇文章中对acceptCount的分析其实很到位,在理解了tcp的握手过程、syn队列和accept队列的知识之后,就比较容易理解这个参数的含义以及在对tomcat的缓冲和保护作用了。
- 在看了tomcat的源码之后,也容易理解文章中说的关于connectionTimeout其实就是SO_TIMEOUT也是对的,作为服务端的connectionTimeout这个参数很容易被名字误导跟一些客户端连接工具(比如HttpClient、数据库连接池之类)的connectionTimeout参数搞混,客户端这个参数代表英文直接翻译过来的意思“建立连接超时”,尝试跟对端建立连接然后尝试了这个时间之后还没连上就抛异常,而tomcat作为服务端的这个参数跟“建立连接”没关系,事实上它应该是建立连接之后,如果超过connectionTimeout这个时间还没收到客户端的请求,则抛异常。tomcat会默认用它来设置socket的readTimeout和writeTimeout、从这也可以看出tomcat中这个参数的含义。另外,这个参数跟maxKeepaliveTimeout的区别是maxKeepaliveTimeout强调是请求处理完了之后等待下一次请求的超时时间。
- 文章中关于maxThreads的理解笔者也是认同的,关于tomcat工作线程池的大小如何调整其实取决于task的性质和当前系统运行状态CPU利用率,task阻塞比较多、可以适当调多一些线程个数增加并行处理和吞吐能力,如果task阻塞很少、cpu利用比较充分,那调多线程个数其实会起到相反效果、需知CPU在线程间切换的成本也是比较高的,当CPU花在线程切换上的开销甚至高于实际处理业务逻辑上的开销时,显然这样的调优是误入了歧途。
但是,文章中的一段关于keepalive的观点让我产生了疑惑:
“当开启http keep alive的时候,client端可能没有那么及时地关闭连接,那么server端的worker线程会一直被这些实际上可能不活跃的连接给占用了,导致worker线程没能重复利用起来。”
如果按照上面的描述,当client没有请求发送但维持住长连接不及时关闭,tomcat的worker线程会被占用,那不就是相当于:
- 在tomcat使用nio模式时,client与tomcat之间维持连接,复用于多个request,这时候worker线程是专门为这个连接服务的吗?连接断开之前能否服务其他连接?
-
如果不能,那么是不是就跟bio一样了,相当于有多少个worker线程就能服务多少个连接了?
直觉感觉这不可能,事实上,tomcat NIO模式下,worker线程在一次request的读写过程中是blocking的,但是一次request读写完成之后,等待下次request是unblocking的。这在tomcat的官网上关于其几种IO模式的设计思路上可以查到。见下图:
另外,关于tomcat的IO方式和线程模型笔者也在Tomcat NIO线程模型与IO方式分析 这篇文章中分析过:等待下一次从socket连接过来的请求的时候是把socket注册到Poller的selector上的,这个时候它是非阻塞的,而读一个具体的request body的时候,如果body未读完则使用CountDownLatch阻塞当前线程等待BlockPoller通知继续读。
Poller每次提交给worker线程池的task类是SocketProcessor,名字起的有些误导人,事实上提交给线程池处理的不是“一个连接”而应该理解为“一次请求”,同一个连接上可以有多个请求,每个请求可能会分配给不同的worker线程去处理,但是每个请求的body是始终由一个线程处理的。这就是tomcat的请求处理的线程模型。
程序验证
服务端使用springboot2内嵌的tomcat9运行一个servlet,为了验证,对tomcat做如下配置:
server.port=8080
server.servlet.context-path=/prototype
server.tomcat.max-threads=1
server.tomcat.max-connections=6
server.tomcat.accept-count=2
然后启动两个客户端程序,代码基本一样,就是不断的以keepalive的方式往服务端发请求:
import org.apache.http.client.fluent.Request;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 使用HttpClient fluent api客户端工具建立连接并发送http POST请求,
* 客户端有连接池,能够以keepalive方式与服务端进行连接复用,一个连接可发送多个请求。
* */
public class TestHttpKeepAlive1 {
private static Logger logger = LoggerFactory.getLogger(TestHttpKeepAlive1.class);
public static void main(String[] args) {
StringEntity entity = new StringEntity("{\"name\":\"AAA\"}", ContentType.APPLICATION_JSON);
try {
while (true) {
String reponseContent = Request.Post("http://localhost:8080/prototype/testRequestServlet").body(entity)
.execute().returnContent().toString();
logger.info(reponseContent);
TimeUnit.SECONDS.sleep(2);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
tomcat的默认maxKeepaliveTimeout是60s,另外一个控制长连接能保持多久的参数是maxKeepAliveRequests、默认是100,可以通过调试客户端程序配合netstat命令查看tcp连接,当启动一个客户端向服务端发送100次请求之前或发送完一个请求之后的60秒内,客户端与服务端之间的连接一直是同一个,这可以通过客户端的端口号确认。
而当我们启动两个客户端的时候,可以看到服务端也是可以用1个工作线程来同时服务两个客户端连接的,且2个连接一直保持(执行了100次请求之后会打开新连接)。
连接复用可以较少频繁的连接建立与关闭带来的开销,特别是对于传输报文本身比较小的情况,短链接的建立与关闭开销在整个通信过程占比十分可观。但有一点要注意就是长连接对客户端出站端口的占用和服务端tomcat连接数的占用(通过LimitLatch控制),当tomcat仅接收来自其他内部系统的调用的时候、客户端连接的数量相对是可控的,开启长连接可以显著提高性能。
进一步参考
关于tomcat的各个配置参数的含义和默认值,可以参考tomcat官网doc: https://tomcat.apache.org/tomcat-9.0-doc/config/http.html
通过合理的使用keepalive来提高client与tomcat之间的http请求性能,可以参考配置TOMCAT及httpClient的keepalive以高效利用长连接