0%

RestTemplate使用Apache HttpClinet连接池默认大小引发耗时瞬间升高

在Spring项目中,RestTemplate简化了HTTP请求和响应的封装,并且执行了Restful原则。底层HTTP请求由HttpURLConnection,Apache HttpComponentsOkHttp三种实现。最近我们在使用Apache HttpClient作为RestTemplate底层实现时,由于使用不当导致耗时瞬间升高

现象

今天天收到报警说我们有一个服务A的接口TP95瞬间升高,打开监控检查发现流量瞬间升高时,耗时会瞬间升高,如图所示;
图一
打开链路跟踪查看调用链关系,发现该时接口耗时几乎全部耗费在调用下游服务B,如图所示;正常情况下服务B接口TP95耗时在500毫秒以内
图二
而且该服务接口连接超时设置2秒,读取超时设置2秒;所以预期内该接口在4秒内应该结束。

分析

监控检查

  1. 查看服务B的监控发现服务B的耗时一直很稳定,几乎没有波动
  2. 查看服务C对服务B相同接口的调用在该时刻也很稳定
  3. 查看服务A的GC监控,gc最长耗时60ms,也不会引起该问题
  4. 查看网络监控,一切正常

通过监控数据基本确定问题不在服务B,另外Ops工程师反馈近期也没做过任何infrastrucre调整;基本确定问题仍然在服务A,接下来review服务A请求服务B的相关代码

代码分析

服务A代码中用RestTemplate调用服务B的接口, RestTemplate的Bean采用默认注入的Builder来生成,而且设置了连接超时和读取超时。

1
2
3
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.setReadTimeout(Duration.ofMillis(readTimeoutConfig)).setConnectTimeout(Duration.ofMillis(connectTimeoutConfig)).build();
}

以上代码形式RestTemplate底层实现采用了Apache HttpComponents作为HTTP客户端,Apache HttpComponents在初始化过程中会用默认参数初始化连接池,最终代码会执行到

1
2
3
4
5
6
7
8
9
10
11
12
public PoolingHttpClientConnectionManager(
final HttpClientConnectionOperator httpClientConnectionOperator,
final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
final long timeToLive, final TimeUnit tunit) {
super();
this.configData = new ConfigData();
this.pool = new CPool(new InternalConnectionFactory(
this.configData, connFactory), 2, 20, timeToLive, tunit);
this.pool.setValidateAfterInactivity(2000);
this.connectionOperator = Args.notNull(httpClientConnectionOperator, "HttpClientConnectionOperator");
this.isShutDown = new AtomicBoolean(false);
}

在代码第7行构造CPool时传递的常量2表示连接池请求相同域名最大连接数,20表示连接池访问所有域名的最大连接数 。

至此,问题已然清楚,当请求量瞬间升高时服务A访问B的并发量也瞬间增大,此时超过2个并发的HTTP请求只能等待,由于没有设置从连接池获取连接的超时时间,等待会持续到有空闲的HTTP连接为止才会继续发出HTTP请求,这将导致耗时超过设置的HTTP超时时间。

处理

基于以上分析,我们使用自定义的HttpClient传递相关参数即可,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean
public RestTemplate restTemplate() {
PoolingHttpClientConnectionManager connectMgr = new PoolingHttpClientConnectionManager() ;
connectMgr.setDefaultMaxPerRoute(defaultMaxPerRoute);
connectMgr.setMaxTotal(maxTotal);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectMgr)
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
requestFactory.setConnectTimeout(connectTimeoutConfig);
requestFactory.setReadTimeout(readTimeoutConfig);
requestFactory.setConnectionRequestTimeout(connectRequestTimeoutConfig);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}

自定义三个超时时间:
connectTimeout: 建立连接的超时时间
readTimeout: 读取数据的超时时间
connectionRequestTimeout: 从连接池获取连接的超时时间

修改以后上线观察一天即时出现上述问题的高峰瞬间,耗时始终保持平稳。