凌晨三点半,周围安静得只能听到机房通风管道的低鸣。我刚把监控大盘上的P0级红色警报按下去,桌上的咖啡早凉透了。
就在两个小时前,生产环境的Kubernetes集群爆发了诡异的级联故障。最先是API Gateway的P99延迟毫无预兆地飙升到了5秒以上,接着几台承载核心流量的Ingress节点相继进入 NotReady 状态。
第一时间登录堡垒机,切到其中一台出问题的节点。敲下 top,发现用户态CPU负载极低,但 si(SoftIRQ,软中断)直接吃满了单个核心,ksoftirqd 进程赫然顶在最上面。
网络层出问题了。
敲下 sar -n ETCP 1,并没有看到异常规模的TCP连接重试或SYN Flood攻击。接着用 perf top 看了一下内核态的热点函数,屏幕上跳出的结果直接让我血压飙升:
__nf_conntrack_find_get 占用了将近 60% 的CPU开销。
看到这个函数,我心里基本有底了。连接跟踪(conntrack)在处理包时卡住了。但我很纳闷,如果是正常的并发激增导致表满,系统日志里应该会狂刷 nf_conntrack: table full, dropping packet。但我切过去看 dmesg -T,干干净净,一条丢包日志都没有。
既然没丢包,为什么查表开销会这么大?
顺手查了一下当前的连接数和内核参数:
# sysctl net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_max = 8388608
# cat /sys/module/nf_conntrack/parameters/hashsize
65536
看到这两个数字的瞬间,我几乎对着屏幕气笑了。这绝对是人为制造的灾难。
查阅了一下昨天下午的工单记录,果不其然。某位同事在常规巡检时,发现日志里偶尔出现了零星的 conntrack table full 告警。为了“一劳永逸”地解决问题,这位老兄直接去网上抄了一行配置,把 nf_conntrack_max 从默认的 262144 暴力拉大到了 8388608。
他以为他做了一次完美的性能调优,实际上他在内核里埋了一颗滴答作响的炸弹。
为什么说这种操作愚蠢得不可原谅?只要稍微懂一点基础的数据结构,或者翻过一点内核源码就应该知道,Linux的连接跟踪机制底层维护的是一个哈希表。
hashsize 决定了哈希桶(buckets)的数量,而 nf_conntrack_max 决定了表中能容纳的条目总数。每一个进入协议栈的数据包,都要经过这个哈希表去确认是否属于已有连接。
当发生哈希冲突时,内核会怎么做?它会用链表把冲突的条目串起来。
这位同事把最大条目数调到了 8388608,却根本没有去动默认的哈希桶数量(65536)。这意味着什么?
这意味着当系统连接数真正涨上来的时候,每个哈希桶后面的链表平均长度会达到:
8388608 / 65536 = 128
对于高并发的网关节点,每秒钟有上百万个数据包穿过网卡。每一个数据包到达网络层,内核都要在软中断上下文中,持有自旋锁(spinlock),去遍历一条长度为 128 的链表。
这种把 O(1) 的哈希查找,生生劣化成 O(N) 的长链表遍历的操作,直接让CPU在软中断里原地爆炸。网络包处理不过来,网卡的Ring Buffer被打满,后续的包被静默丢弃,导致TCP严重重传,Kubelet与APIServer的心跳包也发不出去,节点直接被判定为离线。
解决这个烂摊子只需要一行命令:
echo 2097152 > /sys/module/nf_conntrack/parameters/hashsize
把哈希桶的数量拉上来,将链表长度控制在理想的 1:4 甚至 1:1 左右。敲下回车不到两秒钟,ksoftirqd 的CPU占用率断崖式下跌,集群节点的 Ready 状态陆续恢复,API的延迟曲线也重新平贴到了底线。
故障是排除了,但这种盲目复制粘贴的运维习惯实在让人厌恶。
技术结论其实很简单:永远不要在不理解底层数据结构的情况下,去随意拨弄内核参数。
nf_conntrack 的调优从来不是单维度的扩容,max 和 hashsize 必须成比例配置。Linux内核是一台精密咬合的机器,你以为你在给它加油,实际上你往它的齿轮里倒了一把沙子。
天快亮了,写完这份RCA(根因分析)报告,发到工作群里。希望能让一些人长点记性。