自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從STGW流量下降探秘內(nèi)核收包機(jī)制

企業(yè)動(dòng)態(tài)
在STGW現(xiàn)網(wǎng)運(yùn)營中,出現(xiàn)了一起流量突然下降的Case,此時(shí)我們的健康撥測機(jī)制探測到失敗,并且用戶側(cè)重試次數(shù)增多、請(qǐng)求延遲增大。

 問題現(xiàn)象

在STGW現(xiàn)網(wǎng)運(yùn)營中,出現(xiàn)了一起流量突然下降的Case,此時(shí)我們的健康撥測機(jī)制探測到失敗,并且用戶側(cè)重試次數(shù)增多、請(qǐng)求延遲增大。但通過已有的各類監(jiān)控進(jìn)行定位,只發(fā)現(xiàn)整體CPU、內(nèi)存、進(jìn)程狀態(tài)、QPS(每秒請(qǐng)求數(shù))等關(guān)鍵指標(biāo)雖然出現(xiàn)波動(dòng),但均未超過告警水位。

[[313051]]

如圖,流量出現(xiàn)了跌幅,并且出現(xiàn)健康檢查撥測失敗。

 

但是,整體CPU在流量出現(xiàn)缺口的期間,并未超過閾值,反而有一些下降,隨后因?yàn)榛謴?fù)正常流量沖高才出現(xiàn)一個(gè)小毛刺。

 

此外,內(nèi)存和應(yīng)用層監(jiān)控,都沒有發(fā)現(xiàn)明顯異常。

前期探索

顯然,僅憑這些常規(guī)監(jiān)控,無法定位問題根本原因,盡量拿到更多的問題信息,成為了當(dāng)務(wù)之急。幸運(yùn)的是,從STGW自研的秒級(jí)監(jiān)控系統(tǒng)中,我們查到了一些關(guān)鍵的信息。

在STGW自研的監(jiān)控系統(tǒng)里,我們增加了核心資源細(xì)粒度監(jiān)控,針對(duì)CPU、內(nèi)存、內(nèi)核網(wǎng)絡(luò)協(xié)議棧這些核心指標(biāo)支持秒級(jí)監(jiān)控、監(jiān)控指標(biāo)更細(xì)化,如下圖就是出問題時(shí)間段,cpu各個(gè)核心的秒級(jí)消耗情況。

 

通過STGW CPU細(xì)粒度監(jiān)控展示的信息,可以看到在出現(xiàn)問題的時(shí)間段內(nèi),部分CPU核被跑滿,并且是由于軟中斷消耗造成,回溯整個(gè)問題時(shí)間段,我們還發(fā)現(xiàn),在一段長時(shí)間內(nèi),這種軟中斷熱點(diǎn)偏高都會(huì)在幾個(gè)固定的核上出現(xiàn),不會(huì)轉(zhuǎn)移給其他核。

此外,STGW的監(jiān)控模塊支持在出現(xiàn)系統(tǒng)核心資源異常時(shí),抓取當(dāng)時(shí)的函數(shù)調(diào)用棧信息,有了函數(shù)調(diào)用信息,我們能更準(zhǔn)確的知道是什么造成了系統(tǒng)核心資源異常,而不是繼續(xù)猜想。如圖展示了STGW監(jiān)控抓到的函數(shù)調(diào)用及cpu占比信息:

 

通過函數(shù)棧監(jiān)控信息,我們發(fā)現(xiàn)了inet_lookup_listener函數(shù)是當(dāng)時(shí)CPU軟中斷熱點(diǎn)的主要消耗者。出現(xiàn)問題時(shí),其他函數(shù)調(diào)用在沒有發(fā)生多少變化情況下,inet_lookup_listener由原本很微小的cpu消耗占比,一下子沖到了TOP1。

通過這里,我們可以初步確定,inet_lookup_listener消耗過高跟軟中斷熱點(diǎn)強(qiáng)相關(guān),當(dāng)熱點(diǎn)將cpu單核跑滿后就可能引發(fā)出流量有損的問題。由于軟中斷熱點(diǎn)持續(xù)在產(chǎn)生,線上穩(wěn)定性隱患很大?;谶@個(gè)緊迫的穩(wěn)定性問題,我們從為什么產(chǎn)生熱點(diǎn)、為什么熱點(diǎn)只在部分cpu core上出現(xiàn)兩個(gè)方向,進(jìn)行了問題分析、定位和解決。

為什么產(chǎn)生了熱點(diǎn)

1. 探秘 inet_lookup_listener

由于perf已經(jīng)給我們提供了熱點(diǎn)所在,首先從熱點(diǎn)函數(shù)入手進(jìn)行分析,結(jié)合內(nèi)核代碼得知,__inet_lookup系列函數(shù)是用于將收到的數(shù)據(jù)包定位到一個(gè)具體的socket上,但只有握手包會(huì)進(jìn)入到找__inet_lookup_listener的邏輯,大部分?jǐn)?shù)據(jù)包是通過__inet_lookup_established尋找socket。

具體分析lookup_listener的代碼我們發(fā)現(xiàn),由于listen socket不具備四元組特征,因此內(nèi)核只能用監(jiān)聽端口計(jì)算一個(gè)哈希值,并使用了 listening_hash 哈希桶存起來,握手包發(fā)過來的時(shí)候,就從該哈希桶中尋找對(duì)應(yīng)的listen socket。

  1. struct sock *__inet_lookup_listener(struct net *net, 
  2.             struct inet_hashinfo *hashinfo, 
  3.             const __be32 saddr, __be16 sport, 
  4.             const __be32 daddr, const unsigned short hnum, 
  5.             const int dif) 
  6. // 省略了部分代碼 
  7. // 獲取listen fd 哈希桶 
  8.   struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash]; 
  9.         result = NULL
  10.   hiscore = 0; 
  11. // 遍歷桶中的節(jié)點(diǎn) 
  12.         sk_nulls_for_each_rcu(sk, node, &ilb->head) { 
  13.     score = compute_score(sk, net, hnum, daddr, dif); 
  14.     if (score > hiscore) { 
  15.       result = sk; 
  16.       hiscore = score; 
  17.       reuseport = sk->sk_reuseport; 
  18.       if (reuseport) { 
  19.         phash = inet_ehashfn(net, daddr, hnum, 
  20.                  saddr, sport); 
  21.         matches = 1; 
  22.       } 
  23.     } else if (score == hiscore && reuseport) { 
  24.       matches++; 
  25.       if (((u64)phash * matches) >> 32 == 0) 
  26.         result = sk; 
  27.       phash = next_pseudo_random32(phash); 
  28.     } 
  29.   } 
  30. }  

相對(duì)來說并不復(fù)雜的lookup_listener函數(shù)為什么會(huì)造成這么大的cpu開銷?經(jīng)過進(jìn)一步定位后,發(fā)現(xiàn)問題所在:listen哈希桶開的太小了,只有32個(gè)。

  1. /* This is for listening sockets, thus all sockets which possess wildcards. */ 
  2. #define INET_LHTABLE_SIZE  32  /* Yes, really, this is all you need. */ 

為什么內(nèi)核這里會(huì)覺得listen哈希桶大小32就滿足需要了呢?

在IETF(互聯(lián)網(wǎng)工程任務(wù)組)關(guān)于端口的規(guī)劃中,0-1023是System port,系統(tǒng)保留使用,1024-49151為Registered port,為IANA(互聯(lián)網(wǎng)數(shù)字分配機(jī)構(gòu))可分配給一些固定應(yīng)用,49152-65535是Dynamic port,是可以真正自由使用的。當(dāng)然了,這只是IETF的一個(gè)規(guī)劃,在Linux中,除了System port,另兩個(gè)端口段并未真的做了明顯區(qū)分,除非端口已經(jīng)被占用,用戶可以自由使用,這里提一個(gè)Linux中跟端口劃分有關(guān)聯(lián)的內(nèi)核參數(shù):ip_local_port_range,它表示系統(tǒng)在建立TCP/UDP連接時(shí),系統(tǒng)給它們分配的端口范圍,默認(rèn)的ip_local_port_range值是32768-60999,進(jìn)行這個(gè)設(shè)置后好處是,61000~65535端口是可以更安全的用來做為服務(wù)器監(jiān)聽,而不用擔(dān)心一些TCP連接將其占用。

因此,在正常的情況下,服務(wù)器的listen port數(shù)量,大概就是幾w個(gè)這樣的量級(jí)。這種量級(jí)下,一個(gè)port對(duì)應(yīng)一個(gè)socket,哈希桶大小為32是可以接受的。然而在內(nèi)核支持了reuseport并且被廣泛使用后,情況就不一樣了,在多進(jìn)程架構(gòu)里,listen port對(duì)應(yīng)的socket數(shù)量,是會(huì)被幾十倍的放大的。以應(yīng)用層監(jiān)聽了5000個(gè)端口,reuseport 使用了50個(gè)cpu核心為例,5000*50/32約等于7812,意味著每次握手包到來時(shí),光是查找listen socket,就需要遍歷7800多次。隨著機(jī)器硬件性能越來越強(qiáng),應(yīng)用層使用的cpu數(shù)量增多,這個(gè)問題還會(huì)繼續(xù)加劇。

正因?yàn)樯鲜鲈?,并且我們現(xiàn)網(wǎng)機(jī)器開啟了reuseport,在端口數(shù)量較多的機(jī)器里,inet_lookup_listener的哈希桶大小太小,遍歷過程消耗了cpu,導(dǎo)致出現(xiàn)了函數(shù)熱點(diǎn)。

2. 如何解決__inet_lookup_listener問題

Linux社區(qū)難道沒有注意到開啟reuseport后,原來的哈希桶大小不夠用這個(gè)問題嗎?

其實(shí)社區(qū)是注意到了這個(gè)問題的,并且有修復(fù)這個(gè)問題。

從Linux 4.17開始,Linux社區(qū)就修復(fù)了由于reuseport帶來的socket數(shù)量過多,導(dǎo)致inet_lookup_listener查找緩慢的問題,修復(fù)方案分兩步:

1. 引入了兩次查找,首先還是根據(jù)目的端口進(jìn)行哈希,接著會(huì)使用握手包中拿到的四元組信息,按照四元組進(jìn)行第一次查找,如果四元組獲取不到結(jié)果,則使用之前那種對(duì)于任意IP地址查找。

  1. struct sock *__inet_lookup_listener(struct net *net, 
  2.             struct inet_hashinfo *hashinfo, 
  3.             struct sk_buff *skb, int doff, 
  4.             const __be32 saddr, __be16 sport, 
  5.             const __be32 daddr, const unsigned short hnum, 
  6.             const int dif, const int sdif) 
  7.   struct inet_listen_hashbucket *ilb2; 
  8.   struct sock *result = NULL
  9.   unsigned int hash2; 
  10.  
  11. // 根據(jù)目的端口進(jìn)行第一次哈希 
  12.   hash2 = ipv4_portaddr_hash(net, daddr, hnum); 
  13.   ilb2 = inet_lhash2_bucket(hashinfo, hash2); 
  14. // 根據(jù)四元組信息再做一次查找 
  15.   result = inet_lhash2_lookup(net, ilb2, skb, doff, 
  16.             saddr, sport, daddr, hnum, 
  17.             dif, sdif); 
  18.   if (result) 
  19.     goto done; 
  20.  
  21.   /* Lookup lhash2 with INADDR_ANY */ 
  22. // 四元組沒查到,嘗試在0.0.0.0監(jiān)聽范圍查找 
  23.   hash2 = ipv4_portaddr_hash(net, htonl(INADDR_ANY), hnum); 
  24.   ilb2 = inet_lhash2_bucket(hashinfo, hash2); 
  25.  
  26.   result = inet_lhash2_lookup(net, ilb2, skb, doff, 
  27.             saddr, sport, htonl(INADDR_ANY), hnum, 
  28.             dif, sdif); 
  29. done: 
  30.   if (IS_ERR(result)) 
  31.     return NULL
  32.   return result; 

2. 合并處理reuseport放大出來的socket,在發(fā)現(xiàn)指定的端口開啟了reuseport后,不再是遍歷式的去獲取到合適的socket,而是將其看成一個(gè)整體,二次哈希后,調(diào)用 reuseport_select_sock,取到合適的socket。

  1. static struct sock *inet_lhash2_lookup(struct net *net, 
  2.         struct inet_listen_hashbucket *ilb2, 
  3.         struct sk_buff *skb, int doff, 
  4.         const __be32 saddr, __be16 sport, 
  5.         const __be32 daddr, const unsigned short hnum, 
  6.         const int dif, const int sdif) 
  7.   bool exact_dif = inet_exact_dif_match(net, skb); 
  8.   struct inet_connection_sock *icsk; 
  9.   struct sock *sk, *result = NULL
  10.   int score, hiscore = 0; 
  11.   u32 phash = 0; 
  12.  
  13.   inet_lhash2_for_each_icsk_rcu(icsk, &ilb2->head) { 
  14.     sk = (struct sock *)icsk; 
  15.     score = compute_score(sk, net, hnum, daddr, 
  16.               dif, sdif, exact_dif); 
  17.     if (score > hiscore) { 
  18.       if (sk->sk_reuseport) { 
  19.                 // 如果是reuseport,進(jìn)行二次哈希查找 
  20.         phash = inet_ehashfn(net, daddr, hnum, 
  21.                  saddr, sport); 
  22.         result = reuseport_select_sock(sk, phash, 
  23.                      skb, doff); 
  24.         if (result) 
  25.           return result; 
  26.       } 
  27.       result = sk; 
  28.       hiscore = score; 
  29.     } 
  30.   } 
  31.  
  32.   return result; 

總結(jié)來說,社區(qū)通過引入兩次查找+合并reuseport sockets的處理,解決了reuseport帶來的sockets數(shù)量放大效果。這里結(jié)合我們的探索,另外提供兩個(gè)可行的低成本解決方案:

1. 修改內(nèi)核哈希桶大小,根據(jù)reuseport增加socket的倍數(shù),相應(yīng)提高INET_LHTABLE_SIZE,或者直接改成例如2048

  1. #define INET_LHTABLE_SIZE  2048  

2. 關(guān)閉reuseport可以減少socket數(shù)目到32個(gè)哈希桶大小能承受的范圍,從而降低該函數(shù)消耗。

加上社區(qū)方案,這里的三個(gè)方法在本質(zhì)上都是減少listen table哈希桶的遍歷復(fù)雜度。社區(qū)的方案一套比較系統(tǒng)的方法,今后隨著內(nèi)核版本升級(jí),肯定會(huì)將這個(gè)問題解決掉。但短期升級(jí)內(nèi)核的成本較高,所以后面兩個(gè)方案就可以用來短期解決問題。此外,關(guān)閉reuseport雖然不需要更改內(nèi)核,但需要考慮應(yīng)用層server對(duì)于reuseport的依賴情況。

為什么熱點(diǎn)只在部分核心出現(xiàn)

解決完哈希桶問題后,我們并沒有定位到全部的問題,前面提到,軟中斷熱點(diǎn)僅在部分cpu核上出現(xiàn),如果僅僅是__inet_lookup_listener問題,按理所有cpu核的軟中斷消耗都會(huì)偏高。如果這里問題沒有解釋清楚,一旦出現(xiàn)熱點(diǎn)函數(shù),一些單核就會(huì)被跑滿,意味著整機(jī)容量強(qiáng)依賴部分單核的性能瓶頸,超出了單核能力就會(huì)有損,這是完全不能接受的。

1. 從CPU中斷數(shù)入手

根據(jù)問題現(xiàn)象,我們做了一些假設(shè),在這里最直觀的假設(shè)就是,我們的數(shù)據(jù)包在各個(gè)核上并不是負(fù)載均衡的。

首先,通過cat /proc/interrupts找到網(wǎng)卡在各個(gè)cpu核的中斷數(shù),發(fā)現(xiàn)網(wǎng)卡在各個(gè)核的硬中斷就已經(jīng)不均衡了。那么會(huì)是硬中斷親和性的問題嗎?接著檢查了網(wǎng)卡各個(gè)隊(duì)列的smp_affiinity,發(fā)現(xiàn)每個(gè)隊(duì)列與cpu核都是一一對(duì)應(yīng)并且互相錯(cuò)開,硬中斷親和性設(shè)置沒有問題。

緊接著,我們排查了網(wǎng)卡,我們的網(wǎng)卡默認(rèn)都打開了RSS(網(wǎng)卡多隊(duì)列),每個(gè)隊(duì)列綁定到一個(gè)核心上,既然硬中斷親和性沒有問題,那么會(huì)是網(wǎng)卡多隊(duì)列本身就不均衡嗎?通過ethtool -S eth0/eth1再過濾出每個(gè)rx_queue的收包數(shù),我們得到如下圖:

 

原來網(wǎng)卡多隊(duì)列收包就已經(jīng)嚴(yán)重不均衡了,以入包量升序排序,發(fā)現(xiàn)不同的rx_queue 收包數(shù)量相差達(dá)到了上萬倍!

2. 探究網(wǎng)卡多隊(duì)列(RSS)

這里我們著重檢查了幾個(gè)網(wǎng)卡多隊(duì)列的參數(shù)

  1. // 檢查網(wǎng)卡的隊(duì)列數(shù) 
  2. ethtool -l eth0 
  3. Current hardware settings: 
  4. RX:             0 
  5. TX:             0 
  6. Other:          1 
  7. Combined:       48 
  8.  
  9. // 檢查硬件哈希開關(guān) 
  10. ethtool -k eth0 
  11. receive-hashing: on 
  12.  
  13. // 檢查硬件哈希的參數(shù),這里顯示以TCP是以四元組信息進(jìn)行哈希 
  14. ethtool -n eth0 rx-flow-hash tcp4 
  15. TCP over IPV4 flows use these fields for computing Hash flow key
  16. IP SA 
  17. IP DA 
  18. L4 bytes 0 & 1 [TCP/UDP src port] 
  19. L4 bytes 2 & 3 [TCP/UDP dst port] 

這些參數(shù)都是符合預(yù)期的,數(shù)據(jù)包會(huì)根據(jù)TCP包的四元組哈希到不同的隊(duì)列上。我們繼續(xù)使用假設(shè)論證法,會(huì)是數(shù)據(jù)包本身就是比如長連接,導(dǎo)致不均衡嗎?通過檢查我們服務(wù)端的日志,發(fā)現(xiàn)請(qǐng)求的ip和端口都是比較分散的,傳輸?shù)臄?shù)據(jù)也都是較小文件,并沒有集中化。

經(jīng)過了一番折騰,我們有了新的假設(shè),由于我們現(xiàn)網(wǎng)大部分流量是IPIP隧道及GRE封裝的數(shù)據(jù)包,在普通數(shù)據(jù)包的IP header上多了一層header,外層IP與我們在server看到的并不一樣,外層IP是非常集中的。這里是否會(huì)讓網(wǎng)卡多隊(duì)列的均衡策略失效呢?

來源網(wǎng)圖,以GRE包為例,IP數(shù)據(jù)包其實(shí)是分了外層IP頭部、gre層、內(nèi)層IP頭部,以及再往上的TCP/UDP層,如果只獲取了外層IP頭部,則較為集中,難以進(jìn)行分散。

 

經(jīng)過同事幫忙牽線,我們從網(wǎng)卡廠商處獲得了重要的信息,不同的網(wǎng)卡對(duì)于多隊(duì)列哈希算法是不一樣的!

從網(wǎng)卡廠商處進(jìn)一步確認(rèn)得知,我們在使用的這款網(wǎng)卡,是不支持解析封裝后的數(shù)據(jù)包的,只會(huì)以外層IP作為哈希依據(jù)。廠商提供了一款新型號(hào)的網(wǎng)卡,是支持解析IPIP及GRE內(nèi)層IP PORT的。我們經(jīng)過實(shí)測這兩種網(wǎng)卡,發(fā)現(xiàn)確實(shí)如此。

看到這里,網(wǎng)卡多隊(duì)列不均衡問題原因已經(jīng)定位清楚,由于現(xiàn)網(wǎng)使用了IPIP或GRE這類封裝協(xié)議,部分網(wǎng)卡不支持解析內(nèi)層IP PORT進(jìn)行哈希,從而導(dǎo)致多隊(duì)列不均衡,進(jìn)一步導(dǎo)致cpu硬中斷不均衡,然后不均衡的軟中斷熱點(diǎn)便出現(xiàn)了。

3. 如何解決網(wǎng)卡多隊(duì)列不均衡

對(duì)于STGW來說,我們已經(jīng)確定了不均衡的網(wǎng)卡型號(hào),都是型號(hào)較老的網(wǎng)卡,我們正在逐步使用新的網(wǎng)卡型號(hào),新網(wǎng)卡型號(hào)已驗(yàn)證支持IPIP及GRE格式的數(shù)據(jù)包負(fù)載均衡。

為什么RPS沒有起作用

Receive Packet Steering (RPS),是內(nèi)核的一種負(fù)載均衡機(jī)制,即便硬件層面收到的數(shù)據(jù)包不均衡的,RPS會(huì)對(duì)數(shù)據(jù)包再次進(jìn)行哈希與分流,保證其進(jìn)入網(wǎng)絡(luò)協(xié)議棧是均衡的。

經(jīng)過確認(rèn),出問題機(jī)器上都開啟了RPS。所以問題還是沒有解釋清楚,即便舊型號(hào)的網(wǎng)卡RSS不均衡,但經(jīng)過內(nèi)核RPS后,數(shù)據(jù)包才會(huì)送給網(wǎng)絡(luò)協(xié)議棧,然后調(diào)用_inet_lookup_listener,此時(shí)依然出現(xiàn)熱點(diǎn)不均衡,說明RPS并未生效。

1. 了解硬件及內(nèi)核收包流程

由于引入了RPS這個(gè)概念,在定位該問題前,我梳理了一份簡明收包流程,通過了解數(shù)據(jù)包是如何通過硬件、內(nèi)核、再到內(nèi)核網(wǎng)絡(luò)協(xié)議棧,可以更清晰的了解RPS所處的位置,以及我們遇到的問題。

 

如上圖所示,數(shù)據(jù)包在進(jìn)入內(nèi)核IP/TCP協(xié)議棧之前,經(jīng)歷了這些步驟:

  1. 網(wǎng)口(NIC)收到packets
  2. 網(wǎng)口通過DMA(Direct memeory access)將數(shù)據(jù)寫入到內(nèi)存(RAM)中。
  3. 網(wǎng)口通過RSS(網(wǎng)卡多隊(duì)列)將收到的數(shù)據(jù)包分發(fā)給某個(gè)rx隊(duì)列,并觸發(fā)該隊(duì)列所綁定核上的CPU中斷。
  4. 收到中斷的核,調(diào)用該核所在的內(nèi)核軟中斷線程(softirqd)進(jìn)行后續(xù)處理。
  5. softirqd負(fù)責(zé)將數(shù)據(jù)包從RAM中取到內(nèi)核中。
  6. 如果開啟了RPS,RPS會(huì)選擇一個(gè)目標(biāo)cpu核來處理該包,如果目標(biāo)核非當(dāng)前正在運(yùn)行的核,則會(huì)觸發(fā)目標(biāo)核的IPI(處理器之間中斷),并將數(shù)據(jù)包放在目標(biāo)核的backlog隊(duì)列中。
  7. 軟中斷線程將數(shù)據(jù)包(數(shù)據(jù)包可能來源于第5步、或第6步),通過gro(generic receive offload,如果開啟的話)等處理后,送往IP協(xié)議棧,及之后的TCP/UDP等協(xié)議棧。

回顧我們前面定位的問題,__inet_lookup_listener熱點(diǎn)對(duì)應(yīng)的是IP協(xié)議棧的問題,網(wǎng)卡多隊(duì)列不均衡是步驟3,RSS階段出現(xiàn)的問題。RPS則是在步驟6中。

2. 探秘RPS負(fù)載不均衡問題

通過cat /proc/net/softnet_stat,可以獲取到每個(gè)核接收的RPS次數(shù)。拿到這個(gè)數(shù)目后,我們發(fā)現(xiàn),不同的核在接收RPS次數(shù)上相差達(dá)到上百倍,并且RPS次數(shù)最多的核,正好就是軟中斷消耗出現(xiàn)熱點(diǎn)的核。

至此我們發(fā)現(xiàn),雖然網(wǎng)卡RSS存在不均衡,但RPS卻依然將過多的數(shù)據(jù)包給了部分cpu core,沒有做到負(fù)載均衡,這才是導(dǎo)致我們軟中斷熱點(diǎn)不均衡的直接原因。

通過在內(nèi)核代碼庫中找到RPS相關(guān)代碼并進(jìn)行分析,我們再次發(fā)現(xiàn)了一些可疑的點(diǎn)

  1. static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb, 
  2.            struct rps_dev_flow **rflowp) 
  3. // 省略部分代碼 
  4.   struct netdev_rx_queue *rxqueue; 
  5.   struct rps_map *map; 
  6.   struct rps_dev_flow_table *flow_table; 
  7.   struct rps_sock_flow_table *sock_flow_table; 
  8.   int cpu = -1; 
  9.   u16 tcpu; 
  10.  
  11.   skb_reset_network_header(skb); 
  12.   if (rps_ignore_l4_rxhash) { 
  13. // 計(jì)算哈希值 
  14.     __skb_get_rxhash(skb); 
  15.     if (!skb->rxhash) 
  16.       goto done; 
  17.   } 
  18.   else if(!skb_get_rxhash(skb)) 
  19.     goto done; 
  20.  
  21. // 通過哈希值計(jì)算出目標(biāo)CPU 
  22.         if (map) { 
  23.     tcpu = map->cpus[((u64) skb->rxhash * map->len) >> 32]; 
  24.  
  25.     if (cpu_online(tcpu)) { 
  26.       cpu = tcpu; 
  27.       goto done; 
  28.     } 
  29.   } 
  30.  
  31. done: 
  32.   return cpu; 
  33.  
  1. /* 
  2.  * __skb_get_rxhash: calculate a flow hash based on src/dst addresses 
  3.  * and src/dst port numbers.  Sets rxhash in skb to non-zero hash value 
  4.  * on success, zero indicates no valid hash.  Also, sets l4_rxhash in skb 
  5.  * if hash is a canonical 4-tuple hash over transport ports. 
  6.  */ 
  7. void __skb_get_rxhash(struct sk_buff *skb) 
  8.   struct flow_keys keys; 
  9.   u32 hash; 
  10.  
  11.   if (!skb_flow_dissect(skb, &keys)) 
  12.     return
  13.  
  14.   if (keys.ports) 
  15.     skb->l4_rxhash = 1; 
  16. // 使用TCP/UDP四元組進(jìn)行計(jì)算 
  17.   /* get a consistent hash (same value on both flow directions) */ 
  18.   if (((__force u32)keys.dst < (__force u32)keys.src) || 
  19.       (((__force u32)keys.dst == (__force u32)keys.src) && 
  20.        ((__force u16)keys.port16[1] < (__force u16)keys.port16[0]))) { 
  21.     swap(keys.dst, keys.src); 
  22.     swap(keys.port16[0], keys.port16[1]); 
  23.   } 
  24. // 使用jenkins哈希算法 
  25.   hash = jhash_3words((__force u32)keys.dst, 
  26.           (__force u32)keys.src, 
  27.           (__force u32)keys.ports, hashrnd); 
  28.   if (!hash) 
  29.     hash = 1; 
  30.  
  31.   skb->rxhash = hash; 

猜想一:rps_ignore_l4_rxhash未打開,導(dǎo)致不均衡?

通過代碼發(fā)現(xiàn) rps_ignore_l4_rxhash 會(huì)影響當(dāng)前是否計(jì)算哈希值,當(dāng)前機(jī)器未設(shè)置ignore_l4_rxhash,則內(nèi)核會(huì)直接使用網(wǎng)卡RSS計(jì)算出的哈希值,根據(jù)上面定位的網(wǎng)卡RSS不均衡的結(jié)論,RSS哈希值可能是不準(zhǔn)的,這里會(huì)導(dǎo)致問題嗎?

我們將ignore_l4_rxhash開關(guān)進(jìn)行打開

  1. sysctl -w kernel.rps_ignore_l4_rxhash=1 

發(fā)現(xiàn)并沒有對(duì)不均衡問題產(chǎn)生任何改善,排除這個(gè)假設(shè)。

猜想二:RPS所使用的哈希算法有缺陷,導(dǎo)致不均衡?

對(duì)于負(fù)載不均衡類的問題,理所應(yīng)當(dāng)會(huì)懷疑當(dāng)前使用的均衡算法存在缺陷,RPS這里使用的是jenkins hash(jhash_3words)算法,是一個(gè)比較著名且被廣泛使用的算法,經(jīng)過了很多環(huán)境的驗(yàn)證,出現(xiàn)缺陷的可能性較小。但我們還是想辦法進(jìn)行了一些驗(yàn)證,由于是內(nèi)核代碼,并且沒有提供替代性的算法,改動(dòng)內(nèi)核的代價(jià)相對(duì)較高。

因此這里我們采取的對(duì)比的手段快速確定,在同樣的內(nèi)核版本,在現(xiàn)網(wǎng)找到了負(fù)載均衡的機(jī)器,檢查兩邊的一些內(nèi)核開關(guān)和RPS配置都是一致的,說明同樣的RPS哈希算法,只是部分機(jī)器不均衡,因此這里算法側(cè)先不做進(jìn)一步挖掘。

猜想三:和RSS問題一樣,RPS也不支持對(duì)封裝后的數(shù)據(jù)進(jìn)行四元組哈希?

skb_flow_dissect是負(fù)責(zé)解析出TCP/UDP四元組,經(jīng)過初步分析,內(nèi)核是支持IPIP、GRE等通用的封裝協(xié)議,并從這些協(xié)議數(shù)據(jù)包中,取出需要的四元組信息進(jìn)行哈希。

在各種假設(shè)與折騰都沒有找到新的突破之時(shí),我們使用systemtap這個(gè)內(nèi)核調(diào)試神器,hook了關(guān)鍵的幾個(gè)函數(shù)和信息,經(jīng)過論證和測試后,在現(xiàn)網(wǎng)進(jìn)行了短暫的debug,收集到了所需要的關(guān)鍵信息。

  1. #! /usr/bin/env stap 
  2. /* 
  3. Analyse problem that softirq not balance with RPS. 
  4.  
  5. Author: dalektan@tencent.com 
  6.  
  7. Usage: 
  8. stap -p4 analyse_rps.stp -m stap_analyse_rps 
  9. staprun -x cpuid stap_analyse_rps.ko 
  10. */ 
  11.  
  12.  
  13. // To record how cpu changed with rps execute 
  14.  
  15. private global target_cpu = 0 
  16. private global begin_cpu 
  17. private global end_cpu 
  18.  
  19. probe begin { 
  20.   target_cpu = target() 
  21.   begin_cpu = target_cpu - 2 
  22.   end_cpu = target_cpu + 2 
  23. // 指定需要分析的cpu范圍,避免對(duì)性能產(chǎn)生影響 
  24.   printf("Prepare to analyse cpu is :%d-%d\n", begin_cpu, end_cpu) 
  25.  
  26.  
  27. // To record tsv ip addr, daddr and protocol(ipip, gre or tcp) 
  28. probe kernel.function("ip_rcv").call { 
  29.   if (cpu() >= begin_cpu && cpu() <= end_cpu) { 
  30.     ip_protocol = ipmib_get_proto($skb) 
  31.     // if not tcp, ipip, gre, then return 
  32.     if (ip_protocol == 4 || ip_protocol == 6 || ip_protocol == 47) { 
  33.       saddr = ip_ntop(htonl(ipmib_remote_addr($skb, 0))) 
  34.       daddr = ip_ntop(htonl(ipmib_local_addr($skb, 0))) 
  35.  
  36.     printf("IP %s -> %s proto:%d rx_queue:%d cpu:%d\n"
  37.            saddr, daddr, ip_protocol, $skb->queue_mapping-1, cpu()) 
  38.     } 
  39.   } 
  40.  
  41. // To record tcp states 
  42. probe tcp.receive.call { 
  43.   if (cpu() >= begin_cpu && cpu() <= end_cpu) { 
  44.      printf("TCP %s:%d -> %s:%d  syn:%d  rst:%d  fin:%d cpu:%d\n"
  45.             saddr, sport , daddr, dport, syn, rst, fin, cpu()) 
  46.   } 

通過使用上述systemtap腳本進(jìn)行分析后,我們得到了一個(gè)關(guān)鍵信息,大量GRE協(xié)議(圖中proto:47)的數(shù)據(jù)包,無論其四元組是什么,都被集中調(diào)度到了單個(gè)核心上,并且這個(gè)核心正好是軟中斷消耗熱點(diǎn)核。并且其他協(xié)議數(shù)據(jù)包未出現(xiàn)這個(gè)問題。

 

走到這里,問題漸為開朗,GRE數(shù)據(jù)包未按預(yù)期均衡到各個(gè)核心,但根據(jù)之前的分析,RPS是支持GRE協(xié)議獲取四元組的,為什么在這里,不同的四元組,依然被哈希算成了同一個(gè)目標(biāo)核呢?

3. 探究GRE數(shù)據(jù)包不均衡之謎

帶著這個(gè)問題進(jìn)一步挖掘,通過抓包以及代碼比對(duì)分析,很快有了突破,定位到了原因是:當(dāng)前內(nèi)核僅識(shí)別GRE_VERSION=0的GRE協(xié)議包并獲取其四元組信息,而我們的數(shù)據(jù)包,是GRE_VERSION=1的。

  1. // skb_flow_dissect 獲取四元組信息 
  2. switch (ip_proto) { 
  3.   case IPPROTO_GRE: { 
  4.     struct gre_hdr { 
  5.       __be16 flags; 
  6.       __be16 proto; 
  7.     } *hdr, _hdr; 
  8.  
  9.     hdr = skb_header_pointer(skb, nhoff, sizeof(_hdr), &_hdr); 
  10.     if (!hdr) 
  11.       return false
  12.     /* 
  13.      * Only look inside GRE if version zero and no 
  14.      * routing 
  15.      */ 
  16. // 只解析GRE_VERSION = 0的GRE協(xié)議數(shù)據(jù)包 
  17.     if (!(hdr->flags & (GRE_VERSION|GRE_ROUTING))) { 
  18.       proto = hdr->proto; 
  19.       nhoff += 4; 
  20.       if (hdr->flags & GRE_CSUM) 
  21.         nhoff += 4; 
  22.       if (hdr->flags & GRE_KEY) 
  23.         nhoff += 4; 
  24.       if (hdr->flags & GRE_SEQ) 
  25.         nhoff += 4; 
  26.       if (proto == htons(ETH_P_TEB)) { 
  27.         const struct ethhdr *eth; 
  28.         struct ethhdr _eth; 
  29.  
  30.         eth = skb_header_pointer(skb, nhoff, 
  31.                sizeof(_eth), &_eth); 
  32.         if (!eth) 
  33.           return false
  34.         proto = eth->h_proto; 
  35.         nhoff += sizeof(*eth); 
  36.       } 
  37.       goto again; 
  38.     } 
  39.     break; 
  40.   }  

首先,我們先確認(rèn)一下,GRE_VERSION=1是符合規(guī)范的嗎,答案是符合的,如果去了解一下PPTP協(xié)議的話,可以知道RFC規(guī)定了PPTP協(xié)議就是使用的GRE協(xié)議封裝并且GRE_VERSION=1。

那么為什么內(nèi)核這里不支持這種標(biāo)記呢?

通過在Linux社區(qū)進(jìn)行檢索,我們發(fā)現(xiàn)Linux 4.10版本開始支持PPTP協(xié)議下的GRE包識(shí)別與四元組獲取,也就是GRE_VERSION=1的情況。由于沒有加入對(duì)PPTP協(xié)議的支持,因此出現(xiàn)不識(shí)別GRE_VERSION=1(PPTP協(xié)議)的情況,RPS不會(huì)去獲取這種數(shù)據(jù)包的四元組信息,哈希后也是不均衡的,最終導(dǎo)致單核出現(xiàn)軟中斷熱點(diǎn)。

4. 如何解決RPS不均衡問題

至此,所有的問題都已經(jīng)撥開云霧,最后對(duì)于RPS不均衡問題,這里提供三種解決方案:

  1. 對(duì)于RSS網(wǎng)卡多隊(duì)列已經(jīng)是均衡的機(jī)器,可以將修改kernel.rps_ignore_l4_rxhash = 0,讓RPS直接使用網(wǎng)卡硬件哈希值,由于硬件哈希值足夠分散,因此RPS效果也是均衡的。
  2. 對(duì)于RSS網(wǎng)卡多隊(duì)列均衡的機(jī)器,通過ethtool -S/-L查看或修改網(wǎng)卡多隊(duì)列數(shù)目,如果隊(duì)列數(shù)不少于cpu核數(shù),再將多隊(duì)列通過/proc/irq/設(shè)備id/smp_affinity分散綁定到不同的cpu核。這樣可以充分利用網(wǎng)卡RSS的均衡效果,而無需打開內(nèi)核RPS。
  3. 在當(dāng)前內(nèi)核進(jìn)行修改或熱補(bǔ)丁,在RPS函數(shù)中新增對(duì)GRE_VERSION=1的GRE數(shù)據(jù)包的識(shí)別與四元組獲取。
  4. 升級(jí)內(nèi)核到Linux 4.10之后,即可支持PPTP協(xié)議包的RPS負(fù)載均衡。

總結(jié)

最后,總結(jié)一下整個(gè)問題和定位過程,我們從一次流量下降,業(yè)務(wù)有損的問題出發(fā),從最開始找不到思路,到找到軟中斷熱點(diǎn)這個(gè)關(guān)鍵點(diǎn),再到區(qū)分IP協(xié)議棧、網(wǎng)卡多隊(duì)列、內(nèi)核收包這三個(gè)層面進(jìn)行問題入手,沒有滿足于已經(jīng)解決的部分問題,不斷深挖了下去,終于各個(gè)方向擊破,撥開了問題的層層面紗,并給出了解決方案。

借著對(duì)問題的定位和解決,收獲良多,學(xué)習(xí)了內(nèi)核收包流程,熟悉了內(nèi)核問題定位工具和手段。感謝STGW組里同事,文中諸多成果都是團(tuán)隊(duì)的共同努力。

【本文為51CTO專欄作者“騰訊技術(shù)工程”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者(微信號(hào):Tencent_TEG)】

 

戳這里,看該作者更多好文

 

責(zé)任編輯:武曉燕 來源: 騰訊技術(shù)工程
相關(guān)推薦

2010-06-10 10:49:32

openSUSE使用教

2013-05-13 09:52:52

Windows內(nèi)核Linux內(nèi)核

2009-08-25 15:30:55

DataGrid We

2023-06-07 15:25:19

Kafka版本日志

2010-03-29 16:48:18

Nginx內(nèi)核優(yōu)化

2014-12-10 11:18:17

搜索社交app運(yùn)營

2016-09-20 15:21:35

LinuxInnoDBMysql

2021-01-06 09:01:05

javaclass

2009-07-09 18:15:42

JDBC事務(wù)處理

2023-11-24 11:24:16

Linux系統(tǒng)

2018-06-26 12:00:09

運(yùn)營商流量漫游5G

2010-09-26 14:08:41

Java垃圾回收

2024-08-12 14:37:38

2009-10-29 09:41:01

Linux內(nèi)核DeviceMappe

2017-08-16 16:20:01

Linux內(nèi)核態(tài)搶占用戶態(tài)搶占

2019-11-28 09:04:32

DDoS網(wǎng)絡(luò)攻擊網(wǎng)絡(luò)安全

2014-07-03 10:29:09

iOSAndroidNet Applica

2020-11-20 07:55:55

Linux內(nèi)核映射

2019-04-10 13:43:19

Linux內(nèi)核進(jìn)程負(fù)載

2023-07-17 10:44:46

ChatGPT聊天機(jī)器人
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)