Linux轉發性能評估與優化(轉發瓶頸分析與解決方式)


線速問題

非常多人對這個線速概念存在誤解。

覺得所謂線速能力就是路由器/交換機就像一根網線一樣。

而這,是不可能的。應該考慮到的一個概念就是延遲。

數據包進入路由器或者交換機,存在一個核心延遲操作,這就是選路。對於路由器而言,就是路由查找,對於交換機而言。就是查詢MAC/port映射表,這個延遲是無法避開的。這個操作須要大量的計算機資源。所以無論是路由器還是交換機。數據包在內部是不可能像在線纜上那樣近光速傳輸的。

類比一下你經過十字街頭的時候。是不是要左顧右盼呢?

       那么。設備的線速能力怎么衡量呢?假設一個數據包經過一個路由器。那么延遲必覽無疑,可是設備都是有隊列或者緩沖區的,那么試想一個數據包緊接一個數據包從輸入port進入設備,然后一個數據包緊接一個數據包從輸出port發出。這是能夠做到的。我們對數據包不予編號,因此你也就無法推斷出來的數據包是不是剛剛進去的那個了,這就是線速。



       我們能夠用電容來理解轉發設備。有人可能會覺得電容具有通高頻阻低頻的功效,我說的不是這個,所以咱不考慮低頻。僅以高頻為例,電容具有存儲電荷的功能,這就相似存儲轉發。電容充電的過程相似於數據包進入輸入隊列緩沖區,電容放電的過程相似於數據包從輸出緩沖區輸出,我們能夠看到,在電流經過電容的前后,其速度是不變的,然而針對詳細的電荷而言,從電容放出的電荷絕不是剛剛在在還有一側充電的那個電荷,電容的充電放電擁有固有延遲。

       我們回到轉發設備。對於交換機和路由器而言,衡量標准是不同的。

       對於交換機而言,線速能力是背板總帶寬。由於它的查表操作導致的延遲並不大,大量的操作都在數據包通過交換矩陣的過程。因此背板帶寬直接導致了轉發效率。而對於路由器,衡量標准則是一個port每秒輸入輸出最小數據包的數量,假設數據包以每秒100個進入,每秒100個流出,那么其線速就是100pps。

       本文針對路由器而不針對交換機。

路由器的核心延遲在路由查找,而這個查找不會受到數據包長度的影響,因此決定路由器線速能力的核心就在數據包輸出的效率。注意。不是數據包輸入的效率,由於僅僅要隊列足夠長,緩存足夠大,輸入總是線速的。可是輸入操作就涉及到了怎樣調度的問題。

這也就說明了為何非常多路由器都支持輸出流量控制而不是輸入流量控制的原因。由於輸入流控即使完美完畢,它也會受到路由器輸出port自身輸出效率的影響。流控結果將不再准確。



       在寫這個方法的前晚,有一個故事。我近期聯系到了初中時一起玩搖滾玩音響的超級鐵的朋友。他如今搞舞台設計,燈光音響之類的。我問他在大型舞台上,音箱擺放的位置不同。距離后級。前置,音源也不同,怎么做到不同聲道或者相同聲道的聲音同步的,要知道,好的耳朵能夠聽出來毫秒級的音差...他告訴我要統一到達時間。即統一音頻流到達各個箱子的時間。而這要做的就是調延遲,要求不同位置的箱子路徑上要有不同的延遲。這對我的設計方案的幫助是多么地大啊。



       然后,在第二天,我就開始整理這個令人悲傷終於心碎的Linux轉發優化方案。

聲明本文僅僅是一篇普通文章。記錄這個方法的點點滴滴,並非一個完整的方案,請勿在格式上較真,內容上也僅僅是寫了些我覺得重要且有意思的。完整的方案是不便於以博文的形式發出來的。見諒。

問題綜述

Linux內核協議棧作為一種軟路由運行時,和其他通用操作系統自帶的協議棧相比。其效率並非例如以下文所說的那樣非常低。然而基於工業路由器的評判標准,確實是低了。
 
       市面上各種基於Linux內核協議棧的路由器產品,甚至網上也有大量的此類文章,比方什么將Linux變成路由器之類的。無非就是打開ip_forward,加幾條iptables規則,搞個配置起來比較方便的WEB界面...我想說這些太低級了。甚至超級低級。我非常想談一下關於專業路由器的我的觀點。可是今天是小小的生日,玩了一天。就不寫了。僅僅是把我的方案整理出來吧。

       Linux的轉發效率究竟低在哪兒?怎樣優化?這是本文要解釋的問題。

依舊如故。本文能夠隨意轉載並基於這個思路實現編碼,可是一旦用於商業目的,不保證沒有個人或組織追責。因此文中我盡量採用盡可能模糊的方式闡述細節。

瓶頸分析概述


1.DMA和內存操作

我們考慮一下一個數據包轉發流程中須要的內存操作。臨時不考慮DMA。
*)數據包從網卡復制到內存
*)CPU訪問內存讀取數據包元數據
*)三層報頭改動,如TTL
*)轉發到二層后封裝MAC頭
*)數據包從內存復制到輸出網卡
這幾個典型的內存操作為什么慢?為什么我們總是對內存操作有這么大的意見?由於訪問內存須要經過總線,首先總線競爭(特別在SMP和DMA下)就是一個打群架的過程。另外由於內存自身的速度和CPU相比差了幾個數量級。這么玩下去,肯定會慢啊!

所以一般都是盡可能地使用CPU的cache,而這須要一定的針對局部性的數據布局,對於數據包接收以及其他IO操作而言。由於數據來自外部。和進程運行時的局部性利用沒法比。

所以必須採用相似Intel I/OAT的技術才干改善。



1.1.Linux作為server時

採用標准零拷貝map技術全然勝任。這是由於。運行於Linux的server和線速轉發相比就是個蝸牛,server在處理client請求時消耗的時間是一個硬性時間,無法優化。這是代償原理。Linux服務唯一須要的就是能高速取到client的數據包,而這能夠通過DMA高速做到。

本文不再詳細討論作為server運行的零拷貝問題,自己百度吧。



1.2.Linux作為轉發設備時

須要採用DMA映射交換的技術才干實現零拷貝。這是Linux轉發性能低下的根本。由於輸入port的輸入隊列和輸出port的輸出隊列互不相識。導致了不能更好的利用系統資源以及多port數據路由到單port輸出隊列時的隊列鎖開銷過大。總線爭搶太嚴重。DMA影射交換須要超級棒的數據包隊列管理設施。它用來調度數據包從輸入port隊列到輸出port隊列,而Linux差點兒沒有這樣的設施。



盡管近年在路由器領域有人提出了輸入隊列管理。可是這項技術對於Linux而言就是還有一個世界,而我,把它引入了Linux世界。

2.網卡對數據包隊列Buff管理

在Linux內核中。差點兒對於全部數據結構,都是須要時alloc。完畢后free,即使是kmem_cache。效果也一般,特別是對於高速線速設備而言(skb內存拷貝,若不採用DMA。則會頻繁拷貝。即便採用DMA。在非常多情況下也不是零拷貝)。

       即使是高端網卡在skb的buffer管理方面,也沒有使用全然意義上的預分配內存池,因此會由於頻繁的內存分配。釋放造成內存顛簸,眾所周知,內存操作是問題的根本,由於它涉及到CPU Cache。總線爭搶,原子鎖等,實際上,內存管理才是根本中的根本,這里面道道太多,它直接影響CPU cache。后者又會影響總線...從哪里分配內存,分配多少,何時釋放,何時能夠重用,這就牽扯到了內存區域着色等技術。通過分析Intel千兆網卡驅動,在我看來,Linux並沒有做好這一點。

3.路由查找以及其他查找操作

Linux不區分對待路由表和轉發表,每次都要最長前綴查找。盡管海量路由表時trie算法比hash算法好,可是在路由分布畸形的情況下依舊會使trie結構退化,或者頻繁回溯。路由cache效率不高(查詢代價太大,不固定大小,僅有弱智的老化算法,導致海量地址訪問時。路由cache沖突鏈過長),終於在內核協議棧中下課。

       假設沒有一個好的轉發表,那么Linux協議棧在海量路由存在時對於線速能力就是一個瓶頸,這是一個可擴展性問題。



       另外。非常多的查詢結果都是能夠被在一個地方緩存的,可是Linux協議棧沒有這樣的緩存。

比方。路由查詢結果就是下一跳,而下一跳和輸出網卡關聯,而輸出網卡又和下一跳的MAC地址以及將要封裝的源MAC地址關聯,這些本應該被緩存在一個表項。即轉發表項內,然而Linux協議棧沒有這么做。

4.不合理的鎖

為何要加鎖。由於SMP。

然而Linux內核差點兒是對稱的加鎖,也就是說,比方每次查路由表時都要加鎖,為何?由於怕在查詢的期間路由表改變了...然而你細致想想,在高速轉發情景下,查找操作和改動操作在單位時間的比率是多少呢?不要以為你用讀寫鎖就好了,讀寫鎖不也有關搶占的操作嗎(盡管我們已經建議關閉了搶占)?起碼也浪費了幾個指令周期。這些時間幾率不正確稱操作的加鎖是不必要的。



       你僅僅須要保證內核本身不會崩掉就可以,至於說IP轉發的錯誤,無論也罷。依照IP協議,它本身就是一個盡力而為的協議。

5.中斷與軟中斷調度

Linux的中斷分為上半部和下半部。動態調度下半部。它能夠在中斷上下文中運行。也能夠在獨立的內核線程上下文中運行。因此對於實時需求的環境。在軟中斷中處理的協議棧處理的運行時機是不可預知的。Linux原生內核並沒有實現Solaris。Windows那樣的中斷優先級化。在某些情況下,Linux靠着自己動態的且及其優秀的調度方案能夠達到極高的性能,然而對於固定的任務,Linux的調度機制卻明顯不足。

       而我須要做的,就是讓不固定的東西固定化。

6.通用操作系統內核協議棧的通病

作為一個通用操作系統內核,Linux內核並非僅僅處理網絡數據,它還有非常多別的子系統,比方各種文件系統。各種IPC等,它能做的僅僅是可用,簡單。易擴展。

       Linux原生協議棧全然未經網絡優化,且基本裝機在硬件相同也未經優化的通用架構上。網卡接口在PCI-E總線上,假設DMA管理不善。總線的占用和爭搶帶來的性能開銷將會抵消掉DMA本意帶來的優點(其實對於轉發而言並沒有帶來什么優點。它僅僅對於作為server運行的Linux有優點,由於它僅僅涉及到一塊網卡)

[ 注意,我覺得內核處理路徑並非瓶頸,這是分層協議棧決定的,瓶頸在各層中的某些操作,比方內存操作(固有開銷)以及查表操作(算法不好導致的開銷)]

綜述:Linux轉發效率受到下面幾大因素影響


IO/輸入輸出的隊列管理/內存改動拷貝 (又一次設計相似crossbar的隊列管理實現DMA ring交換)
各種表查詢操作,特別是最長前綴匹配,諸多本身唯一確定的查詢操作之間的關聯沒有建立
SMP下處理器同步(鎖開銷)(使用大讀鎖以及RCU鎖)以及cache利用率
中斷以及軟中斷調度

Linux轉發性能提升方案

概述

此方案的思路來自基於crossbar的新一代硬件路由器。設計要點:

1.又一次設計的DMA包管理隊列( 思路來自Linux O(1)調度器,crossbar陣列以及VOQ[虛擬輸出隊列]
2.又一次設計的基於定位而非最長前綴查找的轉發表
3.長線程處理(中斷線程化,處理流水線化,添加CPU親和)
4.數據結構無鎖化(基於線程局部數據結構)
5.實現方式
5.1.驅動以及內核協議棧改動
5.2.全然的用戶態協議棧
5.3.評估:用戶態協議棧靈活,可是在某些平台要處理空間切換導致的cache/tlb/mmu表的flush問題

內核協議棧方案

優化框架

0.例行優化

1).網卡多隊列綁定特定CPU核心(利用RSS特性分別處理TX和RX)
[ 能夠參見《Effective Gigabit Ethernet Adapters-Intel千兆網卡8257X性能調優]
2).依照包大小統計動態開關積壓延遲中斷ThrottleRate以及中斷Delay(對於Intel千兆卡而言)
3).禁用內核搶占,降低時鍾HZ,由中斷粒度驅動(見上面)
4).假設不准備優化Netfilter,編譯內核時禁用Netfilter,節省指令
5).編譯選項去掉DEBUG和TRACE,節省指令周期
6).開啟網卡的硬件卸載開關(假設有的話)
7).最小化用戶態進程的數量,降低其優先級
8).原生網絡協議棧優化
    由於不再作為通用OS。能夠讓除了RX softirq的task適當飢餓
    *CPU分組(考慮Linux的cgroup機制),划一組CPU為數據面CPU,每個CPU綁定一個RX softirq或者
    *添加rx softirq一次運行的netdev_budget以及time limit,或者
    *僅僅要有包即處理,每個。控制面/管理面的task能夠綁在別的CPU上。



宗旨:
原生協議棧的最優化方案

1.優化I/O,DMA,降低內存管理操作

    1).降低PCI-E的bus爭用,採用crossbar的全交叉超立方開關的方式
        [ Tips:16 lines 8 bits PCI-E總線拓撲(非crossbar!)的網絡線速不到滿載60% pps]
    2).降低爭搶式DMA,降低鎖總線[Tips:優化指令LOCK,最好採用RISC,方可調高內核HZ]
        [ Tips:交換DMA映射,而不是在輸入/輸出buffer ring之間拷貝數據!

如今。僅僅有傻逼才會在DMA情況拷貝內存。正確的做法是DMA重映射,交換指針!]
    3).採用skb內存池,避免頻繁內存分配/釋放造成的內存管理框架內的抖動
        [Tips:每線程負責一塊網卡(甚至輸入和輸出由不同的線程負責會更好)。保持一個預分配可循環利用的ring buffer,映射DMA]

宗旨:
降低cache刷新和tlb刷新,降低內核管理設施的工作(比方頻繁的內存管理)

2.優化中斷分發

1).添加長路徑支持。降低進程切換導致的TLB以及Cache刷新
2).利用多隊列網卡支持中斷CPU親和力利用或者模擬軟多隊列提高並行性
3).犧牲用戶態進程的調度機會。全部精力集中於內核協議棧的處理,多CPU多路並行的
    [Tips:假設有超多的CPU。建議划分cgroup]
4).中斷處理線程化,內核線程化,多核心並行運行長路經,避免切換抖動
5).線程內部,依照IXA NP微模塊思想採用模塊化(方案未實現,待商榷)

宗旨:
降低cache刷新和tlb刷新
降低協議棧處理被中斷過於頻繁打斷[要么使用IntRate。要么引入中斷優先級]

3.優化路由查找算法

1).分離路由表和轉發表,路由表和轉發表同步採用RCU機制
2).盡量採用線程局部數據
每個線程一張轉發表(由路由表生成。OpenVPN多線程採用,但失敗),採用定位而非最長前綴查找(DxR或者我設計的那個)。若不採用為每個線程復制一份轉發表,則須要又一次設計RW鎖或者使用RCU機制。
3).採用hash/trie方式以及DxR或者我設計的DxRPro定位結構

宗旨:
採用定位而非查找結構
採用局部表,避免鎖操作

4.優化lock

1).查詢定位局部表。無鎖(甚至RW鎖都沒有)不禁止中斷
2).臨界區和內核線程關聯,不禁中斷,不禁搶占(其實內核編譯時搶占已經關閉了)
3).優先級鎖隊列替換爭搶模型。維持cache熱度
4).採用Windows的自旋鎖機制
        [Tips:Linux的ticket spin lock由於採用探測全局lock的方式,會造成總線開銷和CPU同步開銷,Windows的spin lock採用了探測CPU局部變量的方式實現了真正的隊列lock,我設計的輸入輸出隊列管理結構(下面詳述)思路部分來源於Windows的自旋鎖設計]

宗旨:鎖的粒度與且僅與臨界區資源關聯,粒度最小化


優化細節概覽

1.DMA與輸入輸出隊列優化


1.1.問題出在哪兒
假設你對Linux內核協議棧足夠熟悉。那么就肯定知道,Linux內核協議棧正是由於軟件project里面的天天普及的“一件好事”造成了轉發性能低效。這就是“解除緊密耦合”。

       Linux協議棧轉發和Linuxserver之間的根本差別在於,后者的應用服務並不在乎數據包輸入網卡是哪個,它也不必關心輸出網卡是哪一個,然而對於Linux協議棧轉發而言,輸入網卡和輸出網卡之間確實是有必要相互感知的。Linux轉發效率低的根本原因不是路由表不夠高效,而是它的隊列管理以及I/O管理機制的低效,造成這樣的低效的原因不是技術實現上難以做到,而是Linux內核追求的是一種靈活可擴展的性能。這就必須解除出入網卡,驅動和協議棧之間關於數據包管理的緊密耦合。

       我們以Intel千兆網卡驅動e1000e來說明上述的問題。順便說一句,Intel千兆驅動亦如此。其他的就更別說了,其根源在於通用的網卡驅動和協議棧設計並非針對轉發優化的。

初始化:
創建RX ring:RXbuffinfo[MAX]
創建TX ring:TXbuffinfo[MAX]

RX過程:
i = 當前RX ring游歷到的位置;
while(RXbuffinfo中有可用skb) {
        skb = RXbufferinfo[i].skb;
        RXbuffinfo[i].skb = NULL;
        i++;
        DMA_unmap(RXbufferinfo[i].DMA);
        [Tips:至此。skb已經和驅動脫離,全然交給了Linux協議棧]
        [Tips:至此,skb內存已經不再由RX ring維護。Linux協議棧拽走了skb這塊內存]
        OS_receive_skb(skb);
        [Tips:由Linux協議棧負責釋放skb,調用kfree_skb之類的接口]
        if (RX ring中被Linux協議棧摘走的skb過多) {
                alloc_new_skb_from_kmem_cache_to_RXring_RXbufferinfo_0_to_MAX_if_possible;
                [Tips:從Linux核心內存中再次分配skb]
        }
}

TX過程:
skb = 來自Linux協議棧dev_hard_xmit接口的數據包;
i = TX ring中可用的位置
TXbufferinfo[i].skb = skb;
DMA_map(TXbufferinfo[i].DMA);
while(TXbufferinfo中有可用的skb) {
        DMA_transmit_skb(TXbufferinfo[i]);
}
[異步等待傳輸完畢中斷或者在NAPI poll中主動調用]
i = 傳輸完畢的TXbufferinfo索引
while(TXbufferinfo中有已經傳輸完畢的skb) {
        skb = TXbufferinfo[i];
        DMA_unmap(TXbufferinfo[i].DMA);
        kfree(skb);
        i++;
}
以上的流程能夠看出,在持續轉發數據包的時候。會涉及大量的針對skb的alloc和free操作。假設你覺得上面的代碼不是那么直觀,那么下面給出一個圖示:



       頻繁的會發生從Linux核心內存中alloc skb和free skb的操作。這不僅僅是不必要的,並且還會損害CPU cache的利用。不要寄希望於keme_cache,我們能夠看到,全部的網卡和socket差點兒是共享一塊核心內存的,盡管能夠通過dev和kmem cache來優化,但非常遺憾,這個優化沒有質的飛躍。

1.2.構建新的DMA ring buffer管理設施-VOQ,建立輸入/輸出網卡之間隊列的關聯。
類比Linux O(1)調度器算法,每個cpu全局維護一個唯一的隊列,散到各個網卡,靠交換隊列的DMA映射指針而不是拷貝數據的方式優化性能,達到零拷貝。這僅僅是其一。關於交換DMA映射指針而不是拷貝數據這一點不多談,由於差點兒全部的支持DMA的網卡驅動都是這么做的。假設它們不是這么做的,那么肯定有人會將代碼改成這么做的。

       假設類比高端路由器的crossbar交換陣列結構以及真實的VOQ實現,你會發現,在邏輯上,每一對可能的輸入/輸出網卡之間維護一條數據轉發通路是避免隊頭堵塞以及競爭的好方法。

這樣排隊操作僅僅會影響單獨的網卡。不須要再全局加鎖。在軟件實現上,我們相同能夠做到這個。你要明白,Linux的網卡驅動維護的隊列信息被內核協議棧給割裂,從此。輸入/輸出網卡之間彼此失聯。導致最優的二分圖算法無法實施。

       其實,你可能覺得把網卡作為一個集合,把須要輸出的數據包最為還有一個集合,轉發操作須要做的就是建立數據包和網卡之間的一條路徑,這是一個典型的二分圖匹配問題。然而假設把建立路徑的操作與二分圖問題分離,這就是不再是網卡和數據包之間的二分圖匹配問題了。

由於分離出來的路由模塊導致了針對每個要轉發的數據包。其輸出網卡是唯一確定的。這個問題變成了處理輸出網卡輸出操作的CPU集合和輸出網卡之間的二分圖匹配問題。



       這里有一個優化點,那就是假設你有多核CPU。那么就能夠為每一塊網卡的輸出操作綁定一個唯一的CPU,二分圖匹配問題迎刃而解,剩下的就是硬件總線的爭用問題(對於高性能crossbar路由器而言,這也是一個二分圖匹配問題。但對於總線結構的通用系統而言有點差別,后面我會談到)了。作為我們而言,這一點除了使用性價比更高的總線,比方我們使用PCI-E 16Lines 8 bits,沒有別的辦法。作為一個全然的方案。我不能寄希望於底層存在一個多核CPU系統,假設僅僅有一個CPU,那么我們能寄希望於Linux進程調度系統嗎?還是那個觀點,作為一個通用操作系統內核,Linux不會針對網絡轉發做優化,於是乎。進程調度系統是此方案的還有一個優化點,這個我后面再談。

       最后。給出我的數據包隊列管理VOQ的設計方案草圖。





在我的這個針對Linux協議棧的VOQ設計中,VOQ總要要配合良好的輸出調度算法,才干發揮出最佳的性能。

2.分離路由表和轉發表以及建立查找操作之間的關聯

Linux協議棧是不區分對待路由表和轉發表的,而這在高端路由器上顯然是必須的。誠然,我沒有想將Linux協議棧打造成比肩專業路由器的協議棧。然而通過這個排名第二的核心優化,它的轉發效率定會更上一層樓。

       在大約三個月前。我參照DxR結構以及借鑒MMU思想設計了一個用於轉發的索引結構,能夠實現3步定位。無需做最長前綴匹配過程,詳細能夠參見我的這篇文章以DxR算法思想為基准設計出的路由項定位結構圖解。我在此就不再深度引用了。須要注意的是,這個結構能夠依據現行的Linux協議棧路由FIB生成,並且在路由項不規則的情況下能夠在最差情況下動態回退到標准DxR,比方路由項不可匯聚。路由項在IPv4地址空間划分區間過多且分布不均。我將我設計的這個結構稱作DxR Pro++。



       至於說查找操作之間的關聯,這也是一個深度優化,底層構建高速查詢流表實現協議棧短路(流表可參照conntrack設計),這個優化思想直接參照了Netfilter的conntrack以及SDN流表的設計。

盡管IP網絡是一個無狀態網絡,中間路由器的轉發策略也應該是一個無狀態的轉發。然而這是形而上意義上的理念。

假設談到深度優化。就不得不犧牲一點清純性。



       設計一個流表。流的定義能夠不必嚴格依照五元組。而是能夠依據協議頭的隨意字段,每個表項中保存的信息包含但不限於下面的元素:
*流表緩存路由項
*流表緩存neighbour
*流表緩存NAT
*流表緩存ACL規則       
*流表緩存二層頭信息

這樣能夠在協議棧的底層保存一張能夠高速查詢的流表,協議棧收到skb后匹配這張表的某項,一旦成功,能夠直接取出相關的數據(比方路由項)直接轉發,理論上僅僅有一個流的第一個數據包會走標准協議棧的慢速路徑(其實,經過DxR Pro++的優化,一經不慢了...)。

在直接高速轉發中,須要運行一個HOOK,運行標准的例行操作。比方校驗和,TTL遞減等。

  
        關於以上的元素,特別要指出的是和neighbour與二層信息相關的。數據轉發操作一向被覺得瓶頸在發不在收,在數據發送過程。會涉及到下面耗時的操作:>加入輸出網卡的MAC地址作為源-內存拷貝>加入next hop的MAC地址作為目標-內存拷貝又一次,我們遇到了內存操作,惱人的內存操作!假設我們把這些MAC地址保存在流表中,能夠避免嗎?貌似僅僅是能夠高速定位,而無法避免內存拷貝...再一次的,我們須要硬件的特性來幫忙,這就是分散聚集I/O(Scatter-gather IO)。原則上。Scatter-gather IO能夠將不連續的內存當成連續的內存使用。進而直接映射DMA。因此我們僅僅須要告訴控制器,一個將要發送的幀的MAC頭的位置在哪里,DMA就能夠直接傳輸,沒有必要將MAC地址復制到幀頭的內存區域。例如以下圖所看到的:



特別要注意,上述的流表緩存項中的數據存在大量冗余,由於next hop的MAC地址。輸出網卡的MAC地址,這些是能夠由路由項唯一確定的。之所以保存冗余數據,其原則還是為了優化,而標准的通用Linux內核協議棧,它卻是要避免冗余的...既然保存了冗余數據,那么慢速路徑的數據項和高速路經的數據項之間的同步就是一個必須要解決的問題。

我基於讀寫的不正確稱性,着手採用event的方式通知更新,比方慢速路徑中的數據項(路由。MAC信息。NAT,ACL信息等)。一旦這些信息更改,內核會專門觸發一個查詢操作,將高速流表中與之相關的表項disable掉就可以。

值得注意的是,這個查詢操作不是必需太快,由於相比較高速轉發而言。數據同步的頻率要慢天文數字個數量級...相似Cisco的設備。能夠創建幾個內核線程定期刷新慢速路徑表項,用來發現數據項的更改。從而觸發event。



[Tips:能夠高速查找的流表結構可用多級hash(採用TCAM的相似方案),也能夠借鑒我的DxR Pro++結構以及nf-HiPac算法的多維區間匹配結構,我個人比較推崇nf-HiPac]


3.路由Cache優化

雖說Linux的路由cache早已下課,可是它下課的原因並非cache機制本身不好,而是Linux的路由cache設計得不好。因此下面幾點能夠作為優化點來嘗試。


*)限制路由軟cache的大小。保證查找速度[實施精心設計的老化算法和替換算法]
[利用互聯網訪問的時間局部性以及空間局部性(須要利用計數統計)]
[自我PK:假設有了我的那個3步定位結構。難道還用的到路由cache嗎]
*)預制經常使用IP地址到路由cache,實現一步定位
[所謂經常使用IP須要依據計數統計更新,也能夠靜態設置]

4.Softirq在不支持RSS多隊列網卡時的NAPI調度優化

*)將設備依照協議頭hash值均勻放在不同CPU,遠程喚醒softirq,模擬RSS軟實現
眼下的網絡接收軟中斷的處理機制是,哪個CPU被網卡中斷了,哪個CPU就處理網卡接收軟中斷,在網卡僅僅能中斷固定CPU的情況下,這會影響並行性,比方僅僅有兩塊網卡。卻有16核CPU。

怎樣將盡可能多的CPU核心調動起來呢?這須要改動網絡接收軟中斷處理邏輯。我希望多個CPU輪流處理數據包。而不是固定被中斷的數據包來處理。改動邏輯例如以下:

1.全部的rx softirq內核線程組成一個數組

struct task_struct rx_irq_handler[NR_CPUS];

2.全部的poll list組成一個數組
struct list_head polll[NR_CPUS];

3.引入一把保護上述數據的自旋鎖
spinlock_t rx_handler_lock;

4.改動NAPI的調度邏輯
void __napi_schedule(struct napi_struct *n)
{
    unsigned long flags;

    static int curr = 0;
    unsigned int hash = curr++%NR_CPUS;
    local_irq_save(flags);
    spin_lock(&rx_handler_lock);
    list_add_tail(&n->poll_list, polll[hash]);
    local_softirq_pending(hash) |= NET_RX_SOFTIRQ;
    spin_unlock(&rx_handler_lock);
    local_irq_restore(flags);
}

[Tips:注意和DMA/DCA,CPU cache親和的結合,假設連DMA都不支持。那他媽的還優化個毛]

理論上一定要做基於傳輸層以及傳輸層下面的元組做hash。不能隨機分派,在計算hash的時候也不能引入不論什么每包可變的字段。

由於某些高層協議比方TCP以及絕大多數的基於非TCP的應用協議是高度按序的,中間節點的全然基於數據包處理的並行化會引起數據包在端節點的亂序到達。從而引發重組和重傳開銷。自己為了提高線速能力貌似爽了一把,卻給端主機帶來了麻煩。然而我眼下沒有考慮這個。我僅僅是基於輪轉調度的方式來分發poll過程到不同的CPU來處理。這顯然會導致上述的亂序問題。



       若想完美解決上述問題,須要添加一個調度層,將RX softirq再次分為上下兩半部RX softirq1和RX softirq2。上半部RX softirq1僅僅是不斷取出skb並分派到特定CPU核心,下半部才是協議棧處理,改動NAPI的poll邏輯。每當poll出來一個skb,就計算這個skb的hash值,然后將其再次分派到特定的CPU的隊列中,最后喚醒有skb須要處理的CPU上的RX softirq2。這期間須要引入一個位圖來記錄有無情況。

       可是有一個須要權衡的邏輯,是不是真的值得將RX softirq做再次切割,將其分為上下半部,這期間的調度開銷和切換開銷究竟是多少。須要基准測試來評估。

*)延長net softirq的運行時間。有包就一直dispatch loop。管理/控制平面進程被划分到獨立的cgroup/cpuset中。

5.Linux調度器相關的改動

這個優化涉及到方案的完備性,畢竟我們不能保證CPU核心的數量一定大於網卡數量的2倍(輸入處理和輸出處理分離)。那么就必須考慮輸出事件的調度問題。
依照數據包隊列管理的設計方案,考慮單CPU核心,假設有多塊網卡的輸出位圖中有bit被置位,那么究竟調度哪一個網卡進行輸出呢?這是一個明白的task調度問題。你放心把這個工作交給Linux內核的調度器去做嗎?我不會。



       由於我知道,盡管有好幾個網卡可能都有數據包等待發送。可是它們的任務量並不同,這又是一個二分圖問題。我須要三個指標加權來權衡讓哪個網卡先發送,這三個指標是,隊頭等待時間。隊列數據包長度總和以及數據包數量。

由此能夠算出一個優先級prio。總的來講就是虛擬輸出隊列中等待越久,數據包越多。長度越長的那個網卡最值得發送數據。

計算隊列總長勢必會引發非局部訪問。比方訪問其他網卡的虛擬輸出隊列,這就會引發鎖的問題,因此考慮簡單情形,僅僅使用一個指標,即數據包長度。在Linux當前的CFS調度器情形下,須要將排隊虛擬輸出隊列的數據包長度與task的虛擬時間,即vruntime關聯。詳細來講就是,在輸入網卡對輸出網卡的輸出位圖置位的時候,有下列序列:

//僅僅要有skb排隊,無條件setbit
setbit(outcart, incard);
//僅僅要有skb排隊,則將與輸出網卡關聯的輸出線程的虛擬時間減去一個值。該值由數據包長度與常量歸一化計算所得。

outcard_tx_task_dec_vruntime(outcard, skb->len);


對Linux CFS調度不熟悉的,能夠自行google。其實,一旦某個輸出網卡的輸出task開始運行。它也是依照這樣的基於虛擬時間流逝的CFS方式來調度數據包的,即摘下一個最值得發送的數據包隊列描寫敘述符放入TX ring。

       最后有一個思考,假設不採用CFS而採用RT調度類是不是更好?單獨網卡輸出的實時性和多塊網卡輸出之間的公平性怎樣權衡?另外,採用RT調度類就一定帶有實時性嗎?

6.內置包分類和包過濾的情況

這是一個關於Netfilter優化的話題。Netfilter的性能一直被人詬病,其非常大程度上都是由於iptables造成的。一定要區分對待Netfilter和iptables。當然排除了這個誤會,並不表明Netfilter就全然無罪。Netfilter是一個框架,它本身在我們已經關閉了搶占的情況下是沒有鎖開銷的,關鍵的在它內部的HOOK遍歷運行中,作為一些callback,內部的邏輯是不受控制的,像iptables。conntrack(本來數據結構粒度就非常粗-同一張hash存儲兩個方向的元組,又使用大量的大粒度鎖,內存不緊張時我們能夠多用幾把鎖,空間換自由,再說。一把鎖能占多大空間啊)。都是吃性能的大戶,而nf-HiPac就好非常多。因此這部分的優化不好說。



       只是建議還是有的,為一段臨界區加鎖的時候千萬不要盲目,假設一個數據結構被讀的頻率比被寫的頻率高非常多,以至於后者能夠被忽略的地步。那么勸各位不要鎖定它,即使RW鎖,RCU鎖都不要,而是採用復制的形式。拷貝出一個副本。然后讀副本,寫原本,寫入原本后採用原子事件的方式通知副本失效。比方上面提到的關於高速流表的同步問題,一旦路由發生變化。就觸發一個原子事件。查詢高速流表中與之相關的項,失效掉它。查詢能夠非常慢。由於路由更新的頻率非常低。

本節不多談,建議例如以下:
*)預處理ACL或者NAT的ruleset(採用nf-hipac方案替換非預處理的逐條匹配)
[Hipac算法相似於一種針對規則的預處理,將matches進行了拆分,採用多維區間匹配算法
]
*)包調度算法(CFS隊列,RB樹存儲包到達時間*h(x)。h為包長的函數)

7.作為容器的skb

skb作為一個數據包的容器存在,它要和真正的數據包區分開來,其實。它僅僅作為一個數據包的載體。像一輛卡車運載數據包。

它是不應該被釋放的。永遠不該被釋放。每一塊網卡都應該擁有自己的卡車車隊。假設我們把網卡看作是航空港,Linux路由器看作是陸地,那么卡車從空港裝載貨物(數據包),要么把它運輸到某個目的地(Linux作為server),要么把它運輸到還有一個空港(Linux作為轉發路由器),其間這輛卡車運送個來回就可以。這輛卡車一直屬於貨物到達的那個空港,將貨物運到還有一個空港后空車返回就可以。卡車的使用不必中心調度。更無需用完后銷毀,用的時候再造一輛(Linux的轉發瓶頸即在此。。!

)。



       其實還有更加高效的做法,那就是卡車將貨物運輸到還有一個港口或者運輸到陸地目的地后,不必空車返回,而是直接排入目的港口或者目的地的出港隊列。等待運輸貨物滿載返回所屬的港口。可是對於Linux而言,由於須要路由查找后才知道卡車返回哪里。因此在出發的時候,卡車並不能確定它一定會返回它所屬的港口...因此須要對包管理隊列做一定的修正,即解除網卡的RX ring和skb的永久綁定關系。為了統一起見,新的設計將路由到本機的數據包也作為轉發處理,僅僅是輸出網卡變成了一個BSD socket,新的設計例如以下圖所看到的:



其實,類比火車和出租車我們就能看到這個差別。對於火車而言,它的線路是固定的。比方哈爾濱到漢口的火車,它屬於哈爾濱鐵路局,滿客到達漢口后,下客,然后漢口空車又一次上客,它一定返回哈爾濱。

然而對於出租車。就不是這樣。嘉定的滬C牌的出租車理論上屬於嘉定,不拒載情況下,一個人打車到松江,司機到松江后,盡管期待有人打他的車回嘉定。可是乘客上車后(路由查找),告訴司機,他要到閔行,到達后。又一人上車,說要到嘉興...越走越遠。但事實就是這樣。由於乘客上車前。司機是不能確定他要去哪里的。



用戶態協議棧方案

1.爭議

在某些平台上,假設不解決user/kernel切換時的cache,tlb刷新開銷。這樣的方案並非我主推的,這些平台上無論是寫直通還是寫回,訪問cache都是不經MMU的,也不cache mmu權限。且cache直接使用虛地址。

2.爭議解決方式

能夠採用Intel I/OAT的DCA技術,避免上下文切換導致的cache抖動

3.採用PF_RING的方式

改動驅動,直接與DMA buffer ring關聯(參見內核方案的DMA優化)。

4.借鑒Tilera的RISC超多核心方案

並行流水線處理每一層,流水級數為同一時候處理的包的數量,CPU核心數+2,流水數量為處理模塊的數量。
[流水線倒立]

本質上來講,用戶態協議棧和內核協議棧的方案是雷同的。無外乎還是那幾種思想。用戶態協議棧實現起來限制更少。更靈活,同一時候也更穩定。可是並非一味的都是優點。須要注意的是。大量存在的爭議都是形而上的,仁者見仁,智者見智。



穩定性

對於非專業非大型路由器,穩定性問題能夠不考慮。由於無需7*24,故障了大不了重新啟動一下而已,無傷大雅。可是就技術而言,還是有幾點要說的。

在高速總線情形下。並行總線easy竄擾。內存也easy故障,一個位的錯誤,一個電平的不穩定都會引發不可預知的后果。所以PCI-E這樣的高速總線都採用串行的方式數據傳輸,對於硬盤而言。SATA也是一樣的道理。

       在多網卡DMA情況下,對於通過的基於PCI-E的設備而言,總線上的群毆是非常激烈的,這是總線這樣的拓撲結構所決定的,和總線類型無關。再考慮到系統總線和多CPU核心,這樣的群毆會更加激烈。由於CPU們也會參與進來。群毆的時候,打翻桌椅而不是扳倒對方是非經常有的事,僅僅要考慮一下這樣的情況,我就想為三年前我與客戶的一次爭吵而向他道歉。

       2012年,我做一個VPN項目,客戶說我的設備可能下一秒就會宕機,由於不確定性。我說if(true) {printf("cao ni ma!\n")(當然當時我不敢這么說);確定會運行嗎?他說不一定。我就上火了...可是如今看來,他是對的。



VOQ設計后良好的副作用-QoS

VOQ是本方案的一個亮點,本文差點兒是環繞VOQ展開的,關於還有一個亮點DxR Pro,在其他的文章中已經有所闡述。本文僅僅是加以引用而已。臨近末了,我再次提到了VOQ,這次是從一個宏觀的角度,而不是細節的角度來加以說明。
      
       僅僅要輸入緩沖區隊列足夠大,數據包接收差點兒就是線速的。然而對於輸出,卻受到了調度算法。隊頭擁塞等問題的影響,即輸入對於系統來講是被動的。中斷觸發的,而輸出對於系統來講則是主動的。受制於系統的設計。因此對於轉發而言“收易發難”就是一個真理。因此對於QoS的位置,大多數系統都選擇在了輸出隊列上,由於輸入隊列上即便對流量進行了干預。流量在輸出的時候還是會受到二次無辜的干預,而這會影響輸入隊列上的QoS干預效果。我記得以前研究過Linux上的IMQ輸入隊列流控,當時僅僅是關注了實現細節,並沒有進行形而上的思考,如今不了。

       有了VOQ以后,配合設計良好的調度算法,差點兒攻克了全部問題。這是令人興奮的。上文中我提到輸出操作的時候。輸出線程採用基於數據包長度以及虛擬時間的加權公平調度算法進行輸出調度。可是這個算法的效果僅僅是全速發送數據包。

假設這個調度算法策略化,做成一個可插拔的,或者說把Linux的TC模塊中的框架和算法移植進來,是不是會更好呢?

       唉。假設你百度“路由器 線速”,它搜出來的差點兒都是“路由器 限速”。這真是一個玩笑。其實對於轉發而言,你根本不用加入不論什么TC規則就能達到限速的效果。Linux盒子在網上上一串,立即就被自己主動限速了。難道不是這樣嗎?而加上VOQ以后,你確實須要限速了。就像在擁擠的中國城市中區,主干道上寫着限速60。這不是開玩笑嗎?哪個市中心的熙熙攘攘的街道能跑到60....可是一旦上了高速,限速100/120,就是必須的了。

VOQ設計后良好的副作用-隊頭擁塞以及加速比問題


用硬件路由器的術語,假設採用將數據包路由后排隊到輸出網卡隊列的方案,那么就會有多塊網卡同一時候往一塊網卡排隊數據包的情況,這對於輸出網卡而言是被動的,這又是一個令人悲傷的群毆過程。為了讓多個包都能同一時候到達。輸出帶寬一定要是各個輸入帶寬的加和,這就是N倍加速問題,我們希望的是一個輸出網卡主動對數據包進行調度的過程,有序必定高效。這就是VOQ設計的精髓。

       對於Linux而言。由於它的輸出隊列是軟件的。因此N加速比問題變成了隊列鎖定問題,總之,還是一個令人遺憾的群毆過程,因此應對方案的思想是一致的。

因此在Linux中我就模擬了一個VOQ。從這里我們能夠看出VOQ和輸出排隊的差別,VOQ對於輸出過程而言是主動調度的過程,顯然更加高效,而輸出排隊對於輸出過程而言則是一個被動被爭搶的過程,顯然這是令人感到無望的。



       須要說明的是。VOQ僅僅是一個邏輯上的概念,類比了硬件路由器的概念。假設依舊堅持使用輸出排隊而不是VOQ。那么設計多個輸出隊列,每個網卡一個隊列也是合理的,它甚至更加簡化,壓縮掉了一個網卡分派過程,簡化了調度。於是我們得到了Linux VOQ設計的第三版:將虛擬輸出隊列VOQ關聯到輸出網卡而不是輸入網卡(下面一小節我將分析原因)。

總線拓撲和Crossbar


真正的硬件路由器。比方Cisco,華為的設備,路由轉發全由線卡硬件運行,數據包在此期間是精巧在那里的,查詢轉發表的速度是如此之快,以至於相對將數據包挪到輸出網卡隊列的開銷,查表開銷能夠忽略。因此在真正的硬件路由器上。怎樣構建一個高性能交換網絡就是重中之重。


   
       不但如此。硬件路由器還須要考慮的是,數據包在路由查詢過后是由輸入處理邏輯直接通過交換網絡PUSH到輸出網卡隊列呢,還是原地不動,然后等待輸出邏輯通過交換網絡把數據包PULL到那里。這個不同會影響到交換網絡仲裁器的設計。

假設有多個網卡同一時候往一個網卡輸出數據包,PUSH方式可能會產生沖突。由於在Crossbar的一條路徑上,它相當於一條總線,並且沖突通常會發生在交換網絡內部,因此這樣的PUSH的情況下,通常會在交換網絡內部的開關節點上攜帶cache,用來暫存沖突仲裁失敗的數據包。

反之,假設是PULL方式,情況就有所不同。

因此把輸出隊列放在交換網絡的哪一側帶來的效果是不同的。



       可是對於通用系統架構,一般都是採用PCI-E總線連接各個網卡。這是一種典型的總線結構,根本就沒有所謂的交換網絡。

因此所謂的仲裁就是總線仲裁,這並非我關注的重點。誰讓我手上僅僅有一個通用架構的設備呢?!我的優化不包含總線仲裁器的設計。由於我不懂這個。

       因此。對於通用架構總線拓撲的Linux協議棧轉發優化而言。虛擬輸出隊列VOQ關聯在輸入網卡還是輸出網卡,影響不會太大。可是考慮到連續內存訪問帶來的局部性優化,我還是傾向將VOQ關聯到輸出網卡。假設VOQ關聯到輸入網卡,那么在進行輸出調度的時候,輸出網卡的輸出線程就要從輸出位圖指示的每個待發送數據的輸入網卡VOQ中與自己關聯的隊列調度數據包,無疑,這些隊列在內存中是不連續的,假設關聯到輸出網卡。對於每個輸出網卡而言,VOQ是連續的。例如以下圖所看到的:




實現相關


前面我們提到skb僅僅是作為容器(卡車)存在。

因此skb是不必釋放的。理想情況下。在Linux內核啟動。網絡協議棧初始化的時候,依據自身的硬件性能和網卡參數做一次自測,然后分配MAX個skb,這些skb能夠先均勻分配到各個網卡,同一時候預留一個socket skb池,供用戶socket取。后面的事情就是skb運輸行為了。卡車開到哪里算哪里,運輸過程不空載。

       可能你會覺得這個沒有必要,由於skb本身甚至整個Linux內核中絕大部分內存分配都是被預先分配並cache的,slab就是做這個的。不是有kmem_cache機制嗎?是這樣的,我承認Linux內核在這方面做得非常不錯。可是kmem_cache是一個通用的框架,為何不針對skb再提高一個層次呢?每一次調用alloc_skb,都會觸發到kmem_cache框架內的管理機制做非常多工作。更新數據結構,維護鏈表等,甚至可能會觸及到更加底層的伙伴系統。因此。期待直接使用高效的kmem_cache並非一個好的主意。

       你可能會反駁說萬一系統內存吃緊也不釋放嗎?萬事並不絕對,但這並非本文的范疇,這涉及到非常多方面,比方cgroup等。

       針對skb的改動,我加入了一個字段。指示它的所屬地(某個網卡?socket池?...),當前所屬地,這些信息能夠維護skb不會被free到kmem_cache,同一時候也能夠最優化cache利用率。這部分改動已經實現,眼下正在針對Intel千兆卡的驅動做進一步改動。關於DxR Pro的性能。我在用戶態已經經過測試,眼下還沒有移植到內核。

       關於高速查找表的實現,眼下的思路是優化nf_conntrack,做多級hash查找。

最后的聲明

本文僅僅是針對Linux做的轉發調優方案,假設須要更加優化的方案, 請參考ASIC以及NP等硬件方案,不要使用總線拓撲,而是使用交叉陣列拓撲。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM