首页 > 其他分享 >HttpClient连接池的连接淘汰策略分析,以及解决HttpNoResponse异常

HttpClient连接池的连接淘汰策略分析,以及解决HttpNoResponse异常

时间:2023-05-11 22:46:42浏览次数:49  
标签:调用 setValidateAfterInactivity connectionManager PoolingHttpClientConnectionManag

本文分析的apache HttpClient版本为4.5

在HttpClient连接池的使用中,发现有三处关于连接释放的时间配置

  1. PoolingHttpClientConnectionManager构造函数中的timeToLive,默认是-1
public PoolingHttpClientConnectionManager(
        final Registry<ConnectionSocketFactory> socketFactoryRegistry,
        final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
        final SchemePortResolver schemePortResolver,
        final DnsResolver dnsResolver,
        final long timeToLive, final TimeUnit timeUnit)
  1. PoolingHttpClientConnectionManager的setValidateAfterInactivity方法,默认为2000ms
public void setValidateAfterInactivity(final int ms) {
    pool.setValidateAfterInactivity(ms);
}
  1. HttpClientBuilder的evictExpiredConnections,evictIdleConnections方法,默认都是关闭
public final HttpClientBuilder evictExpiredConnections() {
    evictExpiredConnections = true;
    return this;
}

public final HttpClientBuilder evictIdleConnections(final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
    this.evictIdleConnections = true;
    this.maxIdleTime = maxIdleTime;
    this.maxIdleTimeUnit = maxIdleTimeUnit;
    return this;
}

那么这3处理连接的时间配置有什么区别,又该配置为多少比较合适呢

一、PoolingHttpClientConnectionManager构造函数中的timeToLive

1.1 代码分析

通过代码,发现timeToLive最终是设置到Cpool里

而Cpool的使用,仅是在创建createEntry时传入。一个entry可以认为是一个连接的封装

最终这个时间会在PoolEntry里转为validityDeadline,expiry的时间。如果timeToLive<=0,则为永不过期

这个expire的使用是在getPoolEntryBlocking方法,如果从池中获取到的已经是过期的(当前时间 >= expiry),那么会直接关闭,重新获取

另一个是在调用PoolingHttpClientConnectionManager的closeExpiredConnections时,通过expiry判断是否过期

最后expiry还有个更新的逻辑

这个更新是发生在releaseConnection,传入的time为服务器返回的keepalive时间,如果服务器没有返回,则为-1。
相当于每次执行http调用后,会把过期时间再延后keepalive time,但最大不超过设置的validityDeadline,也就是一开始传入的timeToLive

总结:PoolingHttpClientConnectionManager构造函数中的timeToLive,相当于是一个连接存活的最大时间。-1表示永不过期。连接存活超过该时间后,即使服务器依然保持连接,但连接池还是会判断为过期连接,直接关闭

1.2 代码验证

服务端代码,使用springboot起个web服务,配置连接保持10s

server:
  port: 8080
  tomcat:
    connection-timeout: 10000

客户端代码,连接最大存活5s,每隔3s调用一次

    public static void main(String[] args) throws Exception {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(
                5, TimeUnit.SECONDS);
        // 不校验
        connectionManager.setValidateAfterInactivity(-1);
        CloseableHttpClient httpClient = HttpClients.custom()
                                                 .setConnectionManager(connectionManager)
                                                 .build();
        // 第一次调用,新连接
        doGet(httpClient);

        Thread.sleep(3000);

        // 第二次调用,复用连接
        doGet(httpClient);

        Thread.sleep(3000);

        // 第三次调用,已经超过timeToLive,新连接
        doGet(httpClient);

        httpClient.close();
    }


    private static void doGet(CloseableHttpClient httpClient) throws Exception {
        try (CloseableHttpResponse response = httpClient.execute(new HttpGet("http://localhost:8080/hello"))) {
            System.out.println(Arrays.asList(response.getAllHeaders()));
            System.out.println(EntityUtils.toString(response.getEntity()));
        }
    }

通过wireshard抓包查看tcp连接情况

可以看到,第三次http请求已经超过了5s,所以客户端会先发起关闭连接,再重新建立连接

二、PoolingHttpClientConnectionManager的setValidateAfterInactivity方法

2.1 代码分析

通过代码,发现该值是设置到CPool中,在获取连接时AbstractConnPool#lease(T, Object,FutureCallback)进行判断

可以看到,连接空闲时间超过validateAfterInactivity时间后,进入到validate方法,如果校验失败会关闭连接,重新获取

其中CPool的validate方法,最终会调用到LoggingManagedHttpClientConnection的isStale()方法,该方法实现是在BHttpConnectionBase类

该方法会尝试从socket进行读取,如果读取到数据,或者出现SocketTimeoutException,则连接未断开,否则判断服务端已断开连接

总结:PoolingHttpClientConnectionManager的setValidateAfterInactivity方法,设置的是连接的空闲时间,超过该时间的连接,每次从池中获取,会进行校验(读取socket),校验失败会重新获取连接

2.2 代码验证

服务端代码,使用springboot起个web服务,配置连接保持60s

server:
  port: 8080
  tomcat:
    connection-timeout: 60000

客户端代码,校验时间设置超过10s需要校验

public static void main(String[] args) throws Exception {
    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(
            -1, TimeUnit.SECONDS);
    connectionManager.setValidateAfterInactivity(10000);
    CloseableHttpClient httpClient = HttpClients.custom()
                                             .setConnectionManager(connectionManager)
                                             .disableAutomaticRetries()
                                             .build();
    // 第一次调用,新连接
    doGet(httpClient);

    Thread.sleep(30000);

    // 第二次调用,新连接
    doGet(httpClient);
}

流程:

  1. 启动server
  2. 启动client代码
  3. 在client第一次调用完后,重启server(此时client端的连接会失效)
  4. 等待client第二次调用
  5. 第二次调用结果正常返回

如果设置setValidateAfterInactivity=-1,则第二次调用时(此时expiry时间还没到),因为拿到的是失效的连接,直接使用会返回NoHttpResponseException或者Connection reset

三、HttpClientBuilder的evictExpiredConnections,evictIdleConnections方法

3.1 代码分析

这2个方法比较简单,在HttpClientBuilder的build()时,如果设置了,会新建一个后台线程用于清理过期的连接

IdleConnectionEvictor的清理逻辑为,每隔idleTime,清理过期或者空闲超过idleTime的时间

closeExpiredConnections即为清理过期的连接(在第一点timeToLive时提过),closeIdleConnections为清理空闲超过idleTime的时间

总结:evictExpiredConnections,evictIdleConnections这2个方法驱逐逻辑为,后台线程定时检查过期连接,进行删除

3.2 代码验证

启动2个线程调用http,输出连接池里的数量。等待时间超过idle后,再次输出连接池里的数量,发现可用连接为0

    public static void main(String[] args) throws Exception {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(
                -1, TimeUnit.SECONDS);
        connectionManager.setValidateAfterInactivity(-1);
        CloseableHttpClient httpClient = HttpClients.custom()
                                                 .setConnectionManager(connectionManager)
                                                 .disableAutomaticRetries()
                                                 .evictIdleConnections(3, TimeUnit.SECONDS)
                                                 .build();
        Thread t1 = new Thread(() -> doGet(httpClient));
        Thread t2 = new Thread(() -> doGet(httpClient));
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        // 输出 [leased: 0; pending: 0; available: 2; max: 20]
        System.out.println(connectionManager.getTotalStats());

        Thread.sleep(7000);

        // 输出 [leased: 0; pending: 0; available: 0; max: 20]
        System.out.println(connectionManager.getTotalStats());
    }


    private static void doGet(CloseableHttpClient httpClient) {
        try (CloseableHttpResponse response = httpClient.execute(new HttpGet("http://localhost:8080/hello"))) {

            // 不调用close,不归还连接
            Thread.sleep(1000);
            System.out.println(Arrays.asList(response.getAllHeaders()));
            System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

四、连接池参数总结

  1. PoolingHttpClientConnectionManager的timeToLive为连接存活的最大时间,每次获取连接时,只要连接存活超过了该时间,即使服务器仍保持连接,客户端也会断开重新获取连接。默认值为-1
  2. PoolingHttpClientConnectionManager的setValidateAfterInactivity方法顾名思义,为每次获取连接时,连接空闲超过了该时间,就会校验是否可用(调用socket读取)。默认值为2000ms
  3. HttpClientBuilder的evictExpiredConnections,evictIdleConnections方法,是定时任务定时扫描,清理过期,空闲的连接。默认不开启

推荐:

  • 推荐timeToLive保持默认值-1就好,这样优先使用服务器返回的keepalive,只要在keepalive时间范围内,client就不会主动关闭。
  • setValidateAfterInactivity作为保底,每次获取连接时进行校验,时间越短越不容易拿到过期的连接。默认值是2000ms。最好短于keepalive时间。(springboot使用tomcat默认连接保持60s,nginx默认连接保持75s)。
  • evictExpiredConnections,evictIdleConnections定时清理的任务,默认是不开启。推荐还是开启下,这样在没有发生http调用时,可以清理掉无用连接

五、解决偶发出现的HttpNoResponse,Connection reset异常

5.1问题原因

在HttpClient的使用过程中,相信很多人遇到出这2个异常。调用出现HttpNoResponse,Connection reset异常的原因,就是从连接池中获取的连接已经失效了(可能是服务器重启或者其他原因)。

HttpClient在获取到连接读数据时,如果操作系统已经将连接关闭,则返回I/O error: Connection reset;如果还未关闭,则返回end of stream也就是NoHttpResponse异常

验证代码如下

server端:保持60s的keepalive

server:
  port: 8080
  tomcat:
    connection-timeout: 60000

客户端:

public static void main(String[] args) throws Exception {
    PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(
            -1, TimeUnit.SECONDS);
    connectionManager.setValidateAfterInactivity(-1);
    CloseableHttpClient httpClient = HttpClients.custom()
                                             .setConnectionManager(connectionManager)
                                             // 关闭自动重试
                                             .disableAutomaticRetries()
                                             .build();
    doGet(httpClient);

    Thread.sleep(20000);

    doGet(httpClient);
}
}

验证流程

  1. 启动server
  2. 启动client,第一次调用完成后,重启server
  3. 等待client第二次调用

调整第二次调用前的sleep时间,时间越长越可能出现Connection reset

5.2 解决

  1. 配置setValidateAfterInactivity,默认2000ms,时间越短越不容易拿到过期的连接。该时间已经足够短了,没太大必要再设置小于该值
  2. 配置重试(HttpClient默认是打开自动重试的)。但默认是一共重试3次。如果连接池里是5条连接,全部过期。那么还是会出现异常(概率比较小)。另一个方案是配置重试次数> 连接池里单个route的最大连接数量,保证连接池里的连接全部失效后,建立新连接

六、参考

https://czjxy881.github.io/java,nginx/%E8%AE%B0HttpClient%E7%9A%84NoHttpResponse%E9%97%AE%E9%A2%98/

标签:调用,setValidateAfterInactivity,connectionManager,PoolingHttpClientConnectionManag
From: https://www.cnblogs.com/wusanga/p/17392445.html

相关文章

  • HTTP协议客户端之HttpClient介绍及使用
    1.HttpClient介绍HttpClient是ApacheJakartaCommon下的子项目,可以用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议。HttpClient相比JDK自带的URLConnection,增加了易用性和灵活性,使客户端发送Http请求变得更加容......
  • 数据库连接池报错java.lang.NoClassDefFoundError
    第一次用c3p0,在连接时,发声如下报错java.lang.NoClassDefFoundError 经查看,发现它需要辅助包 mchange-commons-java.jar,下载放入后,即可 ......
  • 数据库连接池
     (解决每次数据库连接的卡顿)优点~~~~1.资源重用避免了频繁的创建2.更快的系统反应速度提前创建了若干数据库连接3.新的资源分配手段实现一个应用最大可用数据库连接数的限制4.同意连接管理避免数据连接泄露强制回收被占用连接从而避免了常规数据库连接操作中出现的资源泄露......
  • springboot alibaba druid数据库连接池配置,输出可执行sql
    #数据源配置spring:datasource:type:com.alibaba.druid.pool.DruidDataSourcedruid:#初始连接数initialSize:5#最小连接池数量minIdle:2#最大连接池数量maxActive:50#配置获取连接等待超时的时间......
  • C3P0连接池在tomcat中的详细配置
    http://qiufubin.blog.sohu.com/55457392.html 2007-07-16 | C3P0连接池在tomcat中的详细配置  一.在tomcat_home\common\lib下放入jdbc的驱动程序,额外说一下,如果是使用sqlserver的话,有至少两个驱动可以选择,一个是微软提供的,另一个是jtds,比微软的要好很多,推荐使用二.配......
  • 数据库连接池到底应该设多大?
    >阅读大约3分钟,颠覆认知[toc]前言本文内容95%译自这篇文章:https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing我在研究HikariCP(一个数据库连接池)时无意间在HikariCP的Githubwiki上看到了一篇文章(即前面给出的链接),这篇文章有力地消除了我一直以来的疑虑,看完......
  • 连接池/线程池
    线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有......
  • .net core HttpClient
    .netcoreHttpClient使用之掉坑解析(一)-Jlion-博客园(cnblogs.com) ......
  • 建立redis的连接池
    1、在配置文件中添加参数(application-dev.yml)redis:#***邮件发送服务器地址host:127.0.0.1#***邮件发送服务器端口port:6379#***提醒邮件发件人邮箱timeout:100002、建一个获取redis连接的工具类@ComponentpublicclassJTRedisUtils{pr......
  • Python数据库连接池DBUtils
    DBUtils是Python的一个用于实现数据库连接池的模块。安装pip3instal1dbutilspip3instal1pymysql 此连接池有两种连接模式:模式一:为每个线程创建一个连接,线程即使调用了close方法,也不会关闭,只是把连接重新放到连接池,供自己线程再次使用。当线程终止时,连接自动关闭。......