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

這篇幫到你了嗎?

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

貢獻一條解法