RestTemplate超時引發的血案
最近線上出了一次故障,收銀臺系統所有服務全部假死。訂單量瞬時下降,造成很大損失。
故障總結,導致問題的原因有兩方面:
資料庫慢查詢
● RestTemplate超時時間設定不生效。
● spring-web不同版本設定RestTemplate方式不完全一樣。
預設超時設定
預設情況下是沒有超時設定的,此時超時依賴兩方面:
依賴TCP連線本身的超時時間(tcp空閒連線,超過一定時間,連線會被關閉)。
請求所經過的網路節點的超時時間。e.g. 中間經過nginx, nginx預設讀取後端服務的超時時間是60s,所以超時時間在60s左右(日誌顯示稍微大一點,不會大很多)。
程式碼分析
例子
-
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, 預設使用的 ClientHttpRequestFactory是 SimpleClientHttpRequestFactory
-
public abstract class HttpAccessor {
-
/**
-
* Logger available to subclasses.
-
*/
-
protected final Log logger = LogFactory.getLog(getClass());
-
private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
-
}
-
public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory {
-
private static final int DEFAULT_CHUNK_SIZE = 4096;
-
private Proxy proxy;
-
private boolean bufferRequestBody = true;
-
private int chunkSize = DEFAULT_CHUNK_SIZE;
-
// 連線和讀取超時都是 -1, 也就是沒有超時設定。
-
private int connectTimeout = -1;
-
private int readTimeout = -1;
-
}
那麼我們使用 RestTemplate該如何設定超時時間呢?
RestTemplate超時設定
由上面的程式碼我們瞭解到,超時設定其實應該通過內部的 ClientHttpRequestFactory
來設定的。
所以就可以通過給 RestTemplate設定一個我們自己建立的,設定了超時時間的 ClientHttpRequestFactory來實現。
-
SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory();
-
clientHttpRequestFactory.setConnectTimeout(1000);
-
clientHttpRequestFactory.setReadTimeout(50);
-
RestTemplate restTemplate = new RestTemplate();
-
restTemplate.setRequestFactory(clientHttpRequestFactory);
或者
-
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
-
clientHttpRequestFactory.setConnectTimeout(1000);
-
clientHttpRequestFactory.setReadTimeout(50);
-
RestTemplate restTemplate = new RestTemplate();
-
restTemplate.setRequestFactory(clientHttpRequestFactory);
但是要注意的是: HttpComponentsClientHttpRequestFactory底層使用了apache的 HttpClient,超時時間的設定其實是針對它進行設定的。
HttpComponentsClientHttpRequestFactory
-
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) {
-
Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
-
getHttpClient().getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
-
}
到此,如果就通過上面提到的方式設定超時時間,那麼我們的應用就不用有超時問題,也不會發生故障了。
但問題就發生在,公司內部使用的元件,不是通過 HttpComponentsClientHttpRequestFactory
設定超時時間,而是通過設定 HttpComponentsClientHttpRequestFactory內部的 HttpClient設定的超時時間,並且設定了 HttpClient使用的 HttpClientConnectionManager,從而導致了問題的發生。
問題程式碼&測試
-
@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));
-
}
-
}
● 預設超時 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雜記”。
相關文章
- .Net版本引發的血案
- JDBC亂碼引發的"血案"JDBC
- SwipeRefreshLayout 引發的一場血案
- vue watch陣列引發的血案Vue陣列
- _nop_()函式引發的血案函式
- Flutter 中由 BuildContext 引發的血案FlutterUIContext
- Maven依賴版本號引發的血案Maven
- 一個 Handler 面試題引發的血案!!!面試題
- 一個map函式引發的血案函式
- 一道面試題引發的“血案”面試題
- async,await與forEach引發的血案AI
- js正則全域性匹配引發的血案JS
- 一個ES設定操作引發的“血案”
- 實戰|一個表白牆引發的“血案”
- 一場由postcss-bem引發的血案CSS
- 一個全形空格引發Jquery取值的“血案”jQuery
- #iOS AF上傳圖片引發的血案iOS
- alter index rebuild online引發的血案IndexRebuild
- 【RMAN】Oracle_rman中skip引發的血案Oracle
- 一場 Kafka CRC 異常引發的血案Kafka
- 【原創】一對雙引號引發的goldengate血案Go
- MySQL 中一個雙引號的錯位引發的血案MySql
- 一行超長日誌引發的 “血案” - Containerd 頻繁 OOM 背後的真相AIOOM
- 一個系統BUG引發的血案 -- FKDownloader
- 斷點除錯之壓縮引發的血案斷點除錯
- Mybatis+0+null,小問題引發的血案MyBatisNull
- [WCF]缺少一行程式碼引發的血案行程
- 控制檔案不一致引發的“血案”
- 事故現場:MySQL 中一個雙引號的錯位引發的血案MySql
- 一場版本升級引發的效能血案的追凶過程
- 【web】Spring RestTemplate提交時設定POST請求引數WebSpringREST
- git merge使用不當引發的程式碼丟失血案Git
- 一個由line-height引發的血案與思考
- iOS土味兒講義(一)--一個Button引發的血案iOS
- 為什麼redux要返回一個新的state引發的血案Redux
- 記go中一次http超時引發的事故GoHTTP
- 一場由AI引發的GPU血案,AMD還有機會嗎?AIGPU
- 一個微信面試題引發的血案 --[譯] 什麼阻塞了 DOM?面試題