Linux性能優化12:網路IO的調度模型
這一篇總結Linux網路IO的調度模型。還是那句話,這裡僅僅是建一個便於討論問題的初步模型,更多的信息我們在初稿出來後,有機會就慢慢調整。
[基本模型]
還是老規矩,我們從最簡單的模型談起。我們把重點放在隊列和線程的模型上。
先看發送,用戶程序用socket的send之類的函數給協議棧寫入數據,協議棧使用用戶線程來驅動第一步的行為:分配skb,把數據拷貝進去,經過協議棧的處理,直到到達網卡驅動,這之前都不需要隊列。但到了網卡驅動就不行了,因為我們並不能確認網卡現在就緒了。所以到這裡需要一個等待隊列(具體隊列的形態,我們在qdisc的時候再討論)。
第二個執行流是網卡的中斷,當網卡完成了上一波數據的發送,它給CPU(驅動)發來一個中斷,中斷raise一個軟中斷完成發送的動作。
這段執行有兩個線程(我這裡把具有獨立執行能力的軟中斷也看作是一個線程),兩個隊列:網卡硬體上的隊列和協議棧的發送隊列。
接收則反過來,網卡收到足夠的消息了,給CPU發一個中斷,CPU分配skb,接收報文(實際上現代的網卡通常是把skb預分配給硬體,讓硬體自己填skb,收的時候只是替換一批空的skb,並把收好的包往上送。但這兩者在調度上原理一樣,所以我們這裡認為是一回事的),然後把skb送入協議棧(注意,這裡仍使用softirq的上下文),最後到達socket的隊列,通知用戶態的線程從隊列中獲得數據)
這裡仍是兩個隊列兩個線程。
其實協議棧常常還有第三種執行流,就是把Linux作為一個路由器或者交換機,做轉發,但對於大型商業應用,我們不靠CPU來做轉發,這個部分的壓力分析沒有什麼價值,我這裡就不討論了。
[NAPI]
現代的Linux網卡驅動通常使用napi來實現上面的流程。NAPI提供一個相對統一的處理機制,但其實不怎麼改變前面提到的整個處理過程。
NAPI的主要作用是應對越來越快的網路IO的需要:網路變快以後,從網路送上來的包可能會超過CPU的處理能力,如果讓接收程序持續佔據CPU,CPU就完全沒有時間處理上層的協議了。所以NAPI每次接收(或者發送)不允許超過特定的budget,保證有一定時間是可以讓給上層協議的。
使用NAPI後,網卡驅動的收發IRQ不再使用自己的收發處理,而是調用napi_schedule(),讓napi_schedule()激活softirq,在裡面決定每次poll多少數據,以及在polling超限的時候,等待多長時間再進行下一波調用。(每次polling有tracepoint的,trace_napi_poll,我們可以跟蹤這個點來判斷狀況)。
NAPI比較有趣的地方是,很多網卡的實現,無論是發還是收,其實用的都是NET_RX_SOFTIRQ。這個是我們分析的時候要注意的。
[Offloading]
網卡的Offload特性,比如GRO等,能在很大程度上把很多CPU協議棧必須完成的工作Offload到網卡上,但那個和調度無關,這裡忽略。
[多隊列]
有前面看CPU調度和存儲調度的經驗,我們應該很容易就看到網卡這個模型的問題了:只有4個線程,基本上無法充分利用多核的能力。所以現代網卡通常支持多隊列。用ethtool -l可以查看網卡的多隊列支持情況,下面是我們的網卡的多隊列支持輸出(每個網卡多少個隊列可以通過BIOS配置):
網卡和協議棧會把數據通過特定的Hash演算法分解到不同的隊列上,從而實現性能的提升。發送方向上,協議棧通過設置skb的queue_map參數為一個包選定使用的queue,網卡驅動也可以根據需要調整已經被選擇的隊列。接收方向上,網卡一側的演算法稱為RSS(Receive Side Scaling),它通過源、目標的IP地址和埠組成等進行Hash,算的結果通過一個轉發表調度到不同的queue上,我們可以通過ethtool -x/X來查看或者就該這個轉發表:網卡的多隊列的模型其實挺簡單的,最後就是看我們怎麼分布不同網卡和內存的距離,保證對一個的業務在靠近的Numa Node上就好了。
[qdisc]
RSS主要是針對接收方向的,發送方向的情況會更複雜一些,因為發送方向上我們需要做QoS(接收方向上你沒法做,因為你沒法隨便調整送過來的消息的順序)。Linux網路的QoS特性稱為TC(Traffic Control,實際上TC比一般意義的QoS強大得多),它工作在協議棧和網卡驅動之間。當協議棧最終決定把一個skb發到設備上的時候,它調用dev_queue_xmit()啟動調度,這時進入的不是網卡的發送函數,而是qdisc子系統(它是netdev的一部分),qdisc可以使用不同的演算法把消息分到不同的隊列(注意,這個隊列不是網卡的隊列,而是qdisc的隊列,為了區分,我們後面簡稱disc_q,每個網卡的隊列有至少一個disc_q對應)上,然後用napi的發送函數來觸發qdisc_run()(這個函數會在netif_tx_wake_queue()等函數中自動被調用)來實現真正的驅動中的發送回調。
qdisc支持的調度演算法極多,讀者們自己搜一下register_qdisc()這個函數的調用就可以領略一下。要了解所有這些演算法,可以參考man 8 tc (或者tc-<演算法名>)。
我們這裡簡單看看默認的演算法pfifo_fast體會一下:
FIFO很好理解,就是先入先出(有一個獨立的演算法叫pfifo),pfifo的隊列長度可以用tc手工設置,pfifo的隊列長度由驅動直接指明(netdev->tx_queue_len),如果超過長度沒有發出去,後續的包就會丟棄。pfifo_fast和pfifo不同的地方是,pfifo_fast可以根據報文的ToS域把報文分發到三個隊列,然後按優先順序出隊列(具體優先順序的演算法可以參考man 8 tc-prio)。這在隊列沒有發生積累的時候等於沒有用,但在隊列有積累的時候,高優先順序的包就會優先得到保證。qdisc可以玩出延遲(比如通過netem強行delay每個包的發出,隨機丟包等),限流,控制優先順序等很多花樣。它可以通過class進行疊加:
(這個配置把sfq,tbf,sfq三個演算法疊加在prio演算法中,實現不同分類的包用不同的方式調度)這也給調優帶來很多的變數,這些我們只能在具體的環境中做ftrace/perf才能跟蹤出來了(幸運的是,網路子系統的tracepoint還是很完善的)。
[ODP]
ODP(http://opendataplane.org)是ARM體系結構的協同社區Linaro推出的網路處理構架,這個構架其實和我們討論到的Linux系統調優的關係不大。因為它幾乎和Linux一點關係沒有。ODP一般用於固定使用幾個核用於數據處理的場景。比如你做一個路由器,管理,路由協議的部分使用Linux的協議棧處理,但轉發的部分用Linux這麼複雜的框架進行處理就不合適了。這種情況你可以寫一個ODP的應用程序,獨佔其中幾個核,這個應用程序直接從網卡上把包拉出來,然後處理,然後轉發出去。這個就稱為「數據面」處理。ODP設計上就預期和DPDK的網卡兼容,它對網卡的包的格式沒有強制要求,它提供一整套編程介面(內存分配,定時器,任務管理等),整個平台工作在用戶態,默認的業務模型不是基於中斷的,而是基於polling的(反正這個核除了做轉發也不做別的事情),調度方法就是多個任務(線程)調度「Queue-分發-下一個Queue」這樣的模型,所以一般應用的調優手段和優化原則可以直接用於ODP。我們這裡也沒有什麼可以特別補充的了。
推薦閱讀:
※利用Unity UGUI製作酷炫UI效果(製作篇)
※Wukong 反作弊系統緩存的優化
※#每天一個小目標#Unity技術分享(一)
※Linux下做性能分析6:理解一些基礎的CPU執行模型