RestTemplate超時引發的血案

技術小能手發表於2018-11-23

最近線上出了一次故障,收銀臺系統所有服務全部假死。訂單量瞬時下降,造成很大損失。

故障總結,導致問題的原因有兩方面:

資料庫慢查詢

 ●  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雜記”。


相關文章