Read timed out不是連不上,是連上了、請求也發出去了,但響應遲遲讀不回來。RestTemplate自己不定超時,把它交給底層工廠;預設工廠的超時是 0,也就是「無窮等」,一個卡住的下游接口能把調用執行緒一直掛死,量一大執行緒池就被拖垮。
Issue & Env(案發現場與環境)
- 現象:調用外部 HTTP 接口的程式碼偶爾或高併發時拋超時,應用整體變慢,甚至所有接口都開始排隊。
- 環境:Spring Boot 2.x / 3.x,
RestTemplate,預設使用SimpleClientHttpRequestFactory(底層是 JDK 的HttpURLConnection)。
StackTrace(堆疊日誌)
標準超時例外:
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
at java.io.BufferedInputStream.fill(BufferedInputStream.java:246)
at java.io.BufferedInputStream.read1(BufferedInputStream.java:286)
at java.io.BufferedInputStream.read(BufferedInputStream.java:345)
at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:735)
at sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:678)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1593)
at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1490)
at org.springframework.http.client.SimpleClientHttpRequestFactory$SimpleBufferingClientHttpRequest.executeInternal(SimpleClientHttpRequestFactory.java:72)
at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:741)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:694)
at org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:325)
at com.example.PaymentClient.callDownstream(PaymentClient.java:57)
它和連接超時是兩碼事。連接超時是 TCP 握手階段失敗,報的是:
java.net.ConnectException: Connection timed out
Read timed out 說明連接已建立,但等響應數據的階段超時了。
Reproduce(復現程式碼)
下游接口故意睡很久,客戶端用預設 RestTemplate 調它:
// 下游:睡 30 秒再返回
@GetMapping("/slow")
public String slow() throws InterruptedException {
Thread.sleep(30000);
return "ok";
}
// 客户端:new RestTemplate() 没配任何超时
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject("http://localhost:8080/slow", String.class);
因為預設沒有讀超時,這行程式碼會一直阻塞到下游返回,中途不會自己報 Read timed out。你看到的 Read timed out,通常是在某個地方顯式配了超時之後才會出現。
Root Cause(根因分析)
RestTemplate 是一個門面,真正的連接、讀寫由 ClientHttpRequestFactory 完成。new RestTemplate() 預設用 SimpleClientHttpRequestFactory,它內部是 JDK 的 HttpURLConnection。HttpURLConnection 的 connect 和 read 超時預設都是 0,即永不超時。
這個 0 就是坑。它意味著:只要下游接口不按預期速度返回,調用執行緒就會死死卡在 getForObject 上,既不報錯也不釋放。Tomcat 處理請求的執行緒就這麼多,幾十個慢調用一疊加,執行緒池被耗盡,新請求全部排隊,最終整站「假死」。日誌裡往往看不到明顯例外,只有執行緒 dump 或者「Read timed out」這類散落的超時,排查難度極高。
所以要先分清兩個超時:connectTimeout 管建立連接,readTimeout 管等響應。兩者都要設,缺一個都可能埋雷。
The Fix(解決方案)
Spring Boot 裡最乾淨的做法是用 RestTemplateBuilder:
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.connectTimeout(Duration.ofSeconds(3))
.readTimeout(Duration.ofSeconds(5))
.build();
}
手寫工廠也行:
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(3000);
factory.setReadTimeout(5000);
RestTemplate restTemplate = new RestTemplate(factory);
生產環境建議換成帶連接池的 HttpComponentsClientHttpRequestFactory,避免每次請求都新建連接。超時值不要拍腦袋:readTimeout 要小於上層網關和你自己服務的總超時,否則會發生「你還在等下游,上游已經放棄你」的錯位。真遇到慢接口,先查下游為什麼慢,而不是一味把超時調大。
