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

一、概述

本文分析的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/

posted @ 2023-05-11 22:32  wuworker  阅读(5615)  评论(0编辑  收藏  举报