一次由 tcp_tw_recycle 引发的"灵异"故障排查:基于双网闸架构的踩坑记
背景
最近在排查一个线上服务的网络连接问题,现象非常"诡异":
- 服务端和客户端逻辑都没有明显改动。
- 大部分请求正常,但偶尔会出现连接建立失败、超时。
- 抓包发现 TCP 握手阶段有异常,部分 SYN 包被直接丢弃,没有回应。
- 问题不是必现,但一到业务高峰期就零星出现,让人非常头疼。
经过一系列排查,最终定位到是 Linux 老版本内核参数 tcp_tw_recycle 在作祟。修改后,问题立刻消失。
这篇文章记录一下整个排查思路和背后的原理,希望能帮到遇到类似问题的同学。
网络拓扑与问题现象
我们的网络架构比较特殊,采用了双网闸(安全隔离与信息交换系统) 进行内外网隔离,拓扑简化如下:
- 内网核心 :
T:1/3/0/42, VLAN3102,IP10.10.10.1/24 - 内-外网闸 (172.16.1.156):
- 内网业务口
t1/17:10.10.10.2/30 - 外网链接发起口
t1/17:10.10.20.2/24
- 内网业务口
- 外网 AF :
eth18:10.10.20.1/24eth19:10.10.30.1/30
- 外-内网闸 (172.16.1.157):
- 外网业务口
t1/17:10.10.30.2/30 - 内网链接发起口
t1/17下:10.10.10.3/24
- 外网业务口
数据流向 :
内网核心(10.10.10.1)→ 内-外网闸(10.10.20.2)→ 外网 AF(10.10.20.1)→ 外网 AF(10.10.30.1)→ 外-内网闸(10.10.30.2)→ 内-外网闸(10.10.10.2)→ 内网核心(10.10.10.1)。
故障现象 :
在这种双网闸、多段 NAT 的架构下,内网核心访问外网业务时,部分连接会卡在 SYN 阶段,服务端没有回应 SYN-ACK,也没有 RST。客户端重试几次后,部分连接成功,部分彻底失败。
排查过程
1. 初步怀疑:网络抖动?服务端负载?
一开始怀疑是网络抖动或者服务端负载过高。但查看监控:
- 服务端 CPU、内存、带宽都正常。
- 网闸设备没有丢包告警。
- 问题出现在多个客户端,但又不是全部客户端。
2. 抓包分析:SYN 包被丢弃
在内网核心 (10.10.10.1)和外网 AF(10.10.20.1)同时抓包,发现一个关键现象:
- 内网核心发出 SYN 包。
- 经过内-外网闸后,源 IP 被 NAT 成
10.10.20.2。 - 外网 AF 收到 SYN 包,但没有回应 SYN-ACK。
- 内网核心重试几次后,部分连接成功,部分彻底失败。
这看起来像是外网 AF 或外-内网闸直接把 SYN 包丢了,而不是应用层的问题。
3. 关键验证:关闭 TCP 时间戳后恢复正常
在排查过程中,发现了一个非常关键的验证现象:
- 如果把内网所有访问该外网服务器的客户端 的 TCP 时间戳(
tcp_timestamps)全部关闭,连接完全正常。 - 但只要其中任意一台服务器重新开启时间戳访问,问题就会再次出现。
这个现象几乎直接锁定了问题根源:不是网络设备丢包,而是服务端内核根据 TCP 时间戳做了某种"过滤"。
4. 聚焦内核参数:tcp_tw_recycle
查阅资料后,怀疑到 tcp_tw_recycle 这个参数。
先确认当前内核版本和参数值:
bash
uname -r
# 输出:3.10.0-xxx.el7.x86_64 (老版本内核)
sysctl net.ipv4.tcp_tw_recycle
# 输出:net.ipv4.tcp_tw_recycle = 1
果然,tcp_tw_recycle 被开启了。
什么是 tcp_tw_recycle?
tcp_tw_recycle 是 Linux 内核的一个参数,用于快速回收 TIME_WAIT 状态的 socket。
-
默认值:0(关闭)
-
开启后:内核会使用更激进的策略回收 TIME_WAIT,减少端口占用。
听起来很美好,但它有一个致命缺陷:
在 NAT 环境下,tcp_tw_recycle 会导致部分客户端连接被丢弃。
为什么 NAT 环境下会出问题?
- tcp_tw_recycle 依赖 TCP 时间戳(tcp_timestamps) 来判断连接的新旧。
开启 tcp_tw_recycle 后,内核会记录每个源 IP 的最近时间戳(per-host PAWS)。
- 如果同一个源 IP 发来的新连接,时间戳 小于 之前记录的最近时间戳,内核会认为这是一个"过时"的包,直接丢弃。
问题来了:
在 NAT 环境下,多个客户端共享同一个源 IP(对服务端而言)。
不同客户端的系统时间戳(TCP TSval)可能不同步(有的快,有的慢)。
如果客户端 A 的时间戳较大,客户端 B 的时间戳较小,B 的 SYN 包就会被服务端判定为"过时",直接丢弃。
在我们的拓扑中:
内网核心(10.10.10.1)经过内-外网闸后,源 IP 被 NAT 成 10.10.20.2。外网 AF(10.10.20.1)作为服务端,看到的所有连接都来自 10.10.20.2。如果内网有多台服务器(或同一客户端多次连接),只要其中一台开启了 TCP 时间戳,并且它的 TSval 比之前记录的更大,就会拉高服务端为 10.10.20.2 维护的"最近时间戳"。之后其他时间戳较小的客户端再发起连接,就会被直接丢弃。
这就是为什么:
问题不是必现,而是部分客户端、部分时间出现。
全部关闭时间戳就正常(服务端不再做 PAWS 校验)。
只要有一台开启时间戳就会出问题(它会"污染"服务端为该 NAT 源 IP 维护的时间戳水位)。
解决方案
修改内核参数,关闭 tcp_tw_recycle:
bash
sysctl -w net.ipv4.tcp_tw_recycle=0
永久生效:
bash
echo "net.ipv4.tcp_tw_recycle = 0" >> /etc/sysctl.conf
sysctl -p
修改后,问题立刻消失,连接成功率恢复正常。
- 注意:关闭 tcp_tw_recycle 是根治方案。关闭客户端时间戳只是临时规避手段,且会带来其他副作用(如无法使用 RTTM、PAWS 保护减弱等),不建议作为长期方案。
为什么老版本内核更需要注意?
在 Linux 4.12 之后,tcp_tw_recycle 已经被彻底移除。
但在 CentOS 7、早期 Ubuntu 等老版本内核中,这个参数依然存在,且可能被一些"优化教程"建议开启。
很多运维人员为了减少 TIME_WAIT,会盲目开启 tcp_tw_recycle,结果在 NAT 环境下踩坑。
经验总结
不要盲目开启 tcp_tw_recycle,尤其是在 NAT 环境下。
如果必须优化 TIME_WAIT,优先考虑:
开启 tcp_tw_reuse(相对安全)
调整 tcp_max_tw_buckets
使用长连接
老版本内核升级前,务必检查这个参数。
排查网络问题时,抓包 + 内核参数检查是黄金组合。
在双网闸、多段 NAT 架构下,尤其要注意 TCP 时间戳和 tcp_tw_recycle 的相互作用。
"关闭所有客户端时间戳就正常,开启任意一台就复现" 是 tcp_tw_recycle 在 NAT 环境下引发问题的典型特征,可作为快速定位依据。
- 参考
Linux 内核文档:Documentation/networking/ip-sysctl.txt
RFC 1323:TCP Extensions for High Performance
关于 tcp_tw_recycle 被移除的 commit