客戶老是反應(yīng)調(diào)用我們接口超時,但通過監(jiān)控來看系統(tǒng)并沒有什么異常,所以接口調(diào)用超時時到底發(fā)生了什么呢?讓我們通過本文來一探究竟。
1 模擬一下調(diào)用超時
服務(wù)端程序(一個簡單的REST接口,直接睡眠個10s,模擬響應(yīng)時間長):
@RestController
public class HelloController {
@GetMapping("hello")
public String hello() throws InterruptedException {
System.out.println("開始處理");
Thread.sleep(10000);
System.out.println("處理結(jié)束");
return "hello";
}
}
客戶端程序(通過HttpClient調(diào)用REST接口,配置socketTimeout參數(shù),將超時時間設(shè)置為2s):
@Test
public void testHttpClientTimeout() throws IOException {
try(CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet("http://localhost:8080/hello");
httpGet.setConfig(RequestConfig.custom().setConnectTimeout(10 * 1000).setSocketTimeout(2 * 1000).build());
try(CloseableHttpResponse response = httpClient.execute(httpGet)) {
EntityUtils.toString(response.getEntity());
}
}
}
啟動服務(wù)端,然后客戶端發(fā)起調(diào)用。
客戶端在2s后發(fā)生超時,調(diào)用終止:
java.net.SocketTimeoutException: Read timed out
服務(wù)端接口打印日志:
開始處理
處理結(jié)束
由上述日志可以看出,客戶端調(diào)用在超過2s后還沒有獲得響應(yīng)時,便會拋出Read timed out異常,結(jié)束調(diào)用,但是服務(wù)端針對本次調(diào)用還是會完整的處理完,不會因為客戶端的調(diào)用終止而終止處理。
所以這期間到底發(fā)生了什么?服務(wù)端請求處理完成后還能響應(yīng)給客戶端嗎?
2 刨根問題:調(diào)用超時到底發(fā)生了什么?
重復(fù)上述步驟,模擬客戶端調(diào)用超時,進行抓包。
抓取的數(shù)據(jù)包如下:

根據(jù)抓包得出整個處理流程如下:
1、TCP三次握手;
2、客戶端發(fā)起請求;
3、2s后,客戶端請求超時,發(fā)送FIN包關(guān)閉連接;
4、服務(wù)端響應(yīng)FIN包,返回ACK;
5、10s后,服務(wù)端處理完請求,返回響應(yīng);
6、客戶端響應(yīng)RST重置連接。
可以看到,即使客戶端已經(jīng)返回響應(yīng)超時異常了,服務(wù)端還是會正常處理請求并在完成后響應(yīng),并且這個請求狀態(tài)還是200(這也是服務(wù)端監(jiān)控并沒有發(fā)現(xiàn)響應(yīng)異常的原因),但是在返回響應(yīng)給客戶端時,客戶端會響應(yīng)RST包重置該連接,也就是該響應(yīng)最終并沒有成功返回給客戶端。
所以說,HTTP請求超時是客戶端調(diào)用方的概念,服務(wù)端在處理請求的過程中不會因為處理時間過長而中斷響應(yīng),反而是會一直等待請求處理完成然后返回客戶端。
至此,接口請求超時的整個處理流程應(yīng)該比較清楚了。
但還有一個問題,客戶端是怎么觸發(fā)超時提前拋出異常返回的呢?
首先看超時配置:
httpGet.setConfig(RequestConfig.custom().setConnectTimeout(10 * 1000).setSocketTimeout(2 * 1000).
可以看到上述設(shè)置了超時時間參數(shù)socketTimeout,通過跟代碼,該參數(shù)實際是設(shè)置了Socket連接的soTimeout參數(shù):

繼續(xù)看Socket#setSoTimeout方法,通過注釋該參數(shù)的意思應(yīng)該比較清楚了——如果超過設(shè)置的超時時間讀操作還沒有返回(即沒有接收到響應(yīng)數(shù)據(jù)),則會拋出SocketTimeoutException異常。

所以客戶端的請求超時實際上是通過設(shè)置Socket#soTimeout參數(shù)來實現(xiàn)的,即超過指定時間還未讀到響應(yīng)數(shù)據(jù),則拋出異常。
寫在最后
HTTP請求超時是客戶端調(diào)用方的概念。
服務(wù)端在處理請求的過程中不會因為處理時間過長而中斷響應(yīng),反而是會一直等待請求處理完成然后返回客戶端。
客戶端在調(diào)用超時后,會發(fā)送FIN包中斷連接,所以即使服務(wù)端處理完請求后,也無法正確返回給客戶端。
客戶端的請求超時實際上是通過設(shè)置Socket#soTimeout參數(shù)來實現(xiàn)的。
知道了這么多,那么到底該如何解決客戶端調(diào)用超時的問題呢?客戶端調(diào)用超時的根本原因還是服務(wù)端響應(yīng)過慢,解決方法得從提升服務(wù)端性能,減少響應(yīng)時間入手。如果服務(wù)端涉及到第三方調(diào)用,還需要將同步調(diào)用異步化,通過犧牲一致性來提升性能。
希望今天的內(nèi)容對大家有所幫助,更多精彩文章歡迎關(guān)注微信公眾號:WU雙。