TL;DR;
CloseableHttpClient.close()
方法默认行为是关闭HttpClientConnectionManager
- 如果多个
CloseableHttpClient
共用了同一个HttpClientConnectionManager
,则第一个请求执行完,其他请求就会爆Connection pool shut down
异常 - 备注:httpclient 版本是 4.5.6
1. 背景
某次上线以后,观察到所有的方法调用量都变得特别多,几百倍的增长,但因为当时同一时间段上线逻辑较多,并没有及时定位到问题。
第二天继续排查时,有同事指出 httpclient 连接池 Connection pool shut down
异常增多,于是想到前一天确实使用了相关的方法,代码如下
try {
@Cleanup CloseableHttpClient client = Requests.getHttpclient(
15000, 15000, 15000, proxyIpInfo);
HttpGet httpGet = new HttpGet(url);
@Cleanup final CloseableHttpResponse execute = client.execute(httpGet);
return EntityUtils.toByteArray(execute.getEntity());
} catch (IOException e) {
throw new RuntimeException(e);
}
2. CloseableHttpClient
的 close
方法
CloseableHttpClient
实例是通过如下代码创建的,其中 CONNECTION_MANAGER
是一个全局静态的共用的 HttpClientConnectionManager
实例。
HttpClients.custom()
.setConnectionManager(CONNECTION_MANAGER)
.setDefaultRequestConfig(requestConfig)
.build();
让我们深入 org.apache.http.impl.client.HttpClientBuilder#build
的源码,看一下如何构建 CloseableHttpClient
的 close
方法的,核心代码如下,注意看其中我补充的注释
List<Closeable> closeablesCopy = closeables != null ? new ArrayList<Closeable>(closeables) : null;
// 如果 connManagerShared 属性是 false(默认值)
// 那么其 close 方法的逻辑就是关闭 ConnectionManager
if (!this.connManagerShared) {
if (closeablesCopy == null) {
closeablesCopy = new ArrayList<Closeable>(1);
}
final HttpClientConnectionManager cm = connManagerCopy;
... ...
closeablesCopy.add(new Closeable() {
@Override
public void close() throws IOException {
cm.shutdown();
}
});
}
3. bug 定位
结合上面两个代码片段,真相已经大白,当 @Cleanup CloseableHttpClient client = Requests.getHttpclient(15000, 15000, 15000, proxyIpInfo);
执行过后,@Cleanup
自动执行 client
的 close
方法,这时关闭了共用的全局静态 ConnectionManager
当其他请求获取 CloseableHttpClient
对象并请求时,ConnectionManager
已经被关闭,遂报出 Connection pool shut down
异常
这一过程速度非常快,导致原来函数执行时间缩短很多。因为一次 http 请求 100ms - 数秒,而检测 ConnectionManager
关闭报错只要几 ms。这进而导致了单位时间内,所有函数的执行次数都飙升的很高。