RestTemplate超时引发的血案

前言

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

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

  1. 数据库慢查询
  2. RestTemplate超时时间设置不生效。
  3. spring-web不同版本设置RestTemplate方式不完全一样。

默认超时设置

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

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

代码分析

例子
1
2
3
4
5
6
7
8
9
long start = System.currentTimeMillis();
try {
RestTemplate restTemplate = new RestTemplate();
Map responseObject = restTemplate.getForObject(url, Map.class);
System.out.println(responseObject);
} catch (Exception e) {
Assert.assertNotNull(e);
System.out.println("timeout = " + (System.currentTimeMillis() - start));
}

原因:

RestTemplate 继承自 HttpAccessor, 默认使用的ClientHttpRequestFactorySimpleClientHttpRequestFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public abstract class HttpAccessor {

true/**
* Logger available to subclasses.
*/
trueprotected final Log logger = LogFactory.getLog(getClass());

trueprivate ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
}

public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory {

trueprivate static final int DEFAULT_CHUNK_SIZE = 4096;


trueprivate Proxy proxy;

trueprivate boolean bufferRequestBody = true;

trueprivate int chunkSize = DEFAULT_CHUNK_SIZE;

// 连接和读取超时都是 -1, 也就是没有超时设置。
trueprivate int connectTimeout = -1;
trueprivate int readTimeout = -1;
}

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

RestTemplate超时设置

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

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

1
2
3
4
5
SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory();
clientHttpRequestFactory.setConnectTimeout(1000);
clientHttpRequestFactory.setReadTimeout(50);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(clientHttpRequestFactory);

或者

1
2
3
4
5
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
clientHttpRequestFactory.setConnectTimeout(1000);
clientHttpRequestFactory.setReadTimeout(50);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(clientHttpRequestFactory);

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

HttpComponentsClientHttpRequestFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

private static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 100;

private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 5;
//默认读取超时 60s
private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000);

private HttpClient httpClient;

/**
* Set the connection timeout for the underlying HttpClient.
* A timeout value of 0 specifies an infinite timeout.
* @param timeout the timeout value in milliseconds
*/
public void setConnectTimeout(int timeout) {
trueAssert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
truegetHttpClient().getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
}

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

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

问题代码&测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void testRestTemplateWithRequestFactoryWithoutTimeOut() {
long start = System.currentTimeMillis();
try {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();

//2.设置超时时间, 设置/不设置ConnectionManager
HttpClient httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(getRequestConfig())
.setDefaultSocketConfig(getSocketConfig())
.setConnectionManager(new PoolingHttpClientConnectionManager(3, TimeUnit.MINUTES))
.build();

requestFactory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(requestFactory);

Map responseObject = restTemplate.getForObject(QUERY_USER_RENEW_URL, Map.class);
System.out.println(responseObject);
} catch (Exception e) {
Assert.assertNotNull(e);
System.out.println("timeout = " + (System.currentTimeMillis() - start));
}
}

结论

spring-web 版本 3.2.0

  1. 默认超时 60s, 因为nginx默认的proxy_read_timeout 是60s
  2. 设置了 HttpClient的超时时间, 不设置 ConnectionManager 超时生效
  3. 设置了 HttpClient的超时时间, 设置 ConnectionManager 超时生效

spring-web 版本 4.0.9.RELEASE

  1. 默认超时 60s, 因为nginx默认的proxy_read_timeout 是60s
  2. 设置了 HttpClient的超时时间, 不设置 ConnectionManager 超时生效
  3. 设置了 HttpClient的超时时间, 设置 ConnectionManager 超时不生效 (qiyue-store 就是这样问题)

spring-web 版本 4.3.0.RELEASE

  1. 默认超时 60s, 因为nginx默认的proxy_read_timeout 是60s
  2. 设置了 HttpClient的超时时间, 不设置 ConnectionManager 超时生效
  3. 设置了 HttpClient的超时时间, 设置 ConnectionManager 超时生效

spring-web 版本 4.3.11.RELEASE

  1. 默认超时 60s, 因为nginx默认的 proxy_read_timeout 是60s
  2. 设置了 HttpClient的超时时间, 不设置 ConnectionManager 超时生效
  3. 设置了 HttpClient的超时时间, 设置 ConnectionManager 超时生效

其实问题就在与不同的版本中HttpComponentsClientHttpRequestFactory.createRequest方法的实现逻辑不同。如何不同,自己查看。😁

总结

  1. 超时设置至关重要。外部依赖接口调用可以通过Hystrix进行包装。
  2. 任何参数的设置都需要验证是否可以正常工作,可以加入到测试环节中,方便在不同的依赖版本中进行验证。
文章目录
  1. 1. 前言
  2. 2. 默认超时设置
    1. 2.1. 代码分析
  3. 3. RestTemplate超时设置
  4. 4. 问题代码&测试
  5. 5. 结论
    1. 5.1. spring-web 版本 3.2.0
    2. 5.2. spring-web 版本 4.0.9.RELEASE
    3. 5.3. spring-web 版本 4.3.0.RELEASE
    4. 5.4. spring-web 版本 4.3.11.RELEASE
  6. 6. 总结
|