凌晨两点半,机房的VPN还没挂断,手里这杯美式已经彻底凉透了。
刚才过去的两个小时,我一直在盯着监控大盘上那条像心电图一样剧烈抖动的502报错曲线。业务群里还在嗡嗡作响,几个前端和产品经理在艾特后端开发,后端开发又在群里艾特我,嚷嚷着“K8S网络又抽风了”、“容器丢包了”。
我没有理会群里的喧哗,直接连上了出问题的Node节点。多年的直觉告诉我,系统层面不会无缘无故丢包,如果在CPU、内存、IO吞吐都平稳的情况下出现大面积的网络中断,去查查内核底层的网络栈绝对没错。
敲下第一条命令:
dmesg -T | grep -i conntrack
屏幕上立刻滚出了一大片刺眼的红字:
[Fri Oct 27 01:15:32 2023] nf_conntrack: table full, dropping packet
很好,案子破了一半。Netfilter的连接跟踪表被打满了,内核为了自保,正在无差别地丢弃新来的网络包。
但这只是表象,是什么东西在疯狂消耗连接跟踪表?我紧接着看了一眼当前的TCP状态分布:
ss -s
输出结果简直让人脑溢血:
Total: 310524
TCP: 298512 (estab 120, closed 295000, orphaned 0, timewait 294800)
单台机器上,竟然堆积了接近30万个TIME_WAIT状态的连接。
我切进那个引发报警的业务Pod,调出对应的业务日志和代码仓库。看了不到三分钟,我感到一种难以名状的荒谬感。
我们的某位“资深”微服务开发同事,写了一个定时同步下游配置的后台任务。这本没什么,但他用的是原生的HTTP Client,没有开启Keep-Alive,没有连接池限制,以每秒钟将近4000次的频率,向同集群的另一个Service发起短连接轮询。
在这个技术点上犯错,是极其业余且不可原谅的。
任何一个对TCP/IP协议栈有基本敬畏心的人都应该知道,TCP四次挥手后,主动关闭连接的一方会进入TIME_WAIT状态,这个状态必须等待2 * MSL(Maximum Segment Lifetime,Linux默认通常是60秒)才会彻底释放。
在K8S环境下,情况会变得更糟。Pod之间的跨Service通信,不论是走iptables还是IPVS,都会经过NAT(网络地址转换)。每一次新建短连接,Netfilter都要在nf_conntrack表里为这个连接分配一个条目(Entry)来记录状态。
你每秒钟打出4000个短连接,60秒内就会产生24万个无法回收的TIME_WAIT套接字。而这台宿主机上分配的临时端口范围(net.ipv4.ip_local_port_range)默认只有大约28000个。端口被耗尽,连接跟踪表被塞满,宿主机上的所有Pod——包括正常处理用户请求的网关——全部跟着一起陪葬,连DNS解析的UDP包都被内核直接Drop掉。
开发把操作系统当成了一个拥有无限资源的黑盒,以为只要代码不报Exception,底层就会永远默默兜底。
我叹了口气,先解决眼下的可用性问题。在Node节点上热加载了内核参数,强行抬高conntrack上限,并加速复用:
# 临时调大连接跟踪表大小
sysctl -w net.netfilter.nf_conntrack_max=1048576
# 允许将TIME-WAIT sockets重新用于新的TCP连接
sysctl -w net.ipv4.tcp_tw_reuse=1
# 缩短TIME_WAIT在conntrack中的保留时间(注意:仅限特定内核版本或已合入相关patch的系统)
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
配置刷下去,监控曲线在30秒内拉平,502报错归零。群里安静了,开发甚至还发了个“赞”的表情,仿佛危机已经完美解除。
但我很清楚,调优内核参数永远只是在给糟糕的架构擦屁股。
我关掉终端,在群里甩下了一段复盘结论作为今晚的结束语,然后合上电脑。
技术结论:
1. 永远不要在核心链路或高频调用中使用短连接。 HTTP/1.1必须显式复用TCP连接(开启Keep-Alive),或者直接升级到HTTP/2、gRPC进行多路复用。连接池(Connection Pool)是分布式系统的保命底线。
2. 不要盲目迷信网上的“优化大全”去开启net.ipv4.tcp_tw_recycle。在Linux Kernel 4.12版本之后,这个参数已经被彻底废弃。在NAT环境下(如K8S),开启它会导致时间戳错乱,直接造成正常的TCP握手被静默丢弃(PAWS机制引发)。
3. 当你遇到nf_conntrack: table full时,调大nf_conntrack_max只是续命。根本解法只有两个:要么优化业务逻辑消除不合理的并发短连接;要么在网络层对高频流量执行NOTRACK规则(-j NOTRACK),让其绕过连接跟踪机制。
底层不相信魔法,代码的每一丝愚蠢,最终都会在内核的计数器里精准变现。