一、爲什麼會報Timeout waiting for connection from pool
首先我們需要知道,HttpClient是不建議每次使用都創建的,因爲它本身就帶一個連接池。如果我們使用頻繁的話,頻繁創建HttpClient對象也不是明智的。
我在使用HttpClient的時候,在一個調用鏈中,只是依次使用了HttpClient調用了幾次http接口,卻發現在第5個的時候,報錯了
org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
這是我當時使用的配置:
private static HttpClient client;
static {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setConnectionRequestTimeout(3000)
.setSocketTimeout(5000).build();
client = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
.build();
}
查了下才知道這個錯,是因爲連接池沒有空閒連接,並且等待獲取連接的時間達到了setConnectionRequestTimeout
,我很疑惑爲啥才一個單線程使用了幾下就報錯了呢?於是我開始了測試。
二、測試
這裏先說結論
HttpClient默認連接池,最大連接是20個,最大同路由的是2個,也就是maxConnTotal是20,maxConnPerRoute是2。
maxConnTotal是同時間正在使用的最多的連接數
maxConnPerRoute是針對一個域名同時間正在使用的最多的連接數
看源碼
可以看org.apache.http.impl.conn.PoolingHttpClientConnectionManager
類,
public PoolingHttpClientConnectionManager(HttpClientConnectionOperator httpClientConnectionOperator, HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory, long timeToLive, TimeUnit timeUnit) {
this.log = LogFactory.getLog(this.getClass());
this.configData = new PoolingHttpClientConnectionManager.ConfigData();
this.pool = new CPool(new PoolingHttpClientConnectionManager.InternalConnectionFactory(this.configData, connFactory), 2, 20, timeToLive, timeUnit);
this.pool.setValidateAfterInactivity(2000);
this.connectionOperator = (HttpClientConnectionOperator)Args.notNull(httpClientConnectionOperator, "HttpClientConnectionOperator");
this.isShutDown = new AtomicBoolean(false);
}
驗證1
先前5個調用百度,後5次調用hao123
package com.zgd.demo.web.test;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import java.io.IOException;
/**
* @Author: zgd
* @Date: 2019/8/15 00:04
* @Description: 測試httpclient
*/
@Slf4j
public class MyHttpClientTest {
private static HttpClient client;
static {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setConnectionRequestTimeout(3000)
.setSocketTimeout(5000).build();
client = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
.build();
}
@Test
public void fun01() {
for (int i = 0; i < 10; i++) {
String url = "";
if (i < 5){
url = "http://www.baidu.com";
}else {
url = "http://www.hao123.com";
}
HttpGet httpGet = new HttpGet(url);
log.info("------url:{}-----",url);
HttpResponse res = null;
try {
res = client.execute(httpGet);
} catch (IOException e) {
log.error("異常: ",e);
return;
}
int code = res.getStatusLine().getStatusCode();
log.info("---url:{}\tcode:{}\ti:{}",url, code,i+1);
}
}
}
運行結果
在第3個,i=2的時候就跑這個異常了,在debug級別日誌打印出total kept alive: 0; route allocated: 2 of 2; total allocated: 2 of 20
,也就是這個route下一共2個連接,已經使用2個。整個pool有20個連接,使用了2個
驗證2
依次調用百度和hao123
@Test
public void fun01() {
for (int i = 0; i < 10; i++) {
String url = "";
if (i % 2 == 0){
url = "http://www.baidu.com";
}else {
url = "http://www.hao123.com";
}
HttpGet httpGet = new HttpGet(url);
log.info("------url:{}-----",url);
HttpResponse res = null;
try {
res = client.execute(httpGet);
} catch (IOException e) {
log.error("異常: ",e);
return;
}
int code = res.getStatusLine().getStatusCode();
log.info("---url:{}\tcode:{}\ti:{}",url, code,i+1);
}
}
驗證3
我們修改代碼如下
@Test
public void fun01() {
for (int i = 0; i < 10; i++) {
String url = "";
if (i % 2 == 0){
url = "http://www.baidu.com";
}else {
url = "http://www.hao123.com";
}
HttpGet httpGet = new HttpGet(url);
log.info("------url:{}-----",url);
HttpResponse res = null;
try {
res = client.execute(httpGet);
int code = res.getStatusLine().getStatusCode();
log.info("---url:{}\tcode:{}\ti:{}",url, code,i+1);
} catch (IOException e) {
log.error("異常: ",e);
return;
}finally {
if (res != null){
EntityUtils.consumeQuietly(res.getEntity());
}
}
}
}
不管是先百度後好123,還是依次調用,都完全ok了
這裏使用了HttpClient提供的工具類EntityUtils
來處理,EntityUtils的consume
方法都對連接資源進行了釋放。
public static void consumeQuietly(HttpEntity entity) {
try {
consume(entity);
} catch (IOException var2) {
}
}
...
public static void consume(HttpEntity entity) throws IOException {
if (entity != null) {
if (entity.isStreaming()) {
InputStream inStream = entity.getContent();
if (inStream != null) {
inStream.close();
}
}
}
}
三、結果
可以看出,出現Timeout waiting for connection from pool異常的原因主要有二:
- 連接沒關閉,資源沒釋放。
- pool的連接數設置太小
可以看到第1條,是老生常談的資源關閉問題。博主在開發的時候,正是因爲不需要知道調用的返回結果,所以沒有對Response進行處理,所以資源並沒有釋放,導致後面再使用HttpClient,就拿不到連接了
第2條,需要知道maxConnTotal和maxConnPerRoute的具體含義,如果是需要頻繁調用同一個域名,那麼即使maxConnTotal設置再大,還是受限制與maxConnPerRoute
根據業務實際情況合理配置連接池
private static HttpClient client;
static {
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setConnectionRequestTimeout(1000)
.setSocketTimeout(5000).build();
client = HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
.setMaxConnTotal(200)
.setMaxConnPerRoute(50)
.build();
}