從STGW流量下降探秘內核收包機制
問題現象
在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 /* Yes, really, this is all you need. */
為什么內核這里會覺得listen哈希桶大小32就滿足需要了呢?
在IETF(互聯網工程任務組)關于端口的規劃中,0-1023是System port,系統保留使用,1024-49151為Registered port,為IANA(互聯網數字分配機構)可分配給一些固定應用,49152-65535是Dynamic port,是可以真正自由使用的。當然了,這只是IETF的一個規劃,在Linux中,除了System port,另兩個端口段并未真的做了明顯區分,除非端口已經被占用,用戶可以自由使用,這里提一個Linux中跟端口劃分有關聯的內核參數:ip_local_port_range,它表示系統在建立TCP/UDP連接時,系統給它們分配的端口范圍,默認的ip_local_port_range值是32768-60999,進行這個設置后好處是,61000~65535端口是可以更安全的用來做為服務器監聽,而不用擔心一些TCP連接將其占用。
因此,在正常的情況下,服務器的listen port數量,大概就是幾w個這樣的量級。這種量級下,一個port對應一個socket,哈希桶大小為32是可以接受的。然而在內核支持了reuseport并且被廣泛使用后,情況就不一樣了,在多進程架構里,listen port對應的socket數量,是會被幾十倍的放大的。以應用層監聽了5000個端口,reuseport 使用了50個cpu核心為例,5000*50/32約等于7812,意味著每次握手包到來時,光是查找listen socket,就需要遍歷7800多次。隨著機器硬件性能越來越強,應用層使用的cpu數量增多,這個問題還會繼續加劇。
正因為上述原因,并且我們現網機器開啟了reuseport,在端口數量較多的機器里,inet_lookup_listener的哈希桶大小太小,遍歷過程消耗了cpu,導致出現了函數熱點。
2. 如何解決__inet_lookup_listener問題
Linux社區難道沒有注意到開啟reuseport后,原來的哈希桶大小不夠用這個問題嗎?
其實社區是注意到了這個問題的,并且有修復這個問題。
從Linux 4.17開始,Linux社區就修復了由于reuseport帶來的socket數量過多,導致inet_lookup_listener查找緩慢的問題,修復方案分兩步:
1. 引入了兩次查找,首先還是根據目的端口進行哈希,接著會使用握手包中拿到的四元組信息,按照四元組進行第一次查找,如果四元組獲取不到結果,則使用之前那種對于任意IP地址查找。
- struct sock *__inet_lookup_listener(struct net *net,
- struct inet_hashinfo *hashinfo,
- struct sk_buff *skb, int doff,
- const __be32 saddr, __be16 sport,
- const __be32 daddr, const unsigned short hnum,
- const int dif, const int sdif)
- {
- struct inet_listen_hashbucket *ilb2;
- struct sock *result = NULL;
- unsigned int hash2;
- // 根據目的端口進行第一次哈希
- hash2 = ipv4_portaddr_hash(net, daddr, hnum);
- ilb2 = inet_lhash2_bucket(hashinfo, hash2);
- // 根據四元組信息再做一次查找
- result = inet_lhash2_lookup(net, ilb2, skb, doff,
- saddr, sport, daddr, hnum,
- dif, sdif);
- if (result)
- goto done;
- /* Lookup lhash2 with INADDR_ANY */
- // 四元組沒查到,嘗試在0.0.0.0監聽范圍查找
- hash2 = ipv4_portaddr_hash(net, htonl(INADDR_ANY), hnum);
- ilb2 = inet_lhash2_bucket(hashinfo, hash2);
- result = inet_lhash2_lookup(net, ilb2, skb, doff,
- saddr, sport, htonl(INADDR_ANY), hnum,
- dif, sdif);
- done:
- if (IS_ERR(result))
- return NULL;
- return result;
- }
2. 合并處理reuseport放大出來的socket,在發現指定的端口開啟了reuseport后,不再是遍歷式的去獲取到合適的socket,而是將其看成一個整體,二次哈希后,調用 reuseport_select_sock,取到合適的socket。
- static struct sock *inet_lhash2_lookup(struct net *net,
- struct inet_listen_hashbucket *ilb2,
- struct sk_buff *skb, int doff,
- const __be32 saddr, __be16 sport,
- const __be32 daddr, const unsigned short hnum,
- const int dif, const int sdif)
- {
- bool exact_dif = inet_exact_dif_match(net, skb);
- struct inet_connection_sock *icsk;
- struct sock *sk, *result = NULL;
- int score, hiscore = 0;
- u32 phash = 0;
- inet_lhash2_for_each_icsk_rcu(icsk, &ilb2->head) {
- sk = (struct sock *)icsk;
- score = compute_score(sk, net, hnum, daddr,
- dif, sdif, exact_dif);
- if (score > hiscore) {
- if (sk->sk_reuseport) {
- // 如果是reuseport,進行二次哈希查找
- phash = inet_ehashfn(net, daddr, hnum,
- saddr, sport);
- result = reuseport_select_sock(sk, phash,
- skb, doff);
- if (result)
- return result;
- }
- result = sk;
- hiscore = score;
- }
- }
- return result;
- }
總結來說,社區通過引入兩次查找+合并reuseport sockets的處理,解決了reuseport帶來的sockets數量放大效果。這里結合我們的探索,另外提供兩個可行的低成本解決方案:
1. 修改內核哈希桶大小,根據reuseport增加socket的倍數,相應提高INET_LHTABLE_SIZE,或者直接改成例如2048
- #define INET_LHTABLE_SIZE 2048
2. 關閉reuseport可以減少socket數目到32個哈希桶大小能承受的范圍,從而降低該函數消耗。
加上社區方案,這里的三個方法在本質上都是減少listen table哈希桶的遍歷復雜度。社區的方案一套比較系統的方法,今后隨著內核版本升級,肯定會將這個問題解決掉。但短期升級內核的成本較高,所以后面兩個方案就可以用來短期解決問題。此外,關閉reuseport雖然不需要更改內核,但需要考慮應用層server對于reuseport的依賴情況。
為什么熱點只在部分核心出現
解決完哈希桶問題后,我們并沒有定位到全部的問題,前面提到,軟中斷熱點僅在部分cpu核上出現,如果僅僅是__inet_lookup_listener問題,按理所有cpu核的軟中斷消耗都會偏高。如果這里問題沒有解釋清楚,一旦出現熱點函數,一些單核就會被跑滿,意味著整機容量強依賴部分單核的性能瓶頸,超出了單核能力就會有損,這是完全不能接受的。
1. 從CPU中斷數入手
根據問題現象,我們做了一些假設,在這里最直觀的假設就是,我們的數據包在各個核上并不是負載均衡的。
首先,通過cat /proc/interrupts找到網卡在各個cpu核的中斷數,發現網卡在各個核的硬中斷就已經不均衡了。那么會是硬中斷親和性的問題嗎?接著檢查了網卡各個隊列的smp_affiinity,發現每個隊列與cpu核都是一一對應并且互相錯開,硬中斷親和性設置沒有問題。
緊接著,我們排查了網卡,我們的網卡默認都打開了RSS(網卡多隊列),每個隊列綁定到一個核心上,既然硬中斷親和性沒有問題,那么會是網卡多隊列本身就不均衡嗎?通過ethtool -S eth0/eth1再過濾出每個rx_queue的收包數,我們得到如下圖:
原來網卡多隊列收包就已經嚴重不均衡了,以入包量升序排序,發現不同的rx_queue 收包數量相差達到了上萬倍!
2. 探究網卡多隊列(RSS)
這里我們著重檢查了幾個網卡多隊列的參數
- // 檢查網卡的隊列數
- ethtool -l eth0
- Current hardware settings:
- RX: 0
- TX: 0
- Other: 1
- Combined: 48
- // 檢查硬件哈希開關
- ethtool -k eth0
- receive-hashing: on
- // 檢查硬件哈希的參數,這里顯示以TCP是以四元組信息進行哈希
- ethtool -n eth0 rx-flow-hash tcp4
- TCP over IPV4 flows use these fields for computing Hash flow key:
- IP SA
- IP DA
- L4 bytes 0 & 1 [TCP/UDP src port]
- L4 bytes 2 & 3 [TCP/UDP dst port]
這些參數都是符合預期的,數據包會根據TCP包的四元組哈希到不同的隊列上。我們繼續使用假設論證法,會是數據包本身就是比如長連接,導致不均衡嗎?通過檢查我們服務端的日志,發現請求的ip和端口都是比較分散的,傳輸的數據也都是較小文件,并沒有集中化。
經過了一番折騰,我們有了新的假設,由于我們現網大部分流量是IPIP隧道及GRE封裝的數據包,在普通數據包的IP header上多了一層header,外層IP與我們在server看到的并不一樣,外層IP是非常集中的。這里是否會讓網卡多隊列的均衡策略失效呢?
來源網圖,以GRE包為例,IP數據包其實是分了外層IP頭部、gre層、內層IP頭部,以及再往上的TCP/UDP層,如果只獲取了外層IP頭部,則較為集中,難以進行分散。
經過同事幫忙牽線,我們從網卡廠商處獲得了重要的信息,不同的網卡對于多隊列哈希算法是不一樣的!
從網卡廠商處進一步確認得知,我們在使用的這款網卡,是不支持解析封裝后的數據包的,只會以外層IP作為哈希依據。廠商提供了一款新型號的網卡,是支持解析IPIP及GRE內層IP PORT的。我們經過實測這兩種網卡,發現確實如此。
看到這里,網卡多隊列不均衡問題原因已經定位清楚,由于現網使用了IPIP或GRE這類封裝協議,部分網卡不支持解析內層IP PORT進行哈希,從而導致多隊列不均衡,進一步導致cpu硬中斷不均衡,然后不均衡的軟中斷熱點便出現了。
3. 如何解決網卡多隊列不均衡
對于STGW來說,我們已經確定了不均衡的網卡型號,都是型號較老的網卡,我們正在逐步使用新的網卡型號,新網卡型號已驗證支持IPIP及GRE格式的數據包負載均衡。
為什么RPS沒有起作用
Receive Packet Steering (RPS),是內核的一種負載均衡機制,即便硬件層面收到的數據包不均衡的,RPS會對數據包再次進行哈希與分流,保證其進入網絡協議棧是均衡的。
經過確認,出問題機器上都開啟了RPS。所以問題還是沒有解釋清楚,即便舊型號的網卡RSS不均衡,但經過內核RPS后,數據包才會送給網絡協議棧,然后調用_inet_lookup_listener,此時依然出現熱點不均衡,說明RPS并未生效。
1. 了解硬件及內核收包流程
由于引入了RPS這個概念,在定位該問題前,我梳理了一份簡明收包流程,通過了解數據包是如何通過硬件、內核、再到內核網絡協議棧,可以更清晰的了解RPS所處的位置,以及我們遇到的問題。
如上圖所示,數據包在進入內核IP/TCP協議棧之前,經歷了這些步驟:
- 網口(NIC)收到packets
- 網口通過DMA(Direct memeory access)將數據寫入到內存(RAM)中。
- 網口通過RSS(網卡多隊列)將收到的數據包分發給某個rx隊列,并觸發該隊列所綁定核上的CPU中斷。
- 收到中斷的核,調用該核所在的內核軟中斷線程(softirqd)進行后續處理。
- softirqd負責將數據包從RAM中取到內核中。
- 如果開啟了RPS,RPS會選擇一個目標cpu核來處理該包,如果目標核非當前正在運行的核,則會觸發目標核的IPI(處理器之間中斷),并將數據包放在目標核的backlog隊列中。
- 軟中斷線程將數據包(數據包可能來源于第5步、或第6步),通過gro(generic receive offload,如果開啟的話)等處理后,送往IP協議棧,及之后的TCP/UDP等協議棧。
回顧我們前面定位的問題,__inet_lookup_listener熱點對應的是IP協議棧的問題,網卡多隊列不均衡是步驟3,RSS階段出現的問題。RPS則是在步驟6中。
2. 探秘RPS負載不均衡問題
通過cat /proc/net/softnet_stat,可以獲取到每個核接收的RPS次數。拿到這個數目后,我們發現,不同的核在接收RPS次數上相差達到上百倍,并且RPS次數最多的核,正好就是軟中斷消耗出現熱點的核。
至此我們發現,雖然網卡RSS存在不均衡,但RPS卻依然將過多的數據包給了部分cpu core,沒有做到負載均衡,這才是導致我們軟中斷熱點不均衡的直接原因。
通過在內核代碼庫中找到RPS相關代碼并進行分析,我們再次發現了一些可疑的點
- static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb,
- struct rps_dev_flow **rflowp)
- {
- // 省略部分代碼
- struct netdev_rx_queue *rxqueue;
- struct rps_map *map;
- struct rps_dev_flow_table *flow_table;
- struct rps_sock_flow_table *sock_flow_table;
- int cpu = -1;
- u16 tcpu;
- skb_reset_network_header(skb);
- if (rps_ignore_l4_rxhash) {
- // 計算哈希值
- __skb_get_rxhash(skb);
- if (!skb->rxhash)
- goto done;
- }
- else if(!skb_get_rxhash(skb))
- goto done;
- // 通過哈希值計算出目標CPU
- if (map) {
- tcpu = map->cpus[((u64) skb->rxhash * map->len) >> 32];
- if (cpu_online(tcpu)) {
- cpu = tcpu;
- goto done;
- }
- }
- done:
- return cpu;
- }
- /*
- * __skb_get_rxhash: calculate a flow hash based on src/dst addresses
- * and src/dst port numbers. Sets rxhash in skb to non-zero hash value
- * on success, zero indicates no valid hash. Also, sets l4_rxhash in skb
- * if hash is a canonical 4-tuple hash over transport ports.
- */
- void __skb_get_rxhash(struct sk_buff *skb)
- {
- struct flow_keys keys;
- u32 hash;
- if (!skb_flow_dissect(skb, &keys))
- return;
- if (keys.ports)
- skb->l4_rxhash = 1;
- // 使用TCP/UDP四元組進行計算
- /* get a consistent hash (same value on both flow directions) */
- if (((__force u32)keys.dst < (__force u32)keys.src) ||
- (((__force u32)keys.dst == (__force u32)keys.src) &&
- ((__force u16)keys.port16[1] < (__force u16)keys.port16[0]))) {
- swap(keys.dst, keys.src);
- swap(keys.port16[0], keys.port16[1]);
- }
- // 使用jenkins哈希算法
- hash = jhash_3words((__force u32)keys.dst,
- (__force u32)keys.src,
- (__force u32)keys.ports, hashrnd);
- if (!hash)
- hash = 1;
- skb->rxhash = hash;
- }
猜想一:rps_ignore_l4_rxhash未打開,導致不均衡?
通過代碼發現 rps_ignore_l4_rxhash 會影響當前是否計算哈希值,當前機器未設置ignore_l4_rxhash,則內核會直接使用網卡RSS計算出的哈希值,根據上面定位的網卡RSS不均衡的結論,RSS哈希值可能是不準的,這里會導致問題嗎?
我們將ignore_l4_rxhash開關進行打開
- sysctl -w kernel.rps_ignore_l4_rxhash=1
發現并沒有對不均衡問題產生任何改善,排除這個假設。
猜想二:RPS所使用的哈希算法有缺陷,導致不均衡?
對于負載不均衡類的問題,理所應當會懷疑當前使用的均衡算法存在缺陷,RPS這里使用的是jenkins hash(jhash_3words)算法,是一個比較著名且被廣泛使用的算法,經過了很多環境的驗證,出現缺陷的可能性較小。但我們還是想辦法進行了一些驗證,由于是內核代碼,并且沒有提供替代性的算法,改動內核的代價相對較高。
因此這里我們采取的對比的手段快速確定,在同樣的內核版本,在現網找到了負載均衡的機器,檢查兩邊的一些內核開關和RPS配置都是一致的,說明同樣的RPS哈希算法,只是部分機器不均衡,因此這里算法側先不做進一步挖掘。
猜想三:和RSS問題一樣,RPS也不支持對封裝后的數據進行四元組哈希?
skb_flow_dissect是負責解析出TCP/UDP四元組,經過初步分析,內核是支持IPIP、GRE等通用的封裝協議,并從這些協議數據包中,取出需要的四元組信息進行哈希。
在各種假設與折騰都沒有找到新的突破之時,我們使用systemtap這個內核調試神器,hook了關鍵的幾個函數和信息,經過論證和測試后,在現網進行了短暫的debug,收集到了所需要的關鍵信息。
- #! /usr/bin/env stap
- /*
- Analyse problem that softirq not balance with RPS.
- Author: dalektan@tencent.com
- Usage:
- stap -p4 analyse_rps.stp -m stap_analyse_rps
- staprun -x cpuid stap_analyse_rps.ko
- */
- // To record how cpu changed with rps execute
- private global target_cpu = 0
- private global begin_cpu
- private global end_cpu
- probe begin {
- target_cpu = target()
- begin_cpu = target_cpu - 2
- end_cpu = target_cpu + 2
- // 指定需要分析的cpu范圍,避免對性能產生影響
- printf("Prepare to analyse cpu is :%d-%d\n", begin_cpu, end_cpu)
- }
- // To record tsv ip addr, daddr and protocol(ipip, gre or tcp)
- probe kernel.function("ip_rcv").call {
- if (cpu() >= begin_cpu && cpu() <= end_cpu) {
- ip_protocol = ipmib_get_proto($skb)
- // if not tcp, ipip, gre, then return
- if (ip_protocol == 4 || ip_protocol == 6 || ip_protocol == 47) {
- saddr = ip_ntop(htonl(ipmib_remote_addr($skb, 0)))
- daddr = ip_ntop(htonl(ipmib_local_addr($skb, 0)))
- printf("IP %s -> %s proto:%d rx_queue:%d cpu:%d\n",
- saddr, daddr, ip_protocol, $skb->queue_mapping-1, cpu())
- }
- }
- }
- // To record tcp states
- probe tcp.receive.call {
- if (cpu() >= begin_cpu && cpu() <= end_cpu) {
- printf("TCP %s:%d -> %s:%d syn:%d rst:%d fin:%d cpu:%d\n",
- saddr, sport , daddr, dport, syn, rst, fin, cpu())
- }
- }
通過使用上述systemtap腳本進行分析后,我們得到了一個關鍵信息,大量GRE協議(圖中proto:47)的數據包,無論其四元組是什么,都被集中調度到了單個核心上,并且這個核心正好是軟中斷消耗熱點核。并且其他協議數據包未出現這個問題。
走到這里,問題漸為開朗,GRE數據包未按預期均衡到各個核心,但根據之前的分析,RPS是支持GRE協議獲取四元組的,為什么在這里,不同的四元組,依然被哈希算成了同一個目標核呢?
3. 探究GRE數據包不均衡之謎
帶著這個問題進一步挖掘,通過抓包以及代碼比對分析,很快有了突破,定位到了原因是:當前內核僅識別GRE_VERSION=0的GRE協議包并獲取其四元組信息,而我們的數據包,是GRE_VERSION=1的。
- // skb_flow_dissect 獲取四元組信息
- switch (ip_proto) {
- case IPPROTO_GRE: {
- struct gre_hdr {
- __be16 flags;
- __be16 proto;
- } *hdr, _hdr;
- hdr = skb_header_pointer(skb, nhoff, sizeof(_hdr), &_hdr);
- if (!hdr)
- return false;
- /*
- * Only look inside GRE if version zero and no
- * routing
- */
- // 只解析GRE_VERSION = 0的GRE協議數據包
- if (!(hdr->flags & (GRE_VERSION|GRE_ROUTING))) {
- proto = hdr->proto;
- nhoff += 4;
- if (hdr->flags & GRE_CSUM)
- nhoff += 4;
- if (hdr->flags & GRE_KEY)
- nhoff += 4;
- if (hdr->flags & GRE_SEQ)
- nhoff += 4;
- if (proto == htons(ETH_P_TEB)) {
- const struct ethhdr *eth;
- struct ethhdr _eth;
- eth = skb_header_pointer(skb, nhoff,
- sizeof(_eth), &_eth);
- if (!eth)
- return false;
- proto = eth->h_proto;
- nhoff += sizeof(*eth);
- }
- goto again;
- }
- break;
- }
首先,我們先確認一下,GRE_VERSION=1是符合規范的嗎,答案是符合的,如果去了解一下PPTP協議的話,可以知道RFC規定了PPTP協議就是使用的GRE協議封裝并且GRE_VERSION=1。
那么為什么內核這里不支持這種標記呢?
通過在Linux社區進行檢索,我們發現Linux 4.10版本開始支持PPTP協議下的GRE包識別與四元組獲取,也就是GRE_VERSION=1的情況。由于沒有加入對PPTP協議的支持,因此出現不識別GRE_VERSION=1(PPTP協議)的情況,RPS不會去獲取這種數據包的四元組信息,哈希后也是不均衡的,最終導致單核出現軟中斷熱點。
4. 如何解決RPS不均衡問題
至此,所有的問題都已經撥開云霧,最后對于RPS不均衡問題,這里提供三種解決方案:
- 對于RSS網卡多隊列已經是均衡的機器,可以將修改kernel.rps_ignore_l4_rxhash = 0,讓RPS直接使用網卡硬件哈希值,由于硬件哈希值足夠分散,因此RPS效果也是均衡的。
- 對于RSS網卡多隊列均衡的機器,通過ethtool -S/-L查看或修改網卡多隊列數目,如果隊列數不少于cpu核數,再將多隊列通過/proc/irq/設備id/smp_affinity分散綁定到不同的cpu核。這樣可以充分利用網卡RSS的均衡效果,而無需打開內核RPS。
- 在當前內核進行修改或熱補丁,在RPS函數中新增對GRE_VERSION=1的GRE數據包的識別與四元組獲取。
- 升級內核到Linux 4.10之后,即可支持PPTP協議包的RPS負載均衡。
總結
最后,總結一下整個問題和定位過程,我們從一次流量下降,業務有損的問題出發,從最開始找不到思路,到找到軟中斷熱點這個關鍵點,再到區分IP協議棧、網卡多隊列、內核收包這三個層面進行問題入手,沒有滿足于已經解決的部分問題,不斷深挖了下去,終于各個方向擊破,撥開了問題的層層面紗,并給出了解決方案。
借著對問題的定位和解決,收獲良多,學習了內核收包流程,熟悉了內核問題定位工具和手段。感謝STGW組里同事,文中諸多成果都是團隊的共同努力。
【本文為51CTO專欄作者“騰訊技術工程”原創稿件,轉載請聯系原作者(微信號:Tencent_TEG)】