在高并发的HTTP调用场景下,尝试优化 RestTemplate的http连接相关参数,比如超时时间,连接池配置,空闲连接回收等。
项目中也遇到了NoHttpResponseException异常,针对这些情况,尝试着做了优化,分享个完整的示例给大家参考。
项目中每隔几个小时就会出现一些这样的异常:
org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://xxx.yyy.com/zzz": http://xxx.yyy.com:80 failed to respond; nested exception is org.apache.http.NoHttpResponseException: http://xxx.yyy.com:80 failed to respond
at org.springframework.web.client.RestTemplate.doExecute$original$GahflF7J(RestTemplate.java:696)
at org.springframework.web.client.RestTemplate.doExecute$original$GahflF7J$accessor$F3E1NXY9(RestTemplate.java)
at org.springframework.web.client.RestTemplate$auxiliary$yfdQHtiq.call(Unknown Source)
at org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstMethodsInter.intercept(InstMethodsInter.java:86)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java)
项目是springboot的,这里直接贴上RestTemplateConfig类的代码:
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.Header;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.slf4j.LoggerFactory.getLogger;
@Configuration
public class RestTemplateConfig {
private final Logger logger = getLogger(this.getClass());
//可以在yml或者nacos中配置此值,不配默认为200
@Value("${restTemplate.pool.max:200}")
private int poolMax;
public RestTemplateConfig() {
}
/**
* http连接管理器: 针对单个http connection的池化配置
*/
@Bean
public HttpClientConnectionManager poolingHttpClientConnectionManager() {
/*// 注册http和https请求
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build();
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(registry);*/
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
// 连接池里面的最大连接数
poolingHttpClientConnectionManager.setMaxTotal(poolMax);
// 单个路由的默认最大并发数(即允许同一个host:port最多有几个活跃连接)
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(poolMax);
// 指定某个路由的最大连接数,示例:
/*HttpHost localhost = new HttpHost("www.baidu.com", 80);
poolingHttpClientConnectionManager.setMaxPerRoute(new HttpRoute(localhost), 50);*/
//在从连接池获取连接时,连接不活跃多长时间后需要进行一次验证,默认为2s
//poolingHttpClientConnectionManager.setValidateAfterInactivity(2_000);
//自定义线程,用于关闭池中失效的http连接
Thread evictThread = new Thread(new IdleConnectionEvictor(poolingHttpClientConnectionManager));
evictThread.setDaemon(true);
evictThread.start();
return poolingHttpClientConnectionManager;
}
/**
* 单个http connection的参数配置
*/
@Bean
public HttpClient httpClient(HttpClientConnectionManager poolingHttpClientConnectionManager) {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
// 设置http连接管理器
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager)
//httpClient共享模式,这个共享是指与其它httpClient是否共享
.setConnectionManagerShared(false)
//回收过期连接
.evictExpiredConnections()
//回收空闲连接
.evictIdleConnections(10, TimeUnit.SECONDS)
//连接存活时间,如果不设置,则根据长连接信息决定
.setConnectionTimeToLive(20, TimeUnit.SECONDS)
//长连接Keep-Alive设置
.setKeepAliveStrategy(((response, context) -> Duration.ofSeconds(20).toMillis()))
//禁用重试次数
.setRetryHandler(new DefaultHttpRequestRetryHandler(0, false));
// 设置默认请求头
List<Header> headers = new ArrayList<>();
headers.add(new BasicHeader("Connection", "Keep-Alive"));
httpClientBuilder.setDefaultHeaders(headers);
CloseableHttpClient closeableHttpClient = httpClientBuilder.build();
//JVM停止或重启时,关闭连接池释放掉连接
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
logger.info("ShutdownHook --> closing http client : {}", closeableHttpClient.toString());
closeableHttpClient.close();
logger.info("ShutdownHook --> closed http client");
} catch (Exception e) {
logger.error("ShutdownHook closing http client error={}", ExceptionUtils.getStackTrace(e));
}
}
});
return closeableHttpClient;
}
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) {
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
// httpClient创建器
clientHttpRequestFactory.setHttpClient(httpClient);
// 连接超时时间/毫秒(连接上服务器(握手成功)的时间,超出抛出connect timeout)
clientHttpRequestFactory.setConnectTimeout(5 * 100);
// 数据读取超时时间(socketTimeout)/毫秒(务器返回数据(response)的时间,超过抛出read timeout)
clientHttpRequestFactory.setReadTimeout(1000);
// 连接池获取请求连接的超时时间,不宜过长,必须设置/毫秒(超时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool)
clientHttpRequestFactory.setConnectionRequestTimeout(1000);
return clientHttpRequestFactory;
}
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) {
// boot中可使用RestTemplateBuilder.build创建
RestTemplate restTemplate = new RestTemplate();
// 配置请求工厂
restTemplate.setRequestFactory(clientHttpRequestFactory);
return restTemplate;
}
public static class IdleConnectionEvictor extends Thread {
private final Logger logger = getLogger(this.getClass());
private final HttpClientConnectionManager connMgr;
private volatile boolean stop;
public IdleConnectionEvictor(HttpClientConnectionManager connMgr) {
this.connMgr = connMgr;
}
@Override
public void run() {
try {
while (!stop) {
synchronized (this) {
wait(5000);
logger.debug("IdleConnectionEvictor --> 每隔5秒执行一次,关闭过期和空闲超过30秒的http连接");
//关闭失效链接
connMgr.closeExpiredConnections();
//关闭空闲链接
connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
}
}
} catch (InterruptedException ex) {
logger.error("IdleConnectionEvictor thread stop --> catched InterruptedException:{}", ExceptionUtils.getStackTrace(ex));
shutdown();
}
}
public void shutdown() {
stop = true;
synchronized (this) {
notifyAll();
}
}
}
}