RestTemplate超时引发的血案

简介:

最近线上出了一次故障,收银台系统所有服务全部假死。订单量瞬时下降,造成很大损失。

故障总结,导致问题的原因有两方面:

数据库慢查询

 ●   RestTemplate超时时间设置不生效。

 ●  spring-web不同版本设置RestTemplate方式不完全一样。

默认超时设置

默认情况下是没有超时设置的,此时超时依赖两方面:

依赖TCP连接本身的超时时间(tcp空闲连接,超过一定时间,连接会被关闭)。 请求所经过的网络节点的超时时间。e.g. 中间经过nginx, nginx默认读取后端服务的超时时间是60s,所以超时时间在60s左右(日志显示稍微大一点,不会大很多)。

代码分析

例子

 
  1. long start = System.currentTimeMillis();

  2. try {

  3. RestTemplate restTemplate = new RestTemplate();

  4. Map responseObject = restTemplate.getForObject(url, Map.class);

  5. System.out.println(responseObject);

  6. } catch (Exception e) {

  7. Assert.assertNotNull(e);

  8. System.out.println("timeout = " + (System.currentTimeMillis() - start));

  9. }

原因: RestTemplate继承自 HttpAccessor, 默认使用的 ClientHttpRequestFactory是 SimpleClientHttpRequestFactory

 
  1. public abstract class HttpAccessor {

  2. /**

  3. * Logger available to subclasses.

  4. */

  5. protected final Log logger = LogFactory.getLog(getClass());

  6. private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();

  7. }

  8. public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory {

  9. private static final int DEFAULT_CHUNK_SIZE = 4096;

  10. private Proxy proxy;

  11. private boolean bufferRequestBody = true;

  12. private int chunkSize = DEFAULT_CHUNK_SIZE;

  13. // 连接和读取超时都是 -1, 也就是没有超时设置。

  14. private int connectTimeout = -1;

  15. private int readTimeout = -1;

  16. }

那么我们使用 RestTemplate该如何设置超时时间呢?

RestTemplate超时设置

由上面的代码我们了解到,超时设置其实应该通过内部的 ClientHttpRequestFactory 来设置的。

所以就可以通过给 RestTemplate设置一个我们自己创建的,设置了超时时间的 ClientHttpRequestFactory来实现。

 
  1. SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory();

  2. clientHttpRequestFactory.setConnectTimeout(1000);

  3. clientHttpRequestFactory.setReadTimeout(50);

  4. RestTemplate restTemplate = new RestTemplate();

  5. restTemplate.setRequestFactory(clientHttpRequestFactory);

或者

 
  1. HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();

  2. clientHttpRequestFactory.setConnectTimeout(1000);

  3. clientHttpRequestFactory.setReadTimeout(50);

  4. RestTemplate restTemplate = new RestTemplate();

  5. restTemplate.setRequestFactory(clientHttpRequestFactory);

但是要注意的是: HttpComponentsClientHttpRequestFactory底层使用了apache的 HttpClient,超时时间的设置其实是针对它进行设置的。

HttpComponentsClientHttpRequestFactory

 
  1. private static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 100;

  2. private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 5;

  3. //默认读取超时 60s

  4. private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000);

  5. private HttpClient httpClient;

  6. /**

  7. * Set the connection timeout for the underlying HttpClient.

  8. * A timeout value of 0 specifies an infinite timeout.

  9. * @param timeout the timeout value in milliseconds

  10. */

  11. public void setConnectTimeout(int timeout) {

  12. Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");

  13. getHttpClient().getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);

  14. }

到此,如果就通过上面提到的方式设置超时时间,那么我们的应用就不用有超时问题,也不会发生故障了。

但问题就发生在,公司内部使用的组件,不是通过 HttpComponentsClientHttpRequestFactory 设置超时时间,而是通过设置 HttpComponentsClientHttpRequestFactory内部的 HttpClient设置的超时时间,并且设置了 HttpClient使用的 HttpClientConnectionManager,从而导致了问题的发生。

问题代码&测试

 
  1. @Test

  2. public void testRestTemplateWithRequestFactoryWithoutTimeOut() {

  3. long start = System.currentTimeMillis();

  4. try {

  5. HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();

  6. //2.设置超时时间, 设置/不设置ConnectionManager

  7. HttpClient httpClient = HttpClientBuilder.create()

  8. .setDefaultRequestConfig(getRequestConfig())

  9. .setDefaultSocketConfig(getSocketConfig())

  10. .setConnectionManager(new PoolingHttpClientConnectionManager(3, TimeUnit.MINUTES))

  11. .build();

  12. requestFactory.setHttpClient(httpClient);

  13. RestTemplate restTemplate = new RestTemplate();

  14. restTemplate.setRequestFactory(requestFactory);

  15. Map responseObject = restTemplate.getForObject(QUERY_USER_RENEW_URL, Map.class);

  16. System.out.println(responseObject);

  17. } catch (Exception e) {

  18. Assert.assertNotNull(e);

  19. System.out.println("timeout = " + (System.currentTimeMillis() - start));

  20. }

  21. }

结论
spring-web 版本 3.2.0
 ●   默认超时 60s, 因为nginx默认的proxyreadtimeout 是60s
 ●   设置了 HttpClient的超时时间, 不设置 ConnectionManager 超时生效

 ●  设置了 HttpClient的超时时间, 设置 ConnectionManager 超时生效

spring-web 版本 4.0.9.RELEASE

 ●   默认超时 60s, 因为nginx默认的proxyreadtimeout 是60s
 ●   设置了 HttpClient的超时时间, 不设置 ConnectionManager 超时生效

 ●  设置了 HttpClient的超时时间, 设置 ConnectionManager 超时不生效 (qiyue-store 就是这样问题)

spring-web 版本 4.3.0.RELEASE

 ●   默认超时 60s, 因为nginx默认的proxyreadtimeout 是60s
 ●   设置了 HttpClient的超时时间, 不设置 ConnectionManager 超时生效

 ●  设置了 HttpClient的超时时间, 设置 ConnectionManager 超时生效

spring-web 版本 4.3.11.RELEASE

 ●   默认超时 60s, 因为nginx默认的 proxyreadtimeout 是60s
 ●   设置了 HttpClient的超时时间, 不设置 ConnectionManager 超时生效

 ●  设置了 HttpClient的超时时间, 设置 ConnectionManager 超时生效 其实问题就在与不同的版本中 HttpComponentsClientHttpRequestFactory.createRequest 方法的实现逻辑不同。如何不同,自己查看。:grin:

总结

超时设置至关重要。外部依赖接口调用可以通过Hystrix进行包装。 任何参数的设置都需要验证是否可以正常工作,可以加入到测试环节中,方便在不同的依赖版本中进行验证。


原文发布时间为:2018-11-23

本文来自云栖社区合作伙伴“Java杂记”,了解相关信息可以关注“Java杂记”。

相关文章
|
缓存 NoSQL 中间件
【Sa-Token】6、Sa-Token集成Redis
Sa-Token 支持 Redis、Memcached 等专业的缓存中间件中, 做到重启数据不丢失,而且保证分布式环境下多节点的会话一致性
3202 0
|
3月前
|
人工智能 前端开发 JavaScript
AI Agent(智能体)的输出格式应该从 Markdown 转向 HTML吗?
Anthropic工程师提出AI输出应从Markdown转向HTML,强调其高信息密度、交互性与易分享优势;但HTML存在Token消耗大、Diff困难等短板。未来更可能是“分工协作”:HTML主导前端交互(UI/Artifact),Markdown坚守后端通信(Prompt/知识结构),二者共存演进。
307 0
|
网络安全
Caused by: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://xxxx.svc.cluster.local:8080/xxxx": Connection reset; nested exception is java.net.SocketException: Connection reset 什么原因导致得
Caused by: org.springframework.web.client.ResourceAccessException: I/O error on POST request for "xxxx.svc.cluster.local:8080/xxxx ": Connection reset; nested exception is java.net.SocketException: Connection reset 什么原因导致得
5372 0
|
存储 NoSQL 物联网
MongoDB应用场景
MongoDB应用场景
|
SQL Java 数据库连接
mybatisplus QueryWrapper or写法
# MyBatis-Plus QueryWrapper的OR写法详解 MyBatis-Plus是一款基于MyBatis的增强工具,提供了丰富的简化操作,使开发者能更高效地进行数据库操作。`QueryWrapper`是MyBatis-Plus中用于构建查询条件的一个核心类,支持多种条件组合,包括AND和OR条件。本文将详细介绍如何使用 `QueryWrapper`实现OR条件的查询。 ## QueryWrapper简介 `QueryWrapper`用于构建动态SQL查询条件,它封装了各种条件构造方法,使得查询条件的构建更加简洁和直观。`QueryWrapper`中提供了丰富的方法来支持多
3260 0
|
存储 安全 API
GitHub代码删了也无用,任何人仍可永久访问?!微软:这不是Bug而是有意设计...
开源安全公司Truffle Security发现,GitHub上的数据删除可能只是表面现象,实际上被删的数据仍可被访问。这一发现震惊了开源社区。研究人员引入了“跨分叉对象引用”(CFOR)这一概念,描述了如何通过已删除或私有fork访问敏感数据。即便存储库被删除,提交的数据仍可通过fork存取,甚至私有存储库的数据也可能被公开访问。尽管GitHub回应称这是有意为之的设计,但对于许多用户来说,这打破了对数据隐私的基本期望。此发现不仅影响GitHub用户,还可能波及其他版本控制系统。
1012 4
|
存储 搜索推荐 Java
如何在Java中使用Elasticsearch
如何在Java中使用Elasticsearch
深入理解Spring Boot中的配置加载顺序
深入理解Spring Boot中的配置加载顺序