架構決定可擴充套件性--聊聊使用者態協議棧的意義
在進入這個話題之前先說說通用和專業之間的區別。
舉個很好的例子,好比我們個人,絕大部分的人都是“通用”的,而只有極少部分的人是“專業”的。通用的人主要目標是活下去,即在最壞的條件下如何活下去,而專業的人目標在於在特定領域內將能量發揮到極致,這時考慮的是最好的條件,簡單點說,通用的人什麼都做,衣食住行必須一樣不落下,而專業的人只需要做好一件事,其它的事他未必搞得定。
本來就是閒聊,所以也就沒有列什麼提綱,這裡再來說說TCP擁塞控制。已經跑了30多年的基於AIMD的Reno及其變體CUBIC被證明是優秀的,很多人搞不清楚為什麼它是優秀的,難道BBR不是更好嗎?不!Reno/CUBIC之優秀說的是,即使在最壞的網路環境中也能保證擁塞控制起作用,維持公平性,這背後的哲學是,資源匱乏時,不患寡而患不均,因為冪律無法維持能量總量的穩定性,而在資源充盈時,則放開讓冪律起作用,所謂讓一部分人先富起來。這在數學上,AIMD的收斂模型可以證明這個問題,這裡就不再展開,展開的話就會涉及到控制論的問題了。
那麼Reno/CUBIC作為一個通用的擁塞控制演算法當然是絕佳的了,至於說BBR,姑且認為它的1.0版本是B4網路上專業的擁塞控制演算法吧,然而直到目前,也沒能從數學上證明BBR在最壞的網路環境下能維持公平,甚至都不能證明它和其它演算法配合得有多好,所以說雖然在效能上BBR可以說在絕大多數場景下表現不錯,但由於其沒有通用性的特徵,故而CUBIC還是有一定市場。
剛剛扯到了人和TCP,現在來看看作業系統。這裡主要來看看巨集核心的Linux。首先Linux就是一個通用的作業系統,它保證的是,即使CPU再垃圾,記憶體再少,它也能做一個名副其實的多工現代作業系統跑多個程序。所謂的現代作業系統其含義包含了兩種善意的欺騙,在空間維度上,作業系統的虛擬地址空間機制讓每一個程序都認為自己獨佔了記憶體,在時間維度上,作業系統的時間片排程機制讓每一個程序都認為自己獨享了CPU時間,本質上,Linux把這兩件事做好就OK了,至於說別的,像I/O啦,像TCP/IP啦,這些都是程序的事情。
但是貌似Linux核心代理了這些本不該由它管的事情,比如磁碟I/O是核心處理的,檔案系統也是核心的一個子系統,TCP/IP協議棧就別說,也是核心的一個子系統,這是為什麼?
答案在於外圍硬體與CPU架構之間的不相容,所以必須由作業系統核心來提供軟體相容層適配二者,此話怎講?前面我提到,現代作業系統在時間和空間維度上提供了兩類假象,作為一個可用的系統,除了CPU,記憶體以及作業系統之外,外設是必不可少的,不然如何接收輸入和寄送輸出,問題就在這裡,輸入和輸出外設並無法提供上述兩類假象,以磁碟為例,它永遠就是那麼個磁碟,程序1為它設定了一個status,然後系統切換程序2執行,程序2看到的磁碟狀態就是程序1剛剛給它設定的,程序之間互相闖地方這肯定會亂套,並且違背了現代作業系統提供的隔離虛擬機器模型,因此必須由作業系統出面協調幹涉。這最終形成了核心檔案系統
無論如何,現實就是這個樣子,幾十年來,這種機制工作的非常好,作為通用作業系統,最關鍵的是這種機制在以往條件艱苦惡劣的情況依然可以發揮作用。不過正如窮慣了的人就算髮財也依然會存錢而不是投資一樣,這種機制在當前高效能伺服器設計領域,會不會已經成為阻礙可擴充套件性阻礙效能的掣肘之制呢?如果是,有沒有什麼辦法可以改變這個現實。
解決方案呼之即來,即不能把單獨任務包裝成獨立的作業系統程序任其去排程,而應該讓獨立的程序主動去處理每一個任務。
我們來看一個案例,即Nginx之於Apache。
我們知道Apache的prefork這種mpm,其它的也差不多。這是典型的甩鍋給作業系統的行為。對於實現者,只需要去實現一個程序,然後處理單個HTTP Request即可,收到一個新連線就跑這麼一個程序,同時處理兩個連線就跑兩個這樣的程序,至於別的,管它呢,讓排程器去管理吧,如果說一個連線的優先順序大於另一個連線,Apache的做法顯然是為高優先順序連線的處理程序設定一個高的排程優先順序,依然是交給排程器去處理…這種方案顯然是不可擴充套件的,因為Apache的可擴充套件性受制於排程器的可擴充套件性。
再來看另一個案例,即TCP/IP協議棧。
看看現狀是什麼。剛才說了,作業系統本不該實現TCP/IP協議棧的,只是不得已而代理實現,現在我可以再進一步說這事,不得已確實不得已,在硬體網絡卡層面確實需要作業系統來代理,但是難道不是把資料包扔到程序隔離的記憶體(BufferRing?嗯,是的!)中就OK了嗎,接下來的事情就是資料包的協議棧處理了,這完全可以交給使用者態程序啊。這個問法問Apache的時候,就有了Nginx,我們試著問下Apache:難道不是把Connection放到一個佇列裡就OK了嗎?接下來的事情就是讓一個程序去處理這個佇列,幹嘛還要搞那麼多個程序…這意味著對於TCP/IP協議棧,有著某種使用者態的解決方案。確實,不賣關子,netmap,DPDK這些都是。
我們來看下作業系統核心實現的TCP/IP協議棧為什麼不好。微觀上說,Linux核心協議棧擴充套件性不好,多核處理資料包特別是小包時pps/核數的曲線上凸跌落,大致阻礙線性擴充套件的因素無非中斷,鎖,軟中斷喚醒使用者程序/切換,以及這些導致的Cache汙染。但這些說再多也都是各個點,即便你各個擊破了也未必能有什麼質的飛躍。如果我們把這種傳統實現的核心態TCP/IP協議棧看作是古老的方法,那麼用古老的方法去處理當代的高效能高擴充套件時尚的話,實際上已經成一場高潮迭起的雜技表演,不管是網絡卡廠商,還是Linux社群,均為這場雜技表演增加了很多看點,隨便舉幾個例子:
- Intel網絡卡RSS
- Intel網絡卡Interrupt Mode
- Intel網絡卡Interrupt Delay
- Intel網絡卡DCA
- 網絡卡各種Offload
- Linux NAPI
- Linux Busy Polling
- Linux RPS/RFS
- Linux中斷執行緒化
- …
有了這些,說實話Linux核心協議棧已經工作地相當不錯,但這些都屬於技藝展示。肖像畫家失業是因為照相機出現了,所以在新的東西面前,技藝是不堪一擊的,如果沒有照相機,也就不會出現立體主義,便不會有格爾尼卡這種作品。所以說,當這些優化技巧,優化元件越來越多的時候,應該從架構上顛覆舊方法的時刻就不遠了。
那麼核心協議棧的問題到底在哪裡?很簡單,和Apache的問題一樣,只是不在一個層次而已。那就是中斷!歸根結底核心協議棧對資料包的處理還是依賴了作業系統的排程器。
一次中斷來到,協議棧收包軟中斷便可能在任意上下文執行,我們巨集觀上講,中斷本身就是一次任務切換,即便是使能了單獨的中斷stack,即便是中斷執行緒化,即便收包軟中斷全部在softirqd上下文執行,所有這一切都免不了一次任務切換,我們知道切換意味著什麼,我也也知道在softirq的最上層,還會觸發另一次排程,即wakeup使用者態的處理程序,總而言之,涉及到資料包處理時,作業系統核心的排程子系統完全投入,最終的目標無非就是為了處理一個個單獨的資料包!程序切換的代價是高昂的,現場儲存,負載均衡,cache汙染…由於中斷的到來是不可控的,因此由這個中斷所引發的一系列排程和切換就是不可控的,之所以敢這麼折騰排程器,就是因為現代作業系統核心在設計上是閉環的,你怎麼折騰它也是金剛不壞,它確實不會崩潰,它能工作,但也僅此而已。再次重申,通用作業系統大部分時間工作在惡劣環境下而不崩潰的just fine狀態,而不是工作在精益求精的滿血狀態。
中斷的問題在於,實時優先於吞吐,想要大吞吐,必然要輪詢。我前面的文章寫過,對於個人實時比吞吐重要,只有商人才會在乎吞吐,但又能如何,我們服務的就是商人,畢竟伺服器是要賣錢獲取收益的。
現在讓我們看看正確的做法是什麼。
不管是mtcp還是騰訊的F-Stack這種使用者態協議棧實現,都代表了一種新勢力,它們均是某種完全可用的協議棧實現,並且也都是開源的。但我想表達的不是為這些概念或者說產品做宣傳,我想表達的是它們下層的東西,即一種新的架構。這種架構在概念上非常簡單。以下步驟即可:
- 網絡卡的BufferRing被mmap到使用者態程序;
- 網絡卡只管把資料包放入一個BufferRing,操作寫標;
- 使用者態程序迴圈讀取BufferRing,操作讀標;
- 使用者態程序處理讀取到的每一個packet。
實際的實現可能會有比較複雜,但大致如此。我們發現這是似曾相識的,沒錯,Nginx就是這樣的架構,只是多了一個epoll的通知。看看Nginx如何在併發連線數上秒掉Apache,就知道使用者態協議棧如何在pps上秒掉核心協議棧了。總之,兩種情況下,罪魁禍首都是排程,而解決方案均是不能把單獨任務包裝成獨立的作業系統程序任其去排程,而應該讓獨立的程序主動去處理每一個任務。
要去買小龍蝦了,最後,簡單說一下DPDK。
嗯,很多人都是DPDK粉,所以不能得罪。但DPDK畢竟是一個產品而不是一個作品,相對而言,我更加喜歡netmap,我自己玩的mtcp就是使能了netmap而不是DPDK。
DPDK的問題在於,為了把效能發揮到極致,採用了一種走火入魔的方法,這並不是一個大廠的風範。它會把處理程序和CPU做強繫結,然後Busy polling,這個CPU基本做不了別的什麼事,這是一種粗狂型的優化方案,此外,大部分時候你之所以看到DPDK比netmap表現好,請不要忽略Intel自家的DCA,即直接快取訪問。DCA猛的很吶,你要是知道Cache miss的代價基本也就知道DCA的收益了。我們知道協議棧處理中最頻繁的處理就是包頭的處理,而我們知道,經過精心的設計,一直到TCP層,包頭基本不佔什麼空間,因此DCA便可以通過旁路把包頭提前送入Cache,這樣在CPU處理時,就會Cache hit!厲害吧,軟硬結合!
無關的內容,想到了,就說下。
- Reno:並行連線數量無關,衝突就腰斬降窗
- CUBIC:RTT無關,衝突就縮放
- BBR:快取無關,…
Reno/CUBIC無限可擴充套件,BBR則不行。