美文网首页tomcat
keepalive连接复用对tomcat线程池的影响

keepalive连接复用对tomcat线程池的影响

作者: 肥兔子爱豆畜子 | 来源:发表于2021-08-02 21:58 被阅读0次

    本文来源于看到的一篇文章: 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线程会被占用,那不就是相当于:

    1. 在tomcat使用nio模式时,client与tomcat之间维持连接,复用于多个request,这时候worker线程是专门为这个连接服务的吗?连接断开之前能否服务其他连接?
    2. 如果不能,那么是不是就跟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以高效利用长连接

    相关文章

      网友评论

        本文标题:keepalive连接复用对tomcat线程池的影响

        本文链接:https://www.haomeiwen.com/subject/cbojvltx.html