在Spring项目中,RestTemplate简化了HTTP请求和响应的封装,并且执行了Restful原则。底层HTTP请求由HttpURLConnection
,Apache HttpComponents
和OkHttp
三种实现。最近我们在使用Apache HttpClient作为RestTemplate底层实现时,由于使用不当导致耗时瞬间升高
现象 今天天收到报警说我们有一个服务A的接口TP95瞬间升高,打开监控检查发现流量瞬间升高时,耗时会瞬间升高,如图所示; 打开链路跟踪查看调用链关系,发现该时接口耗时几乎全部耗费在调用下游服务B,如图所示;正常情况下服务B接口TP95耗时在500毫秒以内 而且该服务接口连接超时设置2秒,读取超时设置2秒;所以预期内该接口在4秒内应该结束。
分析 监控检查
查看服务B的监控发现服务B的耗时一直很稳定,几乎没有波动
查看服务C对服务B相同接口的调用在该时刻也很稳定
查看服务A的GC监控,gc最长耗时60ms,也不会引起该问题
查看网络监控,一切正常
通过监控数据基本确定问题不在服务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
表示连接池访问所有域名的最大连接数 。
至此,问题已然清楚,当zeus-order请求量瞬间升高时zeus-order访问zeus的并发量也瞬间增大,如果超过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 : 从连接池获取连接的超时时间
修改以后上线观察一天即时出现上述问题的高峰瞬间,耗时始终保持平稳。