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 要小于上层网关和你自己服务的总超时,否则会发生「你还在等下游,上游已经放弃你」的错位。真遇到慢接口,先查下游为什么慢,而不是一味把超时调大。
