StackTraces
Java已验证

Java Connection reset 与 Broken pipe 排查

@debug_master32 天前更新阅读 6 min00

这两个异常都是在说「你以为还开着的连接,其实早就被对端关了」。区别只在于发现的方式:Connection reset 是读/写时收到对端发来的 RST 复位包;Broken pipe 是往一个对端已经关闭的 socket 里写数据。它们大多不是网络真断了,而是 keep-alive 超时时间两头没对齐,竞态撞出来的。

Issue & Env(案发现场与环境)

  • 现象:调用外部接口或对外服务时,极低比例(0.01%~1%)请求偶发失败,本地复现不了,日志里是 Connection reset 或 Broken pipe。
  • 环境:Java 客户端(HttpClient / RestTemplate / WebClient)+ 服务端(Tomcat / 网关 / 负载均衡),中间可能隔 nginx、ALB、K8s Ingress。

StackTrace(堆栈日志)

java.net.SocketException: Connection reset
    at java.base/sun.nio.ch.SocketChannelImpl.throwConnectionReset(SocketChannelImpl.java:401)
    at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:434)
    at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:137)
    at org.apache.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:153)
    at org.apache.http.impl.conn.LoggingInputStream.read(LoggingInputStream.java:87)
    at org.apache.http.impl.io.ContentLengthInputStream.read(ContentLengthInputStream.java:178)
    at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:137)

写侧被掐断则是:

java.io.IOException: Broken pipe
    at java.base/sun.nio.ch.FileDispatcherImpl.write0(Native Method)
    at java.base/sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:62)
    at java.base/sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:113)
    at java.base/sun.nio.ch.IOUtil.write(IOUtil.java:79)
    at java.base/sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:537)
    at org.apache.http.impl.io.SessionOutputBufferImpl.streamWrite(SessionOutputBufferImpl.java:124)
    at org.apache.http.impl.io.SessionOutputBufferImpl.flushBuffer(SessionOutputBufferImpl.java:131)
    at org.apache.http.impl.io.ContentLengthOutputStream.write(ContentLengthOutputStream.java:114)

注意:这里不是「Java 主动断了连接」,而是「Java 使用中的连接已经被别人断掉了,此时才发现」。

Reproduce(复现代码)

典型竞态:服务端 keep-alive 超时 60 秒就关空闲连接,客户端却 90 秒还在复用同一条连接。

T+0s   Request 1 完成,连接进入空闲
T+60s  服务端超时,向客户端发 FIN 关连接
T+60s  客户端不知道,复用这条「以为还活着」的连接发新请求
T+60s  服务端收到发往已关闭 socket 的请求,回 RST 复位
T+60s  客户端读到 RST → Connection reset

Root Cause(根因分析)

HTTP keep-alive 让客户端和服务器复用同一条 TCP 连接,双方各自有一个「空闲多久就关」的超时。当服务端的超时比客户端短,就会在客户端毫不知情的时候把连接关掉,客户端下一次复用正好撞上这个时间窗,于是 0.1% 的请求空窗失败。

除了这个错配,还有几类常见成因:

  1. 客户端 read timeout 设得太短,服务端还在处理,客户端先放弃并把连接关了,服务端写完才发现对方没了。
  2. 中间的负载均衡、代理、防火墙有自己的 idle timeout,比两端都短,先动手清连接。
  3. 服务端在客户端还没读完响应时就把连接关了,客户端写数据时触发 Broken pipe。

它们共同点在于:都不是网络「断了」,而是「谁先关、谁不知道」的时序问题,所以本地、低并发下几乎必然不出现。

The Fix(解决方案)

核心原则:让客户端在服务端之前主动关空闲连接,留出安全余量,消除竞态窗口。

服务端 keep-alive 60 秒,客户端就把空闲连接回收设成 55 秒:

server:
  tomcat:
    keep-alive-timeout: 60s
    connection-timeout: 30s

客户端(Apache HttpClient 5)在服务端之前清理空闲连接:

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(100);
cm.setDefaultMaxPerRoute(10);

CloseableHttpClient client = HttpClients.custom()
    .setConnectionManager(cm)
    .evictIdleConnections(TimeValue.ofSeconds(55)) // 低于服务端的 60s
    .build();

对外调用时,重试要有节制:读操作可以重试,写/变更类请求必须在幂等有保障的前提下重试,否则「服务端其实处理成功了、客户端超时重试」会重复扣款、重复下单。Broken pipe 一旦发生,同一条连接已经不能用,重试必须建立在新建连接之上,而不是对着旧 socket 再写一次。

References(参考来源)

最后更新于 2026-09-09

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法