导语:STGW作为公司七层接入网关,在云和自研业务中承担多种网络协议接入与转发的功能,由于业务数量庞大、接入形式多样、网络环境复杂,会遇到一些很有挑战的疑难杂症。某次业务出现了流量突然下降,此时用户侧也有延迟上升和重试增多的问题。在团队自研的秒级监控助力下,我们从CPU软中断热点入手追查,发现了内核listen port哈希机制存在消耗过高问题,但热点只出现在部分核心上,接着在网卡多队列、内核Receive Packet Steering(RPS)上发现了负载均衡策略的缺陷,找出最终原因后我们在硬件和内核层面都做出了解决方案,并在现网进行了修复。
问题现象
在STGW现网运营中,出现了一起流量突然下降的Case,此时我们的健康拨测机制探测到失败,并且用户侧重试次数增多、请求延迟增大。但通过已有的各类监控进行定位,只发现整体CPU、内存、进程状态、QPS(每秒请求数)等关键指标虽然出现波动,但均未超过告警水位。
如图,流量出现了跌幅,并且出现健康检查拨测失败。
但是,整体CPU在流量出现缺口的期间,并未超过阈值,反而有一些下降,随后因为恢复正常流量冲高才出现一个小毛刺。
此外,内存和应用层监控,都没有发现明显异常。
前期探索
显然,仅凭这些常规监控,无法定位问题根本原因,尽量拿到更多的问题信息,成为了当务之急。幸运的是,从STGW自研的秒级监控系统中,我们查到了一些关键的信息。
在STGW自研的监控系统里,我们增加了核心资源细粒度监控,针对CPU、内存、内核网络协议栈这些核心指标支持秒级监控、监控指标更细化,如下图就是出问题时间段,cpu各个核心的秒级消耗情况。
通过STGW CPU细粒度监控展示的信息,可以看到在出现问题的时间段内,部分CPU核被跑满,并且是由于软中断消耗造成,回溯整个问题时间段,我们还发现,在一段长时间内,这种软中断热点偏高都会在几个固定的核上出现,不会转移给其他核。
此外,STGW的监控模块支持在出现系统核心资源异常时,抓取当时的函数调用栈信息,有了函数调用信息,我们能更准确的知道是什么造成了系统核心资源异常,而不是继续猜想。如图展示了STGW监控抓到的函数调用及cpu占比信息:
通过函数栈监控信息,我们发现了inet_lookup_listener函数是当时CPU软中断热点的主要消耗者。出现问题时,其他函数调用在没有发生多少变化情况下,inet_lookup_listener由原本很微小的cpu消耗占比,一下子冲到了TOP1。
通过这里,我们可以初步确定,inet_lookup_listener消耗过高跟软中断热点强相关,当热点将cpu单核跑满后就可能引发出流量有损的问题。由于软中断热点持续在产生,线上稳定性隐患很大。基于这个紧迫的稳定性问题,我们从为什么产生热点、为什么热点只在部分cpu core上出现两个方向,进行了问题分析、定位和解决。
为什么产生了热点
1. 探秘 inet_lookup_listener
由于perf已经给我们提供了热点所在,首先从热点函数入手进行分析,结合内核代码得知,__inet_lookup系列函数是用于将收到的数据包定位到一个具体的socket上,但只有握手包会进入到找__inet_lookup_listener的逻辑,大部分数据包是通过__inet_lookup_established寻找socket。
具体分析lookup_listener的代码我们发现,由于listen socket不具备四元组特征,因此内核只能用监听端口计算一个哈希值,并使用了 listening_hash 哈希桶存起来,握手包发过来的时候,就从该哈希桶中寻找对应的listen socket。
struct sock *__inet_lookup_listener(struct net *net, struct inet_hashinfo *hashinfo, const __be32 saddr, __be16 sport, const __be32 daddr, const unsigned short hnum, const int dif){// 省略了部分代码// 获取listen fd 哈希桶 struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash]; result = NULL; hiscore = 0;// 遍历桶中的节点 sk_nulls_for_each_rcu(sk, node, &ilb->head) { score = compute_score(sk, net, hnum, daddr, dif); if (score > hiscore) { result = sk; hiscore = score; reuseport = sk->sk_reuseport; if (reuseport) { phash = inet_ehashfn(net, daddr, hnum, saddr, sport); matches = 1; } } else if (score == hiscore && reuseport) { matches++; if (((u64)phash * matches) >> 32 == 0) result = sk; phash = next_pseudo_random32(phash); } }}
相对来说并不复杂的lookup_listener函数为什么会造成这么大的cpu开销?
经过进一步定位后,发现问题所在:listen哈希桶开的太小了,只有32个。
/* This is for listening sockets, thus all sockets which possess wildcards. */#define INET_LHTABLE_SIZE 32 /