Linux TCP Connection reset by peer 与 Broken pipe:RST 排查指南
Linux 网络程序中,客户端看到 Connection reset by peer,服务端日志出现 Broken pipe,经常被统称为“网络断开”。但两者发生在不同的系统调用和方向:前者通常表示本端收到了 TCP RST,后者常见于本端继续向已无可写对端的连接发送数据。要找到责任点,必须把应用时间线、两端 socket 状态和线上数据包对齐。
本文目录(17 节)
先记录错误发生在哪个系统调用
保存进程、线程、连接四元组、请求 ID、错误时间和具体操作。connect()、read()/recv()、write()/send() 返回相似错误时,代表的阶段不同。仅记录一行“reset”无法判断是握手、请求上传、响应读取还是连接复用失败。
同时记录 errno,而不是只保存框架翻译后的文案。ECONNRESET、EPIPE、ETIMEDOUT 和应用层 EOF 不应混为一谈。若语言运行时包裹异常,保留最底层 cause 和调用栈,但从报告中移除凭据与业务正文。
TCP RST 表示什么
RST 是 TCP 对无法接受或需要立即终止的连接段作出的复位信号。常见来源包括目标端口没有监听、对端应用以 abortive 方式关闭、主机内核找不到对应连接、NAT/防火墙主动拒绝,或中间设备伪造复位。
看到 RST 只证明网络路径上有一方发送了带 RST 的段,并不自动证明业务服务器进程主动发送。必须从抓包的源/目的地址、序列号、TTL、时间和两端日志进一步归因。NAT 后的抓包还需转换前后的四元组关联。
Connection reset by peer 的典型路径
当已建立连接收到可接受的 RST,后续读取或其他 socket 操作通常报告连接被对端复位。对端可能在未读取完请求时关闭 socket,内核发现关闭时接收缓冲仍有未读数据,从而采用 RST;应用崩溃、容器被强制终止或负载均衡健康策略也可能中断连接。
先查对端同一时间是否有进程退出、OOM、重启、请求体限制、解析异常或超时。不要只提高客户端重试次数。若请求不是幂等的,无条件重试可能重复写入。
Broken pipe 与 SIGPIPE
应用向已经被关闭或复位的连接写入时,Linux 可能返回 EPIPE,并向进程发送 SIGPIPE。未处理 SIGPIPE 的程序可能直接终止;许多运行时会忽略信号并把 EPIPE 转成 Broken pipe 异常。
错误出现在写入端,不代表写入端最先关闭。对端可能早已发送 FIN 或 RST,只是应用没有及时读取 socket 状态,直到下一次 write 才发现。排障要查看写入前是否已有 EOF、EPOLLHUP、EPOLLERR 或 RST 到达。
FIN 与 RST 不一样
FIN 表示发送方不再发送数据,支持 TCP 半关闭;另一方向仍可继续传输。RST 则立即复位连接,未完成数据可能丢失。应用收到 EOF 后若协议允许,可以停止读取并完成剩余写入;但若对端已经完全关闭或复位,继续写就会失败。
抓包时检查 flags 和序列顺序。若先有正常 FIN/ACK,应用隔了很久还写,重点修复连接生命周期;若没有 FIN 而突然 RST,重点寻找 abort、超时或状态丢失。
SO_LINGER 可能制造主动复位
启用 SO_LINGER 且 linger 时间为 0 时,close 可能执行 abortive close,发送 RST 而不是正常 FIN 关闭。某些连接池、代理或压测工具为了快速释放连接会设置该选项。
检查应用和底层库是否修改 SO_LINGER。不要为了减少 TIME_WAIT 随意使用零 linger,这可能让对端看不到完整响应,并把协议层问题变成 reset。若确需 abort,应明确记录原因并限制使用场景。
未读取请求体就关闭连接
服务器在认证失败、请求体过大或解析头部后立即关闭,而客户端仍在上传大正文时,客户端可能收到 reset 或 Broken pipe。反向代理和应用对最大请求体、读取超时的处理方式也会影响是否优雅消费剩余数据。
比较客户端已发送字节、服务端实际读取字节和返回状态。让服务器尽可能返回明确的 HTTP 错误并按协议处理连接;客户端则应支持 Expect/Continue 或在收到早期响应后停止发送,而不是盲目写完整请求。
空闲连接与连接池复用
服务器、负载均衡、NAT 和客户端连接池往往有不同 idle timeout。中间层清除空闲状态后,客户端仍从池中取出旧连接,首次读取或写入就会 reset。若错误集中在“空闲 N 秒后的第一请求”,应重点比较各层超时。
让上游连接池的最大空闲时间短于下游最短 idle timeout,或在复用前检测连接可用性。TCP keepalive 可帮助发现部分死连接,但参数、探测次数和中间设备行为需要实际验证;它不能代替应用请求超时和安全重试策略。
容器、NAT 与 conntrack 状态
容器网络、四层负载均衡和防火墙依赖连接跟踪。表项提前过期、节点切换、地址复用或规则重载后,新数据包可能找不到原状态并被丢弃或复位。只在容器内抓包可能看到虚拟地址,无法定位宿主机或外部 LB 的行为。
在客户端、服务端容器、宿主机和边界设备选择至少两个观测点,按 NAT 前后四元组关联。检查 conntrack 使用率、超时和丢弃计数,但不要在生产上无边界输出整张连接跟踪表。
用 ss 查看本地 socket 证据
ss 可以显示 TCP 状态、进程、计时器和部分 TCP 内部信息。故障前后采样 ESTAB、CLOSE-WAIT、FIN-WAIT、LAST-ACK 等状态,结合 ss -ti 中的重传、RTT 和拥塞信息,可以判断连接是否已进入关闭阶段。
一次事后 ss 为空并不能证明没有连接,因为 RST 会快速清除状态。对低频故障,应使用有上限的周期采样、eBPF 事件或应用级连接日志,而不是等用户报告后才查看。
抓包如何确定谁发了 RST
在两端用相同时间基准抓取相关连接,过滤四元组并保存握手到错误的完整片段。确认 RST 包出现在哪个观测点、源地址是否经过 NAT、ACK/SEQ 是否落在预期窗口,以及此前最后一个数据段由谁发送。
如果客户端侧看到来自服务器地址的 RST,而服务器主机完全没有发出对应包,中间设备可能代发;若服务器内核发出 RST,再向上追应用 close、监听队列和 socket 状态。不要仅凭 IP 源地址认定物理发送者。
超时和重试要按幂等性设计
reset 发生时,客户端未必知道服务器是否已经处理请求。GET 等通常可按策略重试;支付、创建订单或其他非幂等操作必须使用幂等键、事务状态查询或去重机制。简单“捕获异常再发一次”会把短暂网络错误变成重复业务。
重试应有总时限、次数上限、退避与抖动,并区分连接建立前失败、未发送正文、正文已部分发送和响应读取中断。服务端也应提供可查询的请求标识和处理状态。
一套逐层排查流程
第一步,固定错误时间、四元组、errno 和系统调用。第二步,检查两端进程重启、OOM、超时与限制。第三步,采集 socket 状态和内核计数。第四步,在两侧抓取 FIN/RST 时间线。第五步,检查代理、LB、NAT 和 conntrack 空闲策略。第六步,核对 SO_LINGER、连接池复用和请求体处理。第七步,用幂等请求进行受控复现。
修复后验证新连接、连接复用、超过空闲阈值、大请求体、服务器优雅关闭和进程强制终止。既要确认正常路径没有 reset,也要确认异常路径能返回明确错误且不会重复业务写入。
常见错误
常见误区包括:把 reset 一律归咎于对端应用;只看一侧抓包;混淆 FIN、EOF 与 RST;用 SO_LINGER=0 减少 TIME_WAIT;所有请求无条件重试;只调大超时;忽略连接池与 LB idle timeout;以及抓取完整业务载荷泄露敏感数据。
常见问题
Connection reset by peer 是否一定是服务器发的?
不一定。服务器内核、应用、负载均衡、防火墙或 NAT 都可能产生或转发 RST。需要两端抓包和状态日志确认实际来源。
Broken pipe 为什么在服务端出现?
服务端正在写响应时,客户端可能已关闭、超时离开或发送复位。服务端直到下一次 write 才得到 EPIPE,因此错误时间可能晚于客户端断开时间。
忽略 SIGPIPE 就能解决吗?
忽略或屏蔽 SIGPIPE 可以避免进程被信号终止,但 write 仍会返回 EPIPE。程序仍需正确处理错误、停止写入并清理请求状态。
TCP keepalive 能完全避免死连接复用吗?
不能。它取决于探测参数和网络设备行为,也可能比连接池空闲周期更慢。应同时协调 idle timeout、复用策略和应用请求超时。
reset 后可以直接重试 POST 吗?
不能默认安全。服务器可能已处理请求但响应丢失。应使用幂等键、状态查询或业务去重,并根据请求发送阶段决定是否重试。
总结
Linux 的 ECONNRESET 与 EPIPE 是连接生命周期不同位置的证据,不是同一个模糊的“网络错误”。可靠排障要从系统调用和四元组出发,把应用关闭、FIN/RST、连接池空闲、代理超时和 NAT 状态放到同一时间线。修复不仅要消除异常,还应保证重试不会造成重复写入,异常关闭能被安全、可观测地处理。
来源资料
- RFC Editor, RFC 9293 — Transmission Control Protocol: https:
/ / www. rfc- editor. org/ rfc/ rfc9293 - Linux man-pages, tcp(7): https:
/ / man7. org/ linux/ man- pages/ man7/ tcp. 7. html - Linux man-pages, socket(7): https:
/ / man7. org/ linux/ man- pages/ man7/ socket. 7. html - Linux man-pages, ss(8): https:
/ / man7. org/ linux/ man- pages/ man8/ ss. 8. html