成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

從STGW流量下降探秘內核收包機制

企業動態
在STGW現網運營中,出現了一起流量突然下降的Case,此時我們的健康撥測機制探測到失敗,并且用戶側重試次數增多、請求延遲增大。

 問題現象

在STGW現網運營中,出現了一起流量突然下降的Case,此時我們的健康撥測機制探測到失敗,并且用戶側重試次數增多、請求延遲增大。但通過已有的各類監控進行定位,只發現整體CPU、內存、進程狀態、QPS(每秒請求數)等關鍵指標雖然出現波動,但均未超過告警水位。

[[313051]]

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

 

但是,整體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。

  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. // 遍歷桶中的節點 
  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. }  

相對來說并不復雜的lookup_listener函數為什么會造成這么大的cpu開銷?經過進一步定位后,發現問題所在:listen哈希桶開的太小了,只有32個。

  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. */ 

為什么內核這里會覺得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地址查找。

  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. // 根據目的端口進行第一次哈希 
  12.   hash2 = ipv4_portaddr_hash(net, daddr, hnum); 
  13.   ilb2 = inet_lhash2_bucket(hashinfo, hash2); 
  14. // 根據四元組信息再做一次查找 
  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監聽范圍查找 
  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,在發現指定的端口開啟了reuseport后,不再是遍歷式的去獲取到合適的socket,而是將其看成一個整體,二次哈希后,調用 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,進行二次哈希查找 
  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; 

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

1. 修改內核哈希桶大小,根據reuseport增加socket的倍數,相應提高INET_LHTABLE_SIZE,或者直接改成例如2048

  1. #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)

這里我們著重檢查了幾個網卡多隊列的參數

  1. // 檢查網卡的隊列數 
  2. ethtool -l eth0 
  3. Current hardware settings: 
  4. RX:             0 
  5. TX:             0 
  6. Other:          1 
  7. Combined:       48 
  8.  
  9. // 檢查硬件哈希開關 
  10. ethtool -k eth0 
  11. receive-hashing: on 
  12.  
  13. // 檢查硬件哈希的參數,這里顯示以TCP是以四元組信息進行哈希 
  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] 

這些參數都是符合預期的,數據包會根據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協議棧之前,經歷了這些步驟:

  1. 網口(NIC)收到packets
  2. 網口通過DMA(Direct memeory access)將數據寫入到內存(RAM)中。
  3. 網口通過RSS(網卡多隊列)將收到的數據包分發給某個rx隊列,并觸發該隊列所綁定核上的CPU中斷。
  4. 收到中斷的核,調用該核所在的內核軟中斷線程(softirqd)進行后續處理。
  5. softirqd負責將數據包從RAM中取到內核中。
  6. 如果開啟了RPS,RPS會選擇一個目標cpu核來處理該包,如果目標核非當前正在運行的核,則會觸發目標核的IPI(處理器之間中斷),并將數據包放在目標核的backlog隊列中。
  7. 軟中斷線程將數據包(數據包可能來源于第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相關代碼并進行分析,我們再次發現了一些可疑的點

  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. // 計算哈希值 
  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. // 通過哈希值計算出目標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四元組進行計算 
  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未打開,導致不均衡?

通過代碼發現 rps_ignore_l4_rxhash 會影響當前是否計算哈希值,當前機器未設置ignore_l4_rxhash,則內核會直接使用網卡RSS計算出的哈希值,根據上面定位的網卡RSS不均衡的結論,RSS哈希值可能是不準的,這里會導致問題嗎?

我們將ignore_l4_rxhash開關進行打開

  1. 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,收集到了所需要的關鍵信息。

  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范圍,避免對性能產生影響 
  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腳本進行分析后,我們得到了一個關鍵信息,大量GRE協議(圖中proto:47)的數據包,無論其四元組是什么,都被集中調度到了單個核心上,并且這個核心正好是軟中斷消耗熱點核。并且其他協議數據包未出現這個問題。

 

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

3. 探究GRE數據包不均衡之謎

帶著這個問題進一步挖掘,通過抓包以及代碼比對分析,很快有了突破,定位到了原因是:當前內核僅識別GRE_VERSION=0的GRE協議包并獲取其四元組信息,而我們的數據包,是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協議數據包 
  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.   }  

首先,我們先確認一下,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不均衡問題,這里提供三種解決方案:

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

總結

最后,總結一下整個問題和定位過程,我們從一次流量下降,業務有損的問題出發,從最開始找不到思路,到找到軟中斷熱點這個關鍵點,再到區分IP協議棧、網卡多隊列、內核收包這三個層面進行問題入手,沒有滿足于已經解決的部分問題,不斷深挖了下去,終于各個方向擊破,撥開了問題的層層面紗,并給出了解決方案。

借著對問題的定位和解決,收獲良多,學習了內核收包流程,熟悉了內核問題定位工具和手段。感謝STGW組里同事,文中諸多成果都是團隊的共同努力。

【本文為51CTO專欄作者“騰訊技術工程”原創稿件,轉載請聯系原作者(微信號:Tencent_TEG)】

 

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

 

責任編輯:武曉燕 來源: 騰訊技術工程
相關推薦

2010-06-10 10:49:32

openSUSE使用教

2013-05-13 09:52:52

Windows內核Linux內核

2009-08-25 15:30:55

DataGrid We

2023-06-07 15:25:19

Kafka版本日志

2010-03-29 16:48:18

Nginx內核優化

2014-12-10 11:18:17

搜索社交app運營

2021-01-06 09:01:05

javaclass

2023-11-24 11:24:16

Linux系統

2009-07-09 18:15:42

JDBC事務處理

2016-09-20 15:21:35

LinuxInnoDBMysql

2010-09-26 14:08:41

Java垃圾回收

2024-08-12 14:37:38

2018-06-26 12:00:09

運營商流量漫游5G

2019-11-28 09:04:32

DDoS網絡攻擊網絡安全

2017-08-16 16:20:01

Linux內核態搶占用戶態搶占

2009-10-29 09:41:01

Linux內核DeviceMappe

2014-07-03 10:29:09

iOSAndroidNet Applica

2019-04-10 13:43:19

Linux內核進程負載

2020-11-20 07:55:55

Linux內核映射

2012-12-14 10:15:32

新浪CDN代碼發布部署
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲精品一二三区 | 97av视频在线 | 久久精品国产一区二区三区 | 国产美女永久免费无遮挡 | 国产91久久精品一区二区 | h视频免费在线观看 | 精品国产乱码久久久久久88av | 亚洲精品福利在线 | 欧美成人一区二区三区 | 久久久久久久av麻豆果冻 | 99亚洲精品 | 日韩中文字幕一区二区 | 欧美日本一区二区 | 男女羞羞视频在线免费观看 | 亚洲经典一区 | av在线免费观看网站 | 亚洲精品av在线 | 国产成人一区二区三区久久久 | 九九热精品视频在线观看 | 国产精品视频一区二区三区 | 日韩欧美精品在线 | 国产在线播 | 国产精品一区二区视频 | 久久精品国产a三级三级三级 | 亚洲成av人片在线观看无码 | 91在线网站 | 中国一级特黄真人毛片免费观看 | 免费黄色av | 亚洲成人精品 | 精品久久久久久久久久久 | 色偷偷噜噜噜亚洲男人 | 亚洲天堂成人在线视频 | 欧美日韩国产精品 | 一区二区免费在线 | 97久久久久久久久 | 三级av在线| 亚洲免费在线观看 | 婷婷一级片 | 日韩在线免费播放 | 一级免费毛片 | 久久国产精品一区二区 |