【前言】
使用DPDK開發的朋友應該都了解使用dpdk的fwd線程的工作模式是polling模式,即100%輪詢的方式去加速網絡IO,這樣我們在操作系統層面上來觀察目標processer會發現usage一直為100%,但是這真的是系統的真實負載么?很顯然並不是,本文給出一種方法來計算dpdk的fwd線程的真實負載的方法。
【場景】
使用DPDK頭痛的一點就是DPDK的fwd線程工作在polling模式,會直接消耗一整個processer的計算資源,有的時候為了性能考慮,往往還會給當前processer設置isolcpus,將當前processer從內核的CFS調度器中“剝離”出來,防止有其他的task被“不長眼”的CFS調度器調度到和fwd線程同一個processer上,出現context switch,引起性能下降。
而工作在polling模式的fwd線程會出現非常蛋疼的一點就是面臨“無法有效的感知當前processer的壓力”的問題。查看操作系統的相關信息,會發現這個processer的usage一直處於100%,但是真實情況真的是這樣么?並不是,在流量處於低谷的時候,這個processer往往會出現空轉的情況,就是調用dpdk的api收包函數調100次,次次收包個數都是0,因為根本就沒有流量,所以需要一種新的方法來計算使用dpdk fwd線程的負載情況。
額外多說一點,為了防止fwd線程出現空轉,目前有不同種方法來“盡量”解決這種空轉問題,主流的通常有兩種:
- 利用sleep函數,簡單粗暴,結合內核的NAPI策略,設定一個期望收包個數值,當實際收包個數小於這個數值就判斷當前流量不大,sleep一下。
- 利用dpdk的rsc中斷來解決,由於uio驅動只有一個中斷號,因此這種方法在uio驅動基本沒法用,只能在vfio場景下用。
當然怎么防止dpdk fwd線程出現空轉的解決方法不是這篇文章想討論的主題,我后續會寫我個人的解決方案,本篇文章更多的會聚焦在如何估算負載情況。
【分析】
這里首先說明一下,這個方法里面有一部分是來自於dpdk社區的一篇文章,由intel專家ilia所寫,這里是文章原文地址,本文中會有不少部分來自於這篇文章原文,但是本篇文章在實際應用中仍然是有些地方需要注意的。
https://software.intel.com/en-us/vtune-cookbook-core-utilization-in-dpdk-apps
接下來我會結合實際應用和ilia的文章來闡述怎么“估算”dpdk fwd線程的負載情況。
先直接說結論:
在不同的流量壓力下,dpdk fwd線程對某個網卡隊列的收包行為實際上是存在一定的分布的
這句話怎么理解呢?實際上這句話就是intel專家ilia那篇文章的主要思想。這句話完整的解釋是這樣的:
在不同的流量壓力下,dpdk fwd線程對某個網卡的某條隊列進行收包操作,單位時間內的收包次數中,收到數據包的個數存在一定的分布
舉個例子:
我在10s內,執行了1000次收包操作,在滿載的流量壓力下,1000次收包可能次次收包都能收上來32個數據包;在50%壓力下,我可能只有300次收包是一次調用收上來32個包;在10%的壓力下,我可能只有不到100次的收包是一次性收上來32個包。
如果能明白這個例子,就能明白前面所說的結論,關於這個結論可以直接看【測試結果】一章,觀察測試結果是否符合結論。
以收包個數為0的次數為基准,那么可以推導出一個公式:
此公式也正是ilia的文章中提到的公式。
公式的解釋就是在循環一定次數的收包情況下,收包個數為0的次數占比,即為dpdk rx spin time,直譯就是dpdk 收包空轉次數,那么再將此值用1減去,即可得到dpdk fwd的壓力情況。
【測試結果】
以新建來測試上述公式的結果(新建比較吃cpu...)
測試結果1
測試結果2
測試結果3
【結論】
根據測試結果來看,可以清晰的看到,在不同的系統壓力下,DPDK在單位時間內的收包個數為0的次數占單位時間內總的收包次數的比重是存在一定分布的,在壓力越高的情況下,收包為0的次數越少,在100%滿載壓力的情況下,收包個數為0的次數與收包個數為32的次數比的差距已經非常巨大,這也是ilia那篇文章中主要說的內容,但是是存在以下幾個問題。
- 在真實場景下,一個fwd線程往往不止對單獨的某個網卡隊列進行收包,可能會有多個網卡隊列,這個時候怎么來評估負載呢?
- 這種估算方法實際上依賴於DPDK的收包隊列,但是如果某個fwd線程沒有隊列呢?這種情況往往發生在設備processer的數量大於所有的網卡隊列之和的情況。就是所謂的“僧多粥少”情況,面對這種情況,常見的處理方法是接管網卡驅動的processer收上來數據包后,通過計算RSS將數據包通過ring發送到其他沒有接管網卡隊列的processer上,那么面對這些沒有“搶到”網卡隊列的processer來說,怎么計算出負載呢?
- fwd線程使用了sleep方法來休眠減少空轉,理論上經過sleep后會影響最終估算的“負載”,使計算的負載值偏大。這個也很好理解,利用進程sleep的方法去減少壓力,最常見的影響就是包的時延會增大,數據包會在進程睡眠的時候在rx ring中“積壓”,那么每次收數據包的時候收到的數據包的個數就會偏多,出現0的次數就會少。
- fwd線程開了rxq中斷,這種情況也不需要用本文的方法去計算負載了,直接pidstat就可以看了...
面對上述幾個問題:
- 多隊列的情況下,我個人認為可以采取方案是取壓力最大的隊列的壓力值,實際測試之后發現效果還不錯,因此最后采用了這種方案。
- 這種情況下,我個人覺得可以通過和收網卡隊列相同的思路,在沒有接管到網卡隊列的fwd線程去收ring中的數據包時,也采用這種計算策略,當時由於需求問題,我本人並沒有驗證,有興趣的道友可以去嘗試一下。
- sleep的這種情況比較難以避免,需要根據實際情況去分析,由於采用sleep降低空轉的策略中常常會有兩個參數,一個是sleep的時間,一個是期望的權重,我本人實際測試,這兩者的參數設置的不同對負載的估算是不同的,同樣需要實際場景你測試調整。
【另一個問題:DPDK應用怎么“預見”即將可能發生的流量過載】
在估算出實際fwd線程的壓力后,會發現有這樣的一個問題,系統可以感知逼近的壓力,但是無法得知壓力的具體大小,舉個例子,當上述公式計算出最后fwd線程壓力為100%時,此時是過載還是滿載呢?在性能測試時,這兩種狀態雖然在表現上是壓力皆為100%,但是滿載的情況下,並不會發生丟包,而過載的情況下網卡會發生無差別丟包。
相信搞過性能的朋友常常會遇到一種丟包情況:
rx_missed
rx_missed,這種錯誤在性能測試時測試系統上線會常常預見,在傳統linux場景下,利用ethtool -S [port name]即可觀察到此種丟包。這種丟包常常是由於網卡的rx隊列被數據包“打爆”了,cpu收包的速度比不上實際數據包來的速度,那么就會形成類似於“漏斗”一樣的流程,漏斗上方的進水量大於漏斗下方的出水量,那么只要時間足夠,漏斗上方溢水是遲早的事情。在網卡收包時,cpu從rx ring中收取數據包,但是當流量壓力過大時,rx ring會充滿待處理的數據包,此時網卡無法再將數據包扔到rx ring中,那么網卡會將接下來來臨的數據包進行無差別丟棄,並在rx_missed計數上進行增加。
那么面對這種場景,有沒有方法可以盡可能的預見到即將可能來臨的流量高峰呢?經過上述的敘述相信心中已經有了一種答案,那就是查看網卡rx ring,查看rx ring中還有多少待處理的描述符(description)即可,這里需要對processer怎么從網卡上收包有一定了解,不了解的話也沒關系,我大概介紹一下原理接口,先上圖
圖4.收包原理圖
P.S.這個圖同樣來自ilia專家的那篇文章中,感謝專家...
上圖是一個網卡和processer常見的收包協作圖(發包就是反過來),通常網卡的rx ring上會有兩個index變量,一個叫做Head,一個叫做Tail,網卡會將收取的數據包push到Head指向的包描述符的內存中(不知道描述符是啥的童鞋就當做網卡收包向Head指針指向的空間去扔就行了),然后Head++,同樣,processer從Tail指向的包描述符的內存中去收數據包,這樣一個環形隊列,網卡作為producer,而processer作為consumer演出了一場網卡收包的協奏。這種場景下,並且網卡在將數據包扔到rx ring時,會將對應位置的包描述符回寫一個0x01的狀態位,以ixgbe驅動為例:代碼目錄drivers/net/ixgbe/base/ixgbe_type.h
/* Receive Descriptor bit definitions */ #define IXGBE_RXD_STAT_DD 0x01 /* Descriptor Done */ #define IXGBE_RXD_STAT_EOP 0x02 /* End of Packet */ #define IXGBE_RXD_STAT_FLM 0x04 /* FDir Match */ #define IXGBE_RXD_STAT_VP 0x08 /* IEEE VLAN Packet */
也就是上述代碼中的IXGBE_RXD_STAT_DD標志,那綜上所述,我們只需要統計Tai -> Head之間有多少個0X01狀態的描述符就可以確定(也就是上述圖4的右側Tail和Head之間的方塊數),目前網卡rx ring中“積存”了多少數據包。
但是,上述在操作,在DPDK的代碼中都已經實現啦!
uint32_t
ixgbe_dev_rx_queue_count(struct rte_eth_dev *dev, uint16_t rx_queue_id) { #define IXGBE_RXQ_SCAN_INTERVAL 4 volatile union ixgbe_adv_rx_desc *rxdp; struct ixgbe_rx_queue *rxq; uint32_t desc = 0; rxq = dev->data->rx_queues[rx_queue_id]; rxdp = &(rxq->rx_ring[rxq->rx_tail]); while ((desc < rxq->nb_rx_desc) && (rxdp->wb.upper.status_error & rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD))) { desc += IXGBE_RXQ_SCAN_INTERVAL; rxdp += IXGBE_RXQ_SCAN_INTERVAL; if (rxq->rx_tail + desc >= rxq->nb_rx_desc) rxdp = &(rxq->rx_ring[rxq->rx_tail + desc - rxq->nb_rx_desc]); } return desc; }
上述代碼位置:drivers/net/ixgbe/ixgbe_rxtx.c中,但是上述函數有個很大的問題就是時間復雜度很高...最極端的情況下要循環4096/4 = 1024次(網卡rx ring最大4096,4是由於上述函數遍歷的步長為4)才可以算出有多少個待處理的包。所以上述函數可以利用二分法來加速獲取計算的過程,最極端的情況下也只需要循環12次就可以算出網卡隊列中積存的數據包個數,關鍵就在於IXGBE_RXDADV_STAT_DD這個標志,這里就不說了,有興趣的可以思考一下。
那得到了網卡隊列中積存的數據包個數之后我們怎么才能判斷出是否將要出現“過載流量”呢?
這個也很簡單,只要網卡隊列中積存的數據包處於一個較低的水平,那么就不會出現丟包的可能;如果網卡隊列中積存的數據包數量突然上升,那么很有可能網卡的rx ring直接被流量打爆,在高端設備中,數據包量非常龐大的場景下,打爆最大4096長度的網卡隊列(最多只能緩存4096個數據包)就是一瞬間的事情,這個同樣也很好理解,還是以漏斗舉例,實際上,如果注水的速度小於出水的速度,無論注水的時間長短,漏斗中積存的水量必定為一個較低的水平,或者是根本不會有積存的水量;而當注水的速度大於出水的速度,那么將漏斗打滿只是時間問題。
經過我的實際測試,滿載的情況下,網卡隊列中積存的數據包一直處於300個以下的水平,但是只要測試機的網絡流量超過了系統的處理能力,網卡隊列中積存的數據包很快就上了1000以上.....
【一點想法】
單單靠ilia專家的公式只能計算出當前系統的壓力情況(且有條件限制),無法預知即將到來的流量高峰;單單靠網卡rx ring中積存的數據包個數只能判斷出即將來流量高峰,但是卻無法得知fwd線程壓力,那么我們需要一套組合拳:
if (fwd_rx_load >= 90 && rte_get_rx_queue_count() >= 512)
//需要采取措施,流量很可能即將過載