StackTraces
Spring Boot已驗證

RestTemplate 報 SocketTimeoutException

@debug_master32 天前更新閱讀 6 min00

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 要小於上層網關和你自己服務的總超時,否則會發生「你還在等下游,上游已經放棄你」的錯位。真遇到慢接口,先查下游為什麼慢,而不是一味把超時調大。

References(參考來源)

最後更新於 2026-09-09

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法