本人在项目运用中写了一个数据推送的组件,需要多线程频繁调用远程接口进行传输数据,远程请求通过HttpClient 使用 CloseableHttpClient 发起连接后,使用CloseableHttpResponse 接受返回结果,一开始每次请求耗时时间都比较长,因此引入了httpClient连接池。
为什么要使用连接池:
1、降低延迟:如果不采用连接池,每次连接发起Http请求的时候都会重新建立TCP连接(经历3次握手),用完就会关闭连接(4次挥手),如果采用连接池则减少了这部分时间损耗 2、支持更大的并发:如果不采用连接池,每次连接都会打开一个端口,在大并发的情况下系统的端口资源很快就会被用完,导致无法建立新的连接
在实际项目运行中,发现系统偶尔会出现崩溃,查看日志,发现后台每次请求都返回错误 java.lang.IllegalStateException: Connection pool shut down,寻找错误错误源头,也没发现其他的错误信息,初步判断定位到http请求的httpclient问题。
Caused by: java.lang.IllegalStateException: Connection pool shut down
at org.apache.http.util.Asserts.check(Asserts.java:34)
at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.requestConnection(PoolingHttpClientConnectionManager.java:269)
at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:176)
at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186)
at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:89)
at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:108)
at com.hikvision.eits.demo.xcas.http.PooledCustomHttpClient.download(PooledCustomHttpClient.java:233)
错误1:通过问题查找网上资料,有可能使用httpClient.close()主动关闭了连接,导致下次请求的时候抛出连接池关闭异常,排查代码,发现未存在该代码, 连接池代码中,连接不需要业务管理而是交给连接池管理
错误2:httpclient连接池单个路由最大连接数设置不合理,业务高峰期时无法及时从连接池中获取连接,导致http线程都处于BLOCKED(阻塞于锁)状态,逐渐拖垮应用服务器,当最大线程是不能超过setDefaultMaxPerRoute设置的数字,一旦超过就会死掉。这里会报错 connection pool shut down httpclient,所以每次使用完都需要CloseableHttpResponse.close()释放资源,防止资源一直被占用。
- MaxtTotal是整个池子的大小;
- DefaultMaxPerRoute是根据连接到的主机对MaxTotal的一个细分;
HttpClientBuilder builder = HttpClients.custom();
使用该方式定义了httpcliet,默认使用的是PoolingHttpClientConnectionManager 线程池管理器,默认每个route只允许最多2个connection,总的connection数量不超过20。
connectionManager.setMaxTotal(80);
connectionManager.setDefaultMaxPerRoute(80);
一开始我在项目中设置的最大并发数为80,考虑到实际业务中会通过线程池创建大量线程,又有多处运行了单线程处理,把最大并发数改为200,后续更新后运行仍会出现连接关闭错误。
再次查看自己创建连接池对象代码,排查过程中.setConnectionManagerShared(true) 有说添加这个设置可解决问题,于是把该配置加上,部署后果然正常稳定执行
该配置保证后台使用一个共享连接池,供剩下打开的连接去使用
最后完整创建对象的代码:
private static final int DEFAULT_POOL_MAX_TOTAL = 200;
private static final int DEFAULT_POOL_MAX_PER_ROUTE = 200;
private static final int DEFAULT_CONNECT_TIMEOUT = 8000;
private static final int DEFAULT_CONNECT_REQUEST_TIMEOUT = 12000;
private static final int DEFAULT_SOCKET_TIMEOUT = 20000;
private PoolingHttpClientConnectionManager gcm = null;
private CloseableHttpClient httpClient = null;
private IdleConnectionMonitorThread idleThread = null;
// 连接池的最大连接数
private final int maxTotal;
// 连接池按route配置的最大连接数
private final int maxPerRoute;
// tcp connect的超时时间
private final int connectTimeout;
// 从连接池获取连接的超时时间
private final int connectRequestTimeout;
// tcp io的读写超时时间
private final int socketTimeout;
//构造方法
public PooledCustomHttpClient() {
this(PooledCustomHttpClient.DEFAULT_POOL_MAX_TOTAL,
PooledCustomHttpClient.DEFAULT_POOL_MAX_PER_ROUTE,
PooledCustomHttpClient.DEFAULT_CONNECT_TIMEOUT,
PooledCustomHttpClient.DEFAULT_CONNECT_REQUEST_TIMEOUT,
PooledCustomHttpClient.DEFAULT_SOCKET_TIMEOUT);
}
public PooledCustomHttpClient(int maxTotal, int maxPerRoute, int connectTimeout, int connectRequestTimeout, int socketTimeout) {
this.maxTotal = maxTotal;
this.maxPerRoute = maxPerRoute;
this.connectTimeout = connectTimeout;
this.connectRequestTimeout = connectRequestTimeout;
this.socketTimeout = socketTimeout;
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build();
this.gcm = new PoolingHttpClientConnectionManager(registry);
this.gcm.setMaxTotal(this.maxTotal);
this.gcm.setDefaultMaxPerRoute(this.maxPerRoute);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(this.connectTimeout) // 设置连接超时
.setSocketTimeout(this.socketTimeout) // 设置读取超时
.setConnectionRequestTimeout(this.connectRequestTimeout) // 设置从连接池获取连接实例的超时
.build();
HttpClientBuilder httpClientBuilder = HttpClients.custom();
this.httpClient = httpClientBuilder
.setConnectionManager(this.gcm)
.setConnectionManagerShared(true)
.setDefaultRequestConfig(requestConfig)
.build();
this.idleThread = new IdleConnectionMonitorThread(this.gcm);
this.idleThread.start();
}
最后远程调用后释放连接资源
public String doPostBody(String url, Map<String, String> reqHeader, String body, Map<String, String> respHeader) {
CloseableHttpResponse response = null;
try {
HttpPost httpPost = new HttpPost(url);
if (reqHeader != null && reqHeader.size() > 0) {
for (Map.Entry<String, String> entry : reqHeader.entrySet()) {
httpPost.addHeader(entry.getKey(), entry.getValue());
}
}
if (body != null) {
StringEntity s = new StringEntity(body, Charset.forName("UTF-8"));
httpPost.setEntity(s);
}
response = httpClient.execute(httpPost);
if (response == null) {
return null;
}
String res = null;
HttpEntity entityRes = response.getEntity();
if (entityRes != null) {
res = EntityUtils.toString(entityRes, "UTF-8");
}
return res;
} catch (Throwable e) {
throw new RuntimeException(tip, e);
} finally {
//关闭资源
IOUtils.closeQuietly(response);
}
}