凌晨两点半,连着敲了几个小时键盘,屏幕的光刺得眼睛发酸。刚才的一小时里,生产环境的 K8s 集群经历了一场诡异的局部断网。
告警邮件像雪花一样飞进邮箱:API Gateway 报 504 Gateway Timeout,Ingress Controller 侧出现大量连接超时。诡异的是,这并不是全局瘫痪,而是处于一种“薛定谔的连通”状态——同一个客户端发起的请求,上个请求秒回,下个请求就一直卡到超时。
起初排查方向定在网络插件上。我登上出问题的 Node 节点,看了一下 Calico 的状态,一切正常。接着去抓包:
tcpdump -i any -nn tcp and port 8080
抓包结果显示了一个极其经典的现象:Client 端(在这个场景里是 Ingress Pod)发出了 SYN 握手包,后端的业务 Pod 所在的 Node 网卡明明收到了这个 SYN,但就是不回 SYN-ACK。内核直接把包悄无声息地吞了。
如果不是防火墙拦截,能让 Linux 内核在收到 SYN 时不响应、也不发 RST 的情况并不多。全连接队列满了?查了一下 ss -lnt,并没有溢出。半连接队列满了?netstat -s | grep "SYNs to LISTEN" 也没有异常增长。
直觉告诉我,得看看网络协议栈的丢包统计。
nstat -az | grep PAWS
输出的结果让人心梗:TcpExtPAWSActive 这个计数器正在以每秒几十次的速度疯狂飙升。
PAWS(Protection Against Wrapped Sequence numbers)被触发了。为什么会触发 PAWS?我顺手敲了一条命令:
sysctl -a | egrep 'tcp_tw_recycle|tcp_timestamps'
屏幕上赫然显示:
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_timestamps = 1
破案了。
我翻了一下配置管理的 Git 提交记录,就在今天下午,某位研发同事为了“优化高并发下的 TIME_WAIT 过多问题”,向运维提了个 MR,在 Ansible 剧本里加上了这行 net.ipv4.tcp_tw_recycle = 1,并在晚高峰前推到了全网节点。他甚至还在提交注释里贴了一篇 2014 年的 CSDN 博客链接。
这就是典型的“知其然不知其所以然”导致的灾难。在单机裸奔的年代,开启 tcp_tw_recycle 确实能快速回收 TIME_WAIT 状态的套接字。但在这个充斥着 NAT 和容器的微服务时代,这个参数就是一枚定时炸弹。
技术原理解析:为什么在 K8s 中绝不能开启 tcp_tw_recycle?
当 tcp_tw_recycle 和 tcp_timestamps 同时开启时,Linux 内核会启用一种被称为 per-host 的 PAWS 机制。内核会记录每个来源 IP 地址最后一次发来数据包的 TCP 时间戳(Timestamp)。如果同一个来源 IP 发来的新 SYN 包,其时间戳小于内核记录的该 IP 的上一次时间戳,内核就会认为这是一个重放的旧包,直接丢弃。
在普通的直连网络里,这没问题,因为同一台机器的 CPU 时钟是单调递增的。但是,K8s 里的流量是经过 NAT 的。
当外部请求通过 NodePort 进来,或者在集群内部通过 Service IP(kube-proxy 的 iptables/IPVS 规则)进行转发时,通常会发生 SNAT(源地址转换)。这就导致后端的业务 Node 看来,所有的请求都来自同一个 IP(比如网关 Node 的 IP,或者 Flannel/Calico 的网段网关 IP)。
然而,这些请求实际上是由成百上千个不同的真实的 Client 发出的。每个 Client 的系统启动时间不同,它们生成的 TCP 时间戳自然也不同,毫无顺序可言。
结果就是:后端 Node 刚收到 Client A(时间戳 10000)的请求并记录下来,紧接着收到了 Client B(时间戳 5000)的请求。因为它们经过 SNAT 后源 IP 一样,内核一看,时间戳居然倒退了(5000 < 10000),于是默默把 Client B 的 SYN 包丢进黑洞。Client B 只能干等超时,而业务代码里连个报错的影子都看不到。
解决过程毫无波澜,直接通过 DaemonSet 刷了一波内核参数,关闭它:
sysctl -w net.ipv4.tcp_tw_recycle=0
敲下回车,几秒钟后,监控大盘上的红色断崖式下跌,504 归零,世界清静了。
面对 TIME_WAIT 焦虑症,很多人喜欢在网上乱搜“高并发 Linux 内核优化大全”,然后不加思索地把那堆参数一把梭。但技术这东西,脱离了架构拓扑谈参数,就是耍流氓。事实上,因为 tcp_tw_recycle 在 NAT 环境下的表现太糟糕,Linux Kernel 社区在 4.12 版本中已经彻底将其废弃移除了。如果你们的宿主机内核还在用 3.10(比如某著名的 CentOS 7),请对这些老旧的参数保持最起码的敬畏心。
真正合理的 TIME_WAIT 优化,绝不是靠暴力的 recycle 机制,而是开启 tcp_tw_reuse(只对发起连接的客户端有效),调大 tcp_max_tw_buckets,或者更彻底一点——在应用层用好连接池,开启 HTTP Keep-Alive,从源头上减少短连接的创建。
系统调优不是背书,哪怕是抄,也请先搞清楚你的网络拓扑里有没有 NAT。关了终端,收工。