开启 tcp_tw_recycle 后,NAT 后方部分用户为何间歇性连不上?一次 SYN 被内核静默丢弃的排查记录

服务器为压 TIME_WAIT 开启 tcp_tw_recycle,导致 NAT 后方部分用户 SYN 被 PAWS 静默丢弃、间歇连不上的排查复盘。

问题背景

上线一个新的对外 API 网关后,我们在压测阶段观察到服务器上 TIME_WAIT 连接常年维持在数万条,运维同事担心占满端口、拖慢回收,便照着网上一篇"TCP 内核调优"的老帖子,把 net.ipv4.tcp_tw_recycletcp_tw_reuse 一并设成了 1,还随手写进了 /etc/sysctl.conf 持久化。改完当时压测正常,TIME_WAIT 数字确实降了下来,大家皆大欢喜。

可上线一周后,客服陆续转来投诉:一部分用户反映 App 首页"时好时坏",有时刷不出来、有时秒开;同一个人换个 4G 网络又正常了。奇怪的是这批用户高度集中在几家大企业和连锁门店——也就是"很多人共用一个出口公网 IP"的场景。这类间歇性、只影响部分人群的故障,是运维里最难缠的一种。

故障现象

梳理出来的现象有几个鲜明特征:

  • 故障只影响一部分客户端,且这批客户端往往来自同一批出口 IP(企业 NAT、运营商大网 NAT)后方;家庭宽带、单用户手机热点几乎不受影响。
  • 表现为建连阶段就失败:App 侧报连接超时,浏览器 F12 里请求长时间 pending 后 ERR_CONNECTION_TIMED_OUT,而不是 5xx 或 RST——说明请求根本没被应用处理。
  • 同一出口 IP 下,有的连接成功、有的失败,失败呈间歇性,重试几次往往能连上。
  • 服务器端 CPU、内存、连接数、后端应用日志全都正常,应用层完全"无感"——它压根没收到这些请求。

在服务器上抓包 tcpdump -i any 'tcp[tcpflags] & tcp-syn != 0' 时看到诡异一幕:客户端的 SYN 明明到了网卡,服务器却不回 SYN-ACK,也不回 RST,直接吞掉。客户端只能傻等,直到超时。

排查过程

第一步:确认丢包发生在内核而非应用。 既然抓包能看到 SYN 进来、应用却没日志,说明内核在 TCP 握手前就把包丢了。查 netstat -s 的 TCP 统计,发现两个计数器在持续快速增长:

1
2
3
$ netstat -s | grep -i -E 'passive|timestamp|SYN'
    ... passive connection rejections because of time stamp
    ... packets rejects in established connections because of timestamp

packets rejects ... because of timestamp 这一句直接点题——内核因为时间戳检查拒绝了数据包。这把矛头指向了 TCP 时间戳(RFC 1323)和 PAWS 机制。

第二步:锁定 PAWS + tcp_tw_recycle 的组合。 复习一下机制:开启 tcp_timestamps 后,内核用 PAWS(Protect Against Wrapped Sequences)防止序列号回绕,会记录每个连接对端的时间戳,拒收时间戳比已记录值更小的报文。

关键在于 tcp_tw_recycle:它开启后,为了快速回收 TIME_WAIT,内核会把"最近见过的时间戳"按对端 IP(per-host)缓存到路由项里,而不是 per-connection。于是问题来了——当多个真实客户端藏在同一个 NAT 出口 IP 后面,它们各自的开机时间不同、时间戳时钟互相独立。服务器只认这个源 IP 上"最后见过的最大时间戳",一旦某个客户端的时间戳恰好小于另一个客户端刚刚上报的值,它的 SYN 就会被 PAWS 判定为"旧包"而静默丢弃

这完美解释了所有现象:NAT 后方用户共享 IP → 时间戳乱序 → 部分 SYN 被丢 → 间歇性、只影响 NAT 群体、应用无感。

第三步:实验验证。 拿两台开机时间差异大的测试机,配到同一台 NAT 网关后共享出口 IP,交替向服务器发起连接,服务器端 passive connection rejections because of time stamp 计数器应声跳动,故障稳定复现。反之临时执行 sysctl -w net.ipv4.tcp_tw_recycle=0 后,同样的交替连接全部成功,计数器不再增长。根因坐实。

解决方案

止血非常直接——关闭 tcp_tw_recycle

1
2
3
4
5
# 立即生效
sysctl -w net.ipv4.tcp_tw_recycle=0
# 从 /etc/sysctl.conf 删除或注释该行,防止重启复活
# net.ipv4.tcp_tw_recycle = 1   <-- 删掉
sysctl -p

关闭后计数器立刻停止增长,NAT 后方用户投诉当天清零。

那当初想解决的 TIME_WAIT 问题怎么办?正确姿势是:

  • tcp_tw_reuse=1 可以保留:它只影响**主动发起连接的一方(客户端角色)**复用 TIME_WAIT 端口,基于 per-connection 时间戳判断,对 NAT 场景安全。
  • 服务端作为被连接方,TIME_WAIT 本就该由内核自然回收,靠调大 ip_local_port_range、缩短 tcp_fin_timeout 等参数远比 tw_recycle 稳妥。
  • 真正要削减 TIME_WAIT,治本是上游用长连接/连接池(如 Nginx upstream keepalive),从源头减少短连接数量。

需要说明:tcp_tw_recycle 因为这个坑,已在 Linux 4.12 内核彻底移除。如果你还在维护 3.x/4.4 一类老内核(CentOS 7 等),务必检查这个参数。

根因分析

根因是对内核参数语义的误解 + NAT 环境的水土不服tcp_tw_recycle 的时间戳缓存是 per-host 而非 per-connection,这在"一个 IP = 一个主机"的年代尚可,但在 NAT/CGNAT 普及的今天,“一个 IP 后面藏着成百上千台时钟各异的主机"是常态,per-host 假设彻底崩塌,直接导致合法 SYN 被误杀。而丢包发生在内核 PAWS 检查阶段、不落应用日志、只打薄薄一个 netstat 计数器,隐蔽性极强,是典型的"沉默杀手”。

预防措施

  • 不要照抄网上的"内核调优大全":每个 sysctl 参数都要理解语义和适用边界再上线,尤其是涉及 TCP 状态机的。
  • 明确区分 tcp_tw_reusetcp_tw_recycle:前者(客户端复用,安全)可用,后者(NAT 杀手)禁用;新内核已无 recycle,老内核务必置 0。
  • netstat -s 纳入监控:对 passive connection rejections because of time stamppackets rejects ... because of timestamp 等计数器做增长告警,这类内核级丢包是应用监控的盲区。
  • 变更留痕 + 灰度:sysctl 调整应走变更流程、小范围灰度并观察 NAT 客户群指标,而非全量直接改。
  • 削减 TIME_WAIT 走正道:优先长连接/连接池,而不是靠激进的 TIME_WAIT 快速回收参数。

总结

这次故障的教训在于:一个看似人畜无害的"TCP 调优"参数,在 NAT 普及的真实网络里会把合法用户的 SYN 悄悄丢掉,且现象诡异、只打一个冷门计数器。排查的突破口是 netstat -s 里那句"rejects because of timestamp",它把问题从"网络玄学"一步拉回到 PAWS + per-host 时间戳缓存的确定性机制上。记住一条原则:tcp_tw_recycle 在任何有 NAT 的环境都不要开——而现在,几乎没有环境是没有 NAT 的。

使用 Hugo 构建
主题 StackJimmy 设计