最近几天在测境碰到一个问题,httpclient 在使用线程池时, 间隔性的出现 NoHttpResponseException 异常。
httpclient org.apache.http.NoHttpResponseException: host:443 failed to respond
用了连接池很多年了, 一搜自己的博客, 竟然没做过一次整理和收藏, 其实大致原因也猜出个八九不离十, 秉承着严谨的态度😄, 还是百度了一下...大致总结出2个原因
1.当服务端由于负载过大等情况发生时,可能会导致在收到请求后无法处理(比如没有足够的线程资源),会直接丢弃链接而不进行处理。此时客户端就会报错:NoHttpResponseException。
解决建议: 重试
2.客户端与服务端建立的请求在服务端已经失效。(例如:服务端 springboot 内置 tomcat 默认 keepAliveTimeout :20s,客户端自定义 keepAliveTimeout :30s,客户端连接池中取出的空闲连接可能已经被服务端失效,再次从连接池拿该失效连接进行请求时,就会报错。)
解决建议:检查并关闭失效连接
问题依然解决, 解决过程中温习的知识还是需要记载下, 方便下次出现问题, 或者自己再次使用和有需要的小伙伴方便查找,
PoolingHttpClientConnectionManager pool = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
// pool max connect
pool.setMaxTotal(maxTotal);
// 设置最大路由
pool.setDefaultMaxPerRoute(defaultMaxPerRoute);
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(connectionRequestTimeout)
.setSocketTimeout(socketTimeout)
.setConnectTimeout(connectTimeout)
.build();
HttpClientUtil.closeableHttpClient = HttpClients.custom()
// 设置连接池管理
.setConnectionManager(pool)
// 设置请求配置
.setDefaultRequestConfig(requestConfig)
//问题一解决方案:设置重试
.setServiceUnavailableRetryStrategy(new DefaultServiceUnavailableRetryStrategy(3, 2000))
//问题二解决方案:调整 keepAliveTimeout,这样无法复用长连接
.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
//问题二解决方案:设置重试次数
.setRetryHandler(new DefaultHttpRequestRetryHandler(3, false))
//问题二解决方案:设置自动关闭过期链接
.evictIdleConnections(30, TimeUnit.SECONDS)
.build();
对于问题二解决方案中evictIdleConnections方法的工作原理感兴趣的同学, 可以查看源码, 这里贴出部分代码, 供参考, 其实自己实现也一样
- 初始化变量 HttpClientBuilder.evictIdleConnections()
public final HttpClientBuilder evictIdleConnections(final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
this.evictIdleConnections = true;
this.maxIdleTime = maxIdleTime;
this.maxIdleTimeUnit = maxIdleTimeUnit;
return this;
}
- 构建逻辑 HttpClientBuilder.build()
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();
}
- 初始化链接处理线程 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;
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;
}
}
});
}
3.上线不久, 线上又出现了偶发502 Bad Gateway异常, 运维找了很久也没发现请求包有啥问题, 连接池各种参数修改也无济于事, 执行一段时间, 就会出现一个, 只好放大招利用重试解决 !
解决方案: 自定义ServiceUnavailableRetryStrategy
查看默认实现源码
@Override
public boolean retryRequest(final HttpResponse response, final int executionCount, final HttpContext context) {
return executionCount <= maxRetries &&
response.getStatusLine().getStatusCode() == HttpStatus.SC_SERVICE_UNAVAILABLE;
}
可以发先, 重试策略很简单, 重试次数小于指定次数, 且返回值是503, 这里我们只需加个返回值502即可, 最后修改代码如下
/**
* 自定义服务重试策略
* @ClassName: MyServiceUnavailableRetryStrategy
* @author wangqinghua
* @date 2024年1月21日 下午8:19:54
*/
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class MyServiceUnavailableRetryStrategy implements ServiceUnavailableRetryStrategy {
private final int maxRetries;
private final long retryInterval;
private final List<Integer> statusCodes;
public MyServiceUnavailableRetryStrategy() {
this(3, 2000, Arrays.asList(HttpStatus.SC_SERVICE_UNAVAILABLE));
}
public MyServiceUnavailableRetryStrategy(final int maxRetries, final int retryInterval, final List<Integer> statusCodes) {
super();
Args.positive(maxRetries, "Max retries");
Args.positive(retryInterval, "Retry interval");
Args.notEmpty(statusCodes, "StatusCodes not empty");
this.maxRetries = maxRetries;
this.retryInterval = retryInterval;
this.statusCodes = statusCodes;
}
@Override
public boolean retryRequest(final HttpResponse response, final int executionCount, final HttpContext context) {
return executionCount <= maxRetries && statusCodes.contains(response.getStatusLine().getStatusCode());
}
@Override
public long getRetryInterval() {
return retryInterval;
}
}