
本文是网络排查案例集系列的第 2 篇 叙事框架现象 → 排查过程 → 根因 → 修复 → 预防问题现象本文记录一次典型的 TCP 连接异常断开排查过程。某日早高峰order-query 服务的错误率告警触发error log 中出现大量 java.net.SocketException: Connection reset by peer 错误。通过 tcpdump 抓包确认 RST 包来自下游 Nginx进一步检查发现 Nginx keepalive_timeout 配置为 60 秒而 HttpClient 连接池空闲超时为 30 分钟两者严重不匹配。ss 命令显示 18 个 CLOSE_WAIT 连接均为 Nginx 发送 FIN 后未被应用层及时关闭的僵尸连接。根因分析涉及三个层次Nginx 超时策略、连接池闲置连接检测缺失、TCP keepalive 默认配置过长。修复方案包括调大 Nginx keepalive_timeout 至 300 秒、启用 HttpClient evictIdleConnections 定期清理、调整系统 TCP keepalive 参数。排查过程第一步tcpdump 抓包确认 RST 来源为了定位 RST 包的发送方张工在 order-query 服务器上用 tcpdump 抓取发往 order-service 的流量抓包结果非常清晰——每当 order-query 发送一个 HTTP GET 请求order-service 的 Nginx10.0.2.100:8081立刻回复一个 RST 包。这不是偶发而是每个请求都被 RST。这个模式说明Nginx 已经关闭了连接可能是由于 keepalive_timeout 到期但 HttpClient 还在继续使用它。为了获得完整的交互序列张工又在两边同时抓包并用 Wireshark 分析了三次握手的完整过程数据包的序列清晰地展示了事故链Frame 1-3正常三次握手连接建立Frame 460 秒空闲后Nginx 发出[FIN, ACK]——这就是 keepalive_timeout 到期后的行为Frame 5客户端 ACK 了 FIN进入 CLOSE_WAIT 状态。但应用程序没有 close 这个 socketFrame 65 秒后应用程序从连接池取出这个看起来活着的连接写入 HTTP GET 请求Frame 7Nginx 收到了一个已经关闭的连接上的数据回复[RST, ACK]——这就是 Connection reset by peer第二步检查 Nginx 配置确认 RST 来自 Nginx 后张工登录到 order-service 的 Nginx 服务器检查了 keepalive 相关配置keepalive_timeout60;keepalive_requests1000;Nginx 的 keepalive_timeout 设为了 60 秒。这意味着一旦连接空闲超过 60 秒Nginx 就会主动关闭它。这个配置本身不算错误但问题在于——order-query 的 HttpClient 连接池并不知道连接已经被关闭了。第三步深入分析 RST 包的几种类型在排查过程中张工顺便整理了一个 RST 包的类型速查表帮助团队在未来更快地定位类似问题RST 类型触发时机表现典型原因Type A: Connection RefusedSYN 发出后立即回复 RSTConnection refused端口未监听、防火墙 REJECTType B: Connection Reset数据写入已关闭的连接Connection reset by peerkeepalive 超时、连接池复用死连接Type C: Application RST程序关闭 socket 时对端收到 RSTSO_LINGER0、程序异常退出这次遇到的是典型的Type B连接池复用了一条对端已经关闭的连接。根因分析原因 1Idle 连接超时不一致问题的核心是两个超时时间的不匹配见上文 Nginx 配置截图keepalive_timeout 60;Nginx keepalive_timeout:60秒 ← 60s 空闲后发 FIN 关连接 HttpClient idleTimeout:30分钟 ←30分钟内都不检查连接是否存活这 29 分钟的差异就是故障窗口。Nginx 在 60 秒空闲后关闭连接并发送 FIN但 HttpClient 在接下来的 29 分钟内都不会检查这些连接的状态。任何在 60 秒后、30 分钟前从池中取出连接的请求都会触发 RST。更具体地说假设业务在 09:00 有一次请求高峰连接池填满然后 09:01 进入低峰期所有连接空闲。到了 09:02:01Nginx 关闭了所有这些连接但连接池持有它们。09:05 突然进来一批请求HttpClient 从池中取出空闲连接发送请求结果全部被 RST。这就是为什么错误是间歇性的——它只在空闲期后的第一个请求高峰出现。原因 2连接池缺乏闲置连接检测再来看看 HttpClient 的配置connManager.setValidateAfterInactivity(2000);虽然设了validateAfterInactivity2000ms但这个参数的行为是仅在从池中取出连接时检查连接是否过期。它通过发送一个轻量级的 TCP 探测来验证连接是否仍然有效。但由于这个探测是在取出连接时触发的它不能主动清理池中的死连接。更重要的是连接池没有配置evictIdleConnections定时清理器导致已经 CLOSE_WAIT 的连接在池中长期存在直到被取出使用才被发现。原因 3TCP keepalive 兜底缺失那问题来了TCP 协议本身不是有 keepalive 机制吗为什么没有检测到连接已断开看服务器的 TCP keepalive 参数net.ipv4.tcp_keepalive_time7200←2小时后才发第一个探测包 net.ipv4.tcp_keepalive_intvl75← 探测间隔75秒 net.ipv4.tcp_keepalive_probes9←9次探测失败才断开Linux 默认的 TCP keepalive 配置是2 小时后才发第一个探测包探测间隔 75 秒9 次失败才判定断开。这意味着一个死连接最多需要2 小时 75×9 2 小时 11 分钟才会被内核检测到。在这个场景下Nginx 在 60 秒后就发 FIN 关闭了连接而客户端 TCP 栈要等 2 小时后才尝试 keepalive 探测——这中间差了整整 2 个小时连接池中的僵尸连接早就能造成无数次 RST 了。累计效应故障模式全景综合以上三个因素一次故障过程的完整时序如图所示完整链条如下① 业务高峰 → 连接池填满 TCP 连接 ② 业务低峰 → 所有连接空闲 ③ 60s 后 → Nginx keepalive_timeout 到期发 FIN 关闭每条连接 ④ 客户端 → ACK FIN进入 CLOSE_WAIT但应用不知情 ⑤ 连接池 → 认为这些连接仍然空闲可用保留在池中 ⑥ 新的请求到达 → HttpClient 从池中取出空闲连接 ⑦ 连接复用 → 发出 HTTP GET 请求此时 Nginx 已关闭此连接 ⑧ Nginx → 收到已关闭连接上的数据回复 RST 包 ⑨ 应用 → SocketException: Connection reset by peer ⑩ 错误率飙升 → 触发 P1 告警在这个过程中每条连接的死亡过程只需约 65 秒60s 空闲 5s 窗口但连接池需要 30 分钟才能意识到连接已死。修复方案第一步调整 Nginx keepalive_timeout既然问题是 Nginx 的 keepalive_timeout 太短60s导致的第一反应可能是把这个值调大。但仅仅调大 Nginx 端并不能根治问题——如果不解决连接池的检测机制Nginx 再大的超时也终有到期的时候。我们对三个参数做了联动调整参数改前改后依据Nginx keepalive_timeout60s300s与 TCP keepalive 对齐Nginx keepalive_requests100010000减少因请求数上限导致的连接关闭HttpClient evictIdleConnections未配置5s主动淘汰闲置连接为什么是 300s因为 TCP keepalive 的探测周期改后也是 300s这样三个超时形成了一个完整的保护链连接池淘汰 Nginx 超时 ≤ TCP keepalive 探测周期。第二步修复 HttpClient 连接池配置这是最关键的修复——给 HttpClient 加上主动清理机制关键改动是添加了evictIdleConnections(5, TimeUnit.SECONDS)。这会在 HttpClient 内部启动一个后台线程每 5 秒扫描一次连接池关闭那些空闲超过 5 秒的连接。这样即使 Nginx 关闭了连接后 HttpClient 没有及时感知最多 5 秒后这条死连接就会被清理掉。同时保留了validateAfterInactivity1000ms作为取出连接时的第二道防线。第三步调优 TCP keepalive 内核参数为了兜底还调优了操作系统的 TCP keepalive 参数net.ipv4.tcp_keepalive_time300← 从 7200s 降到 300s5 分钟 net.ipv4.tcp_keepalive_intvl30← 从 75s 降到 30s net.ipv4.tcp_keepalive_probes3← 从9次降到3次调优后的行为连接空闲 300 秒后发送第一个 keepalive 探测包30 秒后发送第二个再 30 秒后发送第三个。如果 3 次都没有收到回应即 30030×3390 秒 6.5 分钟内核就判定连接断开并通知应用层。这不仅对本次的 Nginx 场景有效也对其他中间件如 Redis、MySQL的空闲连接断开问题起到了兜底作用。第四步上线部署配置修改完成后依次执行了以下步骤更新/etc/sysctl.d/10-tcp-keepalive.conf并执行sysctl -p重新编译部署 order-query 服务含新的 HttpClient 配置Nginx 执行nginx -s reload加载新的 keepalive 配置灰度观察 10 分钟确认 error_log 中 Connection reset 归零全量上线验证结果即时指标修复上线后error log 中的 Connection reset 报错立刻消失关键变化CLOSE_WAIT 连接数从 18 降为 0连接池不再持有死连接Connection reset 错误从 247 条降为 0问题根本解决所有连接全部 ESTAB 状态不再有僵尸连接TCP 抓包也验证了修复的有效性可以看到在空闲 300 秒后客户端发送了 TCP keepalive 探测包Nginx 正常回应了 ACK连接继续保持。后继的 HTTP 请求复用同一连接正常收到 200 响应——不再有 RST。团队复盘修复后团队进行了复盘讨论大家一致认为这次问题的根本原因是超时配置的割裂——Nginx 团队设置了 keepalive_timeout业务团队设置了连接池参数但没有人把它们放在一起对账。这种跨组件的配置一致性检查在问题发生前是盲区。避坑建议连接池 idleTimeout 必须小于中间件 keepalive_timeout无论是 Nginx、Redis、MySQL 还是其他中间件应用层的连接池空超时时间必须小于中间件的 keepalive_timeout否则就会出现本文中的死连接复用问题。一定要开启 evictIdleConnectionsHttpClient 的evictIdleConnections不是默认开启的。必须显式配置一个后台线程定期扫描并清理过期连接。推荐设为一个较小的值如 5 秒与连接池的validateAfterInactivity配合使用。TCP keepalive 要主动调优Linux 默认的 2 小时 keepalive 周期对绝大多数现代应用来说太长。在服务网格化和容器化的今天连接寿命通常以分钟计建议将tcp_keepalive_time调到 300-600 秒。跨团队配置要有对账机制Nginx 由运维团队管理应用连接池由开发团队配置——如果没有一个集中管理配置的地方类似的不一致问题会反复发生。建议在新服务上线前添加配置一致性检查步骤。CLOSE_WAIT 数量是重要的健康信号ss 输出的 CLOSE_WAIT 连接数是一个被低估的诊断指标。大量 CLOSE_WAIT 通常意味着应用没有正确关闭对端发起的关闭请求往往是连接池配置问题的早期信号。不要只看错误率要看错误模式持续的 Connection refused 和间歇性的 Connection reset 对应的根因完全不同。前者通常是指定端口未监听或防火墙拦截后者往往是连接复用问题。错误的模式本身就在告诉你方向。setValidateAfterInactivity 不等于心跳这个参数只在新取出连接时检查不能主动清理池中的死连接。它和 evictIdleConnections 是互补关系不是替代关系。附完整命令清单# 1. 初始排查ss-tlnp|grep8080tail-100/var/log/app/order-query/error.log|grepConnection reset|wc-lss-t|grep8081|awk{print $1}|sort|uniq-c# 2. 抓包确认 RST 来源tcpdump-ieth0 port8081-nn-c30tcpdump-ieth0 port8081-nn-c20-w/tmp/rst-capture.pcap tcpdump-r/tmp/rst-capture.pcap-nn# 3. 检查 Nginx 配置cat/etc/nginx/conf.d/order-service-upstream.conf nginx-T2/dev/null|grepkeepalive_timeoutgrep-rkeepalive/etc/nginx/# 4. 查看系统 TCP keepalive 参数cat/proc/sys/net/ipv4/tcp_keepalive_timecat/proc/sys/net/ipv4/tcp_keepalive_intvlcat/proc/sys/net/ipv4/tcp_keepalive_probes# 5. 查看应用连接池配置grep-A5PoolingHttpClientConnectionManager/var/log/app/order-query/app.log# 6. 修复后验证ss-t|grep8081|awk{print $1}|sort|uniq-ccat/etc/sysctl.d/10-tcp-keepalive.confsysctl-p/etc/sysctl.d/10-tcp-keepalive.conf nginx-sreload