本文分析的apache HttpClient版本为4.5
在HttpClient连接池的使用中,发现有三处关于连接释放的时间配置
- 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)
- PoolingHttpClientConnectionManager的setValidateAfterInactivity方法,默认为2000ms
public void setValidateAfterInactivity(final int ms) {
pool.setValidateAfterInactivity(ms);
}
- 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);
}
流程:
- 启动server
- 启动client代码
- 在client第一次调用完后,重启server(此时client端的连接会失效)
- 等待client第二次调用
- 第二次调用结果正常返回
如果设置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();
}
}
四、连接池参数总结
- PoolingHttpClientConnectionManager的timeToLive为连接存活的最大时间,每次获取连接时,只要连接存活超过了该时间,即使服务器仍保持连接,客户端也会断开重新获取连接。默认值为-1
- PoolingHttpClientConnectionManager的setValidateAfterInactivity方法顾名思义,为每次获取连接时,连接空闲超过了该时间,就会校验是否可用(调用socket读取)。默认值为2000ms
- 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);
}
}
验证流程
- 启动server
- 启动client,第一次调用完成后,重启server
- 等待client第二次调用
调整第二次调用前的sleep时间,时间越长越可能出现Connection reset
5.2 解决
- 配置setValidateAfterInactivity,默认2000ms,时间越短越不容易拿到过期的连接。该时间已经足够短了,没太大必要再设置小于该值
- 配置重试(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