后端部署了新的代码,在几分钟后似乎所有的爬取相关的服务均不可用。一服务频繁报错提示:「连接超时: 当前过于拥挤」。
首先立即打开日志查看错误消息,显示:Timeout waiting for connection from pool ,字面意思是从连接池中等待超时。为了了解这个时候的连接池状态如何,在查找资料后,使用以下的代码在出现异常的同时打印当前连接池状态。
for(HttpRoute route : customConnectionPool.getRoutes()){
logger.info("{} :: {}", route.getTargetHost().getHostName() ,customConnectionPool.getStats(route));
}
连接池每个路由(可以认为一个Host:IP为一个路由/Route)的状态由PoolStats这个类表示。这个类有四个字段用于表示该路由对应状态下的连接,每个字段对应一种状态。
“租用”,即表示当前正在被使用的连接。如果这个状态长期保持一个较高的数量,尤其是只增不减,应该要考虑到是连接用完后没有释放的问题了(后话)。
“等待”,表示有请求在等待连接池可用连接,等待超时时间可以在RequestConfig.custom()
中使用setConnectionRequestTimeout
设置(单位为ms),超时后即抛出题示异常。
“可用”,表示空闲的持久连接。根据源码文档,该路由的当前总存在连接数=leased+available。
表示路由最大可以创建的连接数,这个数量取决于PoolingHttpClientConnectionManager
的setDefaultMaxPerRoute
值,或者是由HttpClients.custom()
的setMaxConnPerRoute
决定。
最初代码是这样写的:
customConnectionPool = new PoolingHttpClientConnectionManager();
customConnectionPool.setDefaultMaxPerRoute(1500);
看起来似乎没什么问题。因为最初我认为连接池的最大连接数至少是等于DefaultMaxPerRoute
的。之后查看日志发现出现了大量处于pending状态的请求,表示在等待连接池空闲,说明确实发生了大量的阻塞。
hostA[leased: 20; pending: 217; available: 0; max: 1500]
hostB[leased: 0; pending: 15; available: 0; max: 1500]
hostC[leased: 0; pending: 0; available: 0; max: 1500]
hostD[leased: 0; pending: 28; available: 0; max: 1500]
hostE[leased: 0; pending: 388; available: 0; max: 1500]…
这里观察到hostA[leased: 20,说明问题可能出现在hostA上面,先尝试模拟获取一下连接池的最大连接数。
public static void main(String[] args) {
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setDefaultMaxPerRoute(1500);
System.out.println(cm.getTotalStats());
}
[leased: 0; pending: 0; available: 0; max: 20]
答案是20。因此即使设置了DefaultMaxPerRoute
,也需要使用setMaxTotal
设置连接池最大连接数量。
既然连接池太小了,首先就是扩大连接池的MaxTotal,扩大连接池后也重现了连接的堆积。
hostA [leased: 255; pending: 0; available: 1; max: 5000]
也确实修改了HostA爬取相关的代码,发现在修改的登录这一段,没有及时释放掉连接。
于是立马使用HttpClientUtils.closeQuietly(httpClient)
关闭连接,结果发现他把整个连接池给关了。。。
好吧,应该使用的是HttpRequestBase的releaseConnection()
来释放掉这个连接。
再配合一下HttpClientUtils.closeQuietly(httpResponse)
(注意不是httpClient,与EntityUtils.consumeQuietly()
效果相同)把流给关了。
加上这两句后确认问题得到了解决。
还有一个问题是,为何只有某个服务(hostC)在报错呢。查看日志,发现该服务报错时的耗时与setConnectionRequestTimeout的数值相当。看了一下RequestConfig
的设置,发现默认情况下三个超时值都是-1,也就是永不超时。
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setConnectionRequestTimeout(1000)
.setSocketTimeout(8000)
.build();
setConnectionRequestTimeout设置从连接池中取出连接的时限,设置这个可以防止产生大量处于等待状态下的连接阻塞整个后端。
setConnectTimeout设置向服务器发起连接的超时时间。
setSocketTimeout设置从服务器获取响应数据的超时时间。
最好还是设置一下各种服务的爬取时间吧。
遇到这个问题,通常大家都这么想:能不能开个线程,定时把无用的连接给关掉呢。搜索了一些资料后,发现有人确实手写了个线程,不过这部分其实并不需要自己写。
在配置HttpClient的时候,使用.evictExpiredConnections()
可以配置一个线程自动关闭过期的连接,使用.evictIdleConnections(...)
可以配置自动关闭空闲超过指定时限的连接,看一下源码:
if (evictExpiredConnections || evictIdleConnections) {
final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,
maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS,
maxIdleTime, maxIdleTimeUnit);
closeablesCopy.add(new Closeable() {
@Override
public void close() throws IOException {
connectionEvictor.shutdown();
try {
connectionEvictor.awaitTermination(1L, TimeUnit.SECONDS);
} catch (final InterruptedException interrupted) {
Thread.currentThread().interrupt();
}
}
});
connectionEvictor.start();
}
默认每10秒执行一次,在connectionEvictor里面具体干了什么呢:
this.thread = this.threadFactory.newThread(new Runnable() {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
Thread.sleep(sleepTimeMs);
connectionManager.closeExpiredConnections();
if (maxIdleTimeMs > 0) {
connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);
}
}
} catch (final Exception ex) {
exception = ex;
}
}
});
关键就是这句connectionManager.closeExpiredConnections()
和connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS)
实现了关闭过期连接和空闲超过指定时限的连接。
https://www.jianshu.com/p/85b554e989c1
https://www.codenong.com/jsecc34e182526/
https://blog.csdn.net/QH_JAVA/article/details/78673960