最近工作中使用的HttpClient工具遇到的Connection Reset异常。在客户端和服务端配置不对的时候容易出现问题,下面就是记录一下如何解决这个问题的过程。
出现Connection Reset的原因
1.客户端在读取数据,服务端不再发送新数据(服务器主动关闭了数据)
为什么会出现服务端主动关闭连接?
经过排查线上服务器配置,发现单一个连接空闲时间超过60s,服务器就会将其关闭。如果刚好客户端在使用该连接则客户端就会收到来自服务端的连接复位标志
既然明白了服务端关闭的连接的原因,那为什么客户端会使用空闲时间为60s的连接呢?
排除了HttpClient的配置后发现,项目中的HttpClient使用连接池,虽然设置了池的最大连接数,但是没有配置空闲连接驱逐器(IdleConnectionEvictor)。到这里原因就已经很明朗了,就是httpClient的配置有问题。
解决思路:
如果说服务端会吧空闲时间超过60s的空闲连接关闭掉,导致了connection reset 异常。要解决这个问题,那只要客户端在服务器关闭连接之前把连接关闭掉那就不会出现了。所以按着这个思路我对httpClient的配置进行了修改。
解决方案1:
为HttpClient添加空闲连接驱逐器配置
新加了evictIdleConnections(40, TimeUnit.SECONDS)
配置
HttpClients
.custom()
// 默认请求配置
.setDefaultRequestConfig(customRequestConfig())
// 自定义连接管理器
.setConnectionManager(poolingHttpClientConnectionManager())
// 删除空闲连接时间
.evictIdleConnections(40, TimeUnit.SECONDS)
.disableAutomaticRetries(); // 关闭自动重试
正常情况下到这里问题就解决了,但是现实是线上再次出现了Connection Reset异常。继续排查...
思考:虽然更新配置后再次出现“连接重置”异常,不过出现频率相较于没改之前还是要低不少。所以改的配置还有用的,肯定是什么地方没有配好。为了一探究竟,我翻阅了HttpClient关于IdleConnectionEvictor
驱逐器的源码发现了问题所在。
源码解读:
源码1:
// org.apache.http.impl.client.HttpClientBuilder
public class HttpClientBuilder {
// .....省略无关代码....
// 关注build方法,这这个方法里面启动了空闲连接驱逐器
public CloseableHttpClient build() {
// 。。。。省略代码。。。。
if (!this.connManagerShared) {
if (closeablesCopy == null) {
closeablesCopy = new ArrayList<Closeable>(1);
}
final HttpClientConnectionManager cm = connManagerCopy;
if (evictExpiredConnections || evictIdleConnections) {
// 在这里实例化了IdleConnectionEvictor。maxIdleTime和maxIdleTimeUnit就是我们在配置httpclient时
// 传入的 40 和 TimeUnit.SECONDS
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();
}
}
});
// 调用start()发放启动了线程驱逐器
connectionEvictor.start();
}
closeablesCopy.add(new Closeable() {
@Override
public void close() throws IOException {
cm.shutdown();
}
});
}
// 。。。。省略无关代码。。。。。
}
}
-
evictIdleConnections(40, TimeUnit.SECONDS)
配置的参数在HttpClientBuilder.builder
方法中用于实例化IdleConnectionEvictor
对象的构造参数 -
调用了
connectionEvictor.start()
方法启动了线程驱逐器
源码2:
// org.apache.http.impl.client.IdleConnectionEvictor
public final class IdleConnectionEvictor {
// 。。。。省略无关代码。。。。
// HttpClientBuilder.build()内实例化IdleConnectionEvictor调用了该构造方法
public IdleConnectionEvictor(
final HttpClientConnectionManager connectionManager,
final long sleepTime, final TimeUnit sleepTimeUnit,
final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
this(connectionManager, null, sleepTime, sleepTimeUnit, maxIdleTime, maxIdleTimeUnit);
}
// 。。。。省略无关代码。。。。
}
关键的参数列表
- sleepTime:延时检查时间
- maxIdleTime:最多空闲时间
结合源码1和源码2,可以看到在构造IdleConnectionEvictor
时sleepTime
和maxIdleTime
为同一个值40秒,在这里还看不出什么问题,继续。
源码3:
// org.apache.http.impl.client.IdleConnectionEvictor
public final class IdleConnectionEvictor {
// 省略无关代码
// 重载的构造方法
public IdleConnectionEvictor(
final HttpClientConnectionManager connectionManager,
final ThreadFactory threadFactory,
final long sleepTime, final TimeUnit sleepTimeUnit,
final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
this.connectionManager = Args.notNull(connectionManager, "Connection manager");
this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();
this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;
this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;
// 使用threadFactory线程构造器构造了一个守护线程
this.thread = this.threadFactory.newThread(new Runnable() {
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
// 挂起线程时间是我们传入的时间40秒
Thread.sleep(sleepTimeMs);
// 执行检查代码,关闭过期连接
connectionManager.closeExpiredConnections();
if (maxIdleTimeMs > 0) {
// 关闭超过空闲时间的空闲连接,参数传入我们配置的40秒
connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);
}
}
} catch (final Exception ex) {
exception = ex;
}
}
});
}
// HttpClientBuilder中调用的start()方法
public void start() {
thread.start();
}
}
通过源码3我们可以看到,检查线程的执行周期时间和最大过期时间都是我们传入的40秒。在这里停顿一下思考一下,服务器的空闲连接关闭时间是60s,我们配置的时间是40s,那这样配置会不有出现什么问题?
线程相隔40s执行一下回收任务,那在不执行回收任务的停止的40秒里面出了connection reset异常了怎么吧?问题就明了。
问题复现时序:
- 00:00:00 --- 启动
IdleConnectionEvictor.start()
,挂起检查线程,不执行检查代码 - 00:00:10 --- 10秒后的连接池新建了一个连接
- 00:00:12 --- 连接耗时2s,用完后返回线程池,假设之后都没有再被使用了
- 00:00:40 --- 第一次sleep挂起时间到期,执行检查任务。发现没有过期连接,下一次回收任务发生在 00:01:20
- 00:01:12 --- 这时恰好客户端使用那个空闲的连接,服务端关闭了该连接。在这里爆发了connection reset 异常
- 00:01:20 --- 第二次sleep挂起时间到期,执行检查任务。
结论:
服务端空闲连接关闭时间是60s,我们客户端配置的最大空闲时间值应该小于30s才能避免这个问题
解决方案2:
在解决方案1的基础上,把40s时间改为20s,顺利解决了该问题。