结论陈述
多人开发,创建了多个定时任务用来推送二三十种业务数据,同时也为部分业务数据做了及时推送的机制,采用的restemplate工具类,在一段时间之内 ,系统运行得很正常,突然有一天,测试发现断网之后,很多及时推送的数据就不没有出现在接收方的系统里面,就像连锁反应一样,经排查,原因如下
- restemplate调用接口前,线程能打印出日志,开始调用接口后,就没有输出日志,也就是没有响应消息,甚至异常也捕获不到,此现象断定restemplate调用接口出现了阻塞
- 配置了restemplate的超时时间 ,竟然没有生效,找出失效的原因后,成功捕获到超时异常
问题现象
观察某一次RestTemplate 发起的POST请求卡死
原因分析
一开始怀疑是线程池的可用线程数量不够,加大核心线程池数量、最大线程数量和线程队列数量后,发现阻塞问题依旧。
在查询资料后,发现restemplate是单线程,可能出现阻塞,反思一下,如果配置了超时机制且生效了,那么在第一个接口调用请求之后,就应该正常触发第二个接口调用才对,实际上我们第一个请求都没有闭环,原理上说不通。
当然restemplate的单线程设计也曾质疑过【 Spring 异步HTTP AsyncRestTemplate介绍_zzhongcy的博客-CSDN博客_resttemplate异步还是同步的】,也尝试使用 AsyncRestTemplate改造 RestTemplate,发现 AsyncRestTemplate已经过时了,就不再继续了。
RestTemplate的异步兄弟AsyncRestTemplate。
在 Spring 3 时代,为了能更优雅地实现HTTP调用,引入了 RestTemplate,其中提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率。
在 Spring 4 时代,为了能实现异步地HTTP调用,引入了AsyncRestTemplate,使得编写异步代码和同步代码一样简单。
AsyncRestTemplate 是 Spring中提供异步的客户端HTTP访问的核心类。与RestTemplate类相似,它提供了一些类似的方法,只不过返回类型不是具体的结果,而是ListenableFuture包装类。
在定位到是restemplate阻塞问题后,开始走查代码,将封装了restemplate的工具类,拿来做单元测试,复现了阻塞问题,这样的做法还是非常有必要,因为很多时候代码不是自己写的,别人留下坑,都复现不出来的。
本人曾一度怀疑是restemplate版本不对导致的restemplate本身的超时机制不生效,然则本人使用与项目中同版本的restemplate进行测试后,发现超过机制是正常的,虽然浪费了时间,好在也排查除了一种可能性。
**千算万算没想到啊,在代码中发现竟然以前有人在配置restemplate支持重定向功能时,把已经配置过的超时赶时间,重新覆盖为空了,直接导致超时机制失效,**而
restTemplate用的是直接new的,未重写连接池也未设置超时时间。看源码得知底层用的jdk的httpurlconnection,若readTimeOut和connectionTimeOut没有设置,那请求是没有超时时间的,导致请求一直hang住。
restTemplate超时时间引发的生产事故 - 编程猎人 (programminghunter.com)
问题代码
采用debug走查代码时,在方法【supportRedirectUrl】中创建的HttpComponentsClientHttpRequestFactory,直接将方法【httpComponentsClientHttpRequestFactory】中配置过的超时时间覆盖掉了
@Bean
public RestTemplate restTemplate(HttpComponentsClientHttpRequestFactory httpFactory) {
RestTemplate restTemplate = new RestTemplate(httpFactory);
restTemplate.setErrorHandler(new ResponseErrorHandler() {
@Override
public boolean hasError(ClientHttpResponse clientHttpResponse) {
return false;
}
@Override
public void handleError(ClientHttpResponse clientHttpResponse) {
}
});
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.TEXT_PLAIN);
mediaTypes.add(MediaType.TEXT_HTML);
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(mediaTypes);
restTemplate.getMessageConverters().add(converter);
//添加重定向支持
this.supportRedirectUrl(httpsFactory, restTemplate);
return restTemplate;
}
/**
* 添加重定向支持
*
* @param restTemplate
*/
private void supportRedirectUrl(RestTemplate restTemplate) {
TrustStrategy acceptingTrustStrategy = (x509Certificates, authType) -> true;
SSLContext sslContext = null;
try {
sslContext =
SSLContexts
.custom()
.loadTrustMaterial(null, acceptingTrustStrategy)
.build();
} catch (
NoSuchAlgorithmException | KeyManagementException | KeyStoreException e
) {
e.printStackTrace();
}
SSLConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(
sslContext,
new NoopHostnameVerifier()
);
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
HttpClient httpClient = HttpClientBuilder
.create()
.setRedirectStrategy(new LaxRedirectStrategy())
.setSSLSocketFactory(connectionSocketFactory)
.build();
factory.setHttpClient(httpClient);
restTemplate.setRequestFactory(factory);
}
@Bean(name = "httpFactory")
public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory()
throws Exception {
RequestConfig defaultRequestConfig = RequestConfig
.custom()
.setConnectTimeout(ConnectTimeout)
.setSocketTimeout(socketTimeout)
.setConnectionRequestTimeout(ConnectTimeout)
.build();
CloseableHttpClient closeableHttpClient = HttpClients
.custom()
.setDefaultRequestConfig(defaultRequestConfig)
.build();
HttpComponentsClientHttpRequestFactory httpFactory = new HttpComponentsClientHttpRequestFactory(
closeableHttpClient
);
httpFactory.setReadTimeout(readTimeout);
httpFactory.setConnectTimeout(ConnectTimeout);
httpFactory.setConnectionRequestTimeout(connectionRequestTimeout);
httpFactory.setBufferRequestBody(false);
return httpFactory;
}
总结一句
项目过程中,往往都是多人协作,意料之外的问题往往不是开发语言的问题,反而人为制造问题更为隐匿,遇到这种不可控的情况 ,平时开发工作过程中,做好code review就显得尤其至关重要,做系统,也好比万丈高楼平地起,在团队成员能力不成熟时,做好核心代码管控+代码评审缺一不可,每个人的一砖一瓦都应当对个人负责,对项目负责,一时的无所谓,欠的“债”,逃不掉,总要还。
业务背景
项目中要求向A系统推送存在多种类型数据,代码中以创建多个定时任务来且异步的方式去推送成千上万的数据。
为了保障数据推送,采用方式有
- 选择及时性要求很高的部分数据采用及时和定时推送
- 推送服务启动时,推送一次的全部数据
代码实现
- 底层采用restemplate调用A系统的接口
- application.properties配置线程池初始化参数