结论是RST由连接生命周期在客户端、服务端、中间设备三者间失配导致;需用tcpdump精准过滤抓包(如tcpdump -i any 'host 10.3.2.67 and port 8080' -w rst.pcap),再用Wireshark筛选tcp.flags.reset == 1,根据Source IP判断RST发出方,并结合连接池、健康检查及内核参数交叉验证。

直接结论:这不是单一环节的问题,而是连接生命周期在客户端、服务端、中间设备三者间失配的结果。必须抓包确认 RST 发出方,再结合连接池配置和健康检查策略交叉验证。
怎么用 tcpdump 快速定位 RST 是谁发的
日志里只写 Connection reset by peer,但“peer”到底是下游服务、网关,还是防火墙?不抓包就只能猜。关键不是抓全量流量,而是精准过滤:
- 在报错的服务机器上执行:
tcpdump -i any 'host 10.3.2.67 and port 8080' -w rst.pcap(把10.3.2.67换成实际下游 IP) - 复现问题后,用 Wireshark 打开
rst.pcap,过滤tcp.flags.reset == 1 - 看 RST 包的
Source IP:如果是下游服务 IP,说明它主动断了;如果是 SLB 或网关 IP,问题大概率在健康检查或会话保持策略上 - 特别注意:Nginx 日志中
recv() failed (104)通常意味着 RST 来自 upstream;readv() failed更可能来自客户端侧
为什么 Spring Boot + Feign 调用频繁触发 RST
不是代码写错了,而是默认超时和连接池参数与下游实际行为严重 mismatch。Feign 默认 ReadTimeout 是 1 秒,而很多微服务处理耗时在 2–5 秒之间——连接还没读完,客户端就强行 close,服务端收到后续数据时只能发 RST。
- 必须同时配置连接池和超时:
feign.httpclient.max-connections=200、feign.httpclient.max-connections-per-route=50 - Ribbon 的
ReadTimeout和ConnectTimeout都要显式设为合理值,比如ribbon.ReadTimeout=5000,不能依赖默认 1000 - 如果下游是 Node.js 或 Python Flask 类服务,它们默认不校验
Host头;但一旦加了网关或 WAF,Host不匹配就会被静默 RST,而非返回 400 —— 这类问题必须比对请求原始 header
API 网关或 SLB 怎么偷偷制造 RST
四层负载均衡器(如阿里云 SLB、AWS ALB)的 TCP 健康检查是高频 RST 来源之一。它只做三次握手,不发 HTTP 请求,检查完也不主动断连。后端服务空闲超时后,会自己发 FIN 或 RST 关闭连接,而 SLB 还拿着这个“半死”连接转发新请求,结果就是客户端收到 RST。
- 查 SLB 健康检查配置:是否启用了 TCP 模式?间隔是否短于后端服务的
keepalive_timeout? - 在服务端开
ss -antop | grep CLOSE_WAIT,大量CLOSE_WAIT表明应用没调用close(),连接卡在半关闭状态 - K8s 中更隐蔽:
kube-proxy的 iptables 规则 + conntrack 表老化不一致,会导致 endpoint 消失后旧连接仍被转发,触发 RST
TIME_WAIT 爆满导致新建连接直接 RST
当 net.ipv4.tcp_max_tw_buckets 被打满(默认 32768),Linux 内核会跳过四次挥手,直接对新关闭的连接发 RST。这在高并发短连接场景下极易发生,且错误日志和客户端现象完全一样。
- 用
ss -s | grep -i time查当前 TIME_WAIT 数量 - 临时缓解:
sysctl -w net.ipv4.tcp_tw_reuse=1(仅对客户端有效) - 长期方案:服务端改为主动关闭方(让客户端处于 TIME_WAIT),或启用 SO_LINGER 强制 FIN_WAIT_2 超时退出
- 注意:
tcp_tw_recycle在 NAT 环境下已废弃,禁用
真正麻烦的从来不是单个 RST,而是它背后暴露的连接管理权责模糊——客户端不敢重试,服务端不等写完,网关不管死活。抓包确认发出方只是起点,接下来得对照三方的连接池回收逻辑、健康检查周期、内核参数,一条链路一条链路对齐。否则改完一处,另一处立刻补刀。


















