看着窗外上午十点半的阳光,我原本打算喝完手里的这杯黑咖,顺便把上周搁置的底层存储架构图画完。结果监控大屏上突然跳出的刺眼红光,硬生生把我拉回了现实。
核心交易链路的 API Gateway 突然报出大面积的 502 Bad Gateway,监控曲线上 Ingress 到后端业务 Pod 的建连超时率呈指数级上升。
遇到这种突发流量下跌,第一直觉是后端服务被打挂了。但我切到业务容器的监控面板一看,CPU、内存水位平稳得像一潭死水,甚至连 Load 都没有明显波动。服务没挂,但流量进不去。
我直接登上一台发生报错的 Kubernetes Worker 节点,挑了一个业务 Pod 的 IP,挂上 tcpdump 抓包:
tcpdump -i any host 10.244.3.15 -nn -c 100
屏幕上滚动的报文印证了我的猜想:大量的 SYN 包发向了 Pod 的 8080 端口,但后端业务容器就像个黑洞,一个 SYN-ACK 都没有回。网络层丢包了。
是宿主机网卡满了?还是 conntrack 表爆了?我顺手敲了条命令检查系统网络统计:
netstat -s | grep timestamp
输出结果直接让我瞳孔地震:
234561 passive connections rejected because of time stamp
并且这个数字还在以每秒几千的速度疯狂飙升。
看到这个提示,我已经知道问题出在哪了。我冷着脸敲下 sysctl -a | grep tcp_tw,果不其然,屏幕上赫然写着:
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_tw_reuse = 1
我立刻在部门群里问了一句:“今天早上谁在生产环境的 Worker 节点上跑了内核参数修改的剧本?”
过了一会儿,一个研发同事弱弱地冒泡,说他早上看压测报告,发现有大量的 TIME_WAIT 状态连接,觉得这会消耗系统资源,于是去网上搜了一篇“高并发内核优化指南”,顺手用 Ansible 把所有节点的 tcp_tw_recycle 给打开了,希望能“快速回收”这些连接。
我深吸了一口气,把刚冲好的咖啡放在桌上。这种操作,放在十年前的物理机直连时代或许还能混过去,但在如今的云原生和容器化网络环境下,这简直是在给系统投毒。
为什么说在 Kubernetes 环境下开启 tcp_tw_recycle 是一个极其愚蠢且不可原谅的错误?
这里必须要把底层的技术逻辑扯清楚。tcp_tw_recycle 这个参数的设计初衷,是为了在开启 tcp_timestamps 的前提下,利用 PAWS(Protect Against Wrapped Sequence numbers)机制来快速回收 TIME_WAIT 状态的 socket。它的核心判断逻辑是:如果同一个源 IP 发来的新连接请求,其 TCP SYN 包里的 timestamp 值小于该源 IP 上一次通信的 timestamp,内核就会认为这是一个迟到的、过期的乱序包,并直接在底层静默丢弃(Drop)掉。
听起来很完美对吧?但这位同事完全忽略了我们所处的网络拓扑。
在 Kubernetes 中,无论是 Ingress 转发、Service 的 kube-proxy (iptables/IPVS),还是跨 Node 的 Pod 通信,都充斥着大量的 NAT(网络地址转换)和 SNAT(源地址转换)。
当外部用户的海量请求经过 Gateway 或者 NAT 网关打到后端 Pod 时,在后端 Pod 看来,这些请求的源 IP 全部被替换成了同一个 IP(比如 NAT 网关的 IP 或 Node IP)。
但是!这些请求实际上来自于无数个不同的真实客户端,它们各自机器上的系统时钟和 TCP timestamp 根本不可能是一致的!
结果就是:后端 Pod 所在的宿主机内核,看到来自“同一个源 IP”的 SYN 包,其 timestamp 忽大忽小、疯狂乱跳。一旦某个请求的 timestamp 小于前一个请求的 timestamp,内核直接触发 PAWS 机制,将 SYN 包丢弃。前端 Gateway 收不到 ACK,只能不断重传,最终引发大面积的建连超时和 502 错误。
更讽刺的是,稍微关注点 Linux Kernel 演进的人都会知道,由于 NAT 环境的普及,tcp_tw_recycle 这个参数引发的血案实在太多,Linus Torvalds 已经在 Linux Kernel 4.12 版本(2017年)中,直接把这个参数从内核代码里彻底删除了。
我们生产环境之所以还能执行成功,仅仅是因为这批机器还在使用 CentOS 7 时代的 3.10 老内核。拿着 2017 年就被废弃的机制,去“优化” 2024 年的微服务架构,这种不加甄别直接复制粘贴技术博客的行为,是运维和架构领域的大忌。
我没有在群里继续长篇大论,只是扔了一行命令让运维小哥立刻执行回滚:
ansible worker_nodes -m shell -a "sysctl -w net.ipv4.tcp_tw_recycle=0"
不到一分钟,监控大屏上的 502 报错断崖式下跌,业务全面恢复。
最后的技术结论:
面对高并发下的 TIME_WAIT 飙升,正确的处理思路永远是往上看,而不是往下瞎搞内核。
1. TIME_WAIT 是 TCP 协议的正常状态,不是 Bug。 它的存在是为了保证全双工连接的可靠关闭和防止延迟报文污染新连接。
2. 只要系统内存充裕,几万个 TIME_WAIT 对现代操作系统的消耗微乎其微。
3. 如果真的觉得 TIME_WAIT 太多,去查应用层的连接池配置!检查 HTTP 客户端和服务器是否正确开启了 Keep-Alive,复用长连接才是解决短连接导致 TIME_WAIT 过多的根本途径。
4. 在任何涉及 NAT / 负载均衡 / 容器网络的架构下,绝对、永远、不要开启 tcp_tw_recycle。开启 tcp_tw_reuse 配合 tcp_timestamps 已经足够应付绝大多数需要复用本地端口的场景。
收拾完这个烂摊子,咖啡已经凉了。我重新敲开终端,把清理老旧系统参数的检测脚本加到了下周的巡检计划里。系统可以容忍高负载,但架构的稳定性,绝对不能建立在对底层原理一知半解的“优化”上。