高併發設計:如何設計千萬連接每秒的系統

1.前言

本文將討論單機服務器實現C10M(即單機千萬併發連接)的可能性及其思路。

截至目前,40gpbs、32-cores、256G RAM的X86服務器在Newegg網站上的報價是幾千美元。實際上以這樣的硬件配置來看,它完全可以處理1000萬個以上的併發連接,如果它們不能,那是因爲你選擇了錯誤的軟件,而不是底層硬件的問題。

可以預見在接下來的10年裏,因爲IPv6協議下每個服務器的潛在連接數都是數以百萬級的,單機服務器處理數百萬的併發連接(甚至千萬)並非不可能,但我們需要重新審視目前主流OS針對網絡編程這一塊的具體技術實現。

2.解決C10M問題並非不可能

高性能網絡編程(三):下一個10年,是時候考慮C10M併發問題了。很多人會想當然的認爲,要實現C10M(即單機千萬)併發連接和處理能力,是不可能的。不過事實並非如此,現在系統已經在用你可能不熟悉甚至激進的方式支持千萬級別的併發連接。

要知道它是如何做到的,我們首先要了解Errata Security的CEO Robert Graham,以及他在Shmoocon 2013大會上的“天方夜談”視頻記錄: C10M Defending The Internet At Scale

Robert用一種我以前從未聽說的方式來很巧妙地解釋了這個問題。他首先介紹了一點有關Unix的歷史,Unix的設計初衷並不是一般的服務器操作系統,而是電話網絡的控制系統。由於是實際傳送數據的電話網絡,所以在控制層和數據層之間有明確的界限。問題是我們現在根本不應該使用Unix服務器作爲數據層的一部分。正如設計只運行一個應用程序的服務器內核,肯定和設計多用戶的服務器內核是不同的。

Robert Graham的結論是:OS的內核不是解決C10M問題的辦法,恰恰相反OS的內核正是導致C10M問題的關鍵所在。

這也就意味着:不要讓OS內核執行所有繁重的任務:將數據包處理、內存管理、處理器調度等任務從內核轉移到應用程序高效地完成,讓諸如Linux這樣的OS只處理控制層,數據層完全交給應用程序來處理。

最終就是要設計這樣一個系統,該系統可以處理千萬級別的併發連接,它在200個時鐘週期內處理數據包,在14萬個時鐘週期內處理應用程序邏輯。由於一次主存儲器訪問就要花費300個時鐘週期,所以這是最大限度的減少代碼和緩存丟失的關鍵。

面向數據層的系統可以每秒處理1千萬個數據包,面向控制層的系統,每秒只能處理1百萬個數據包。這似乎很極端,請記住一句老話:可擴展性是專業化的,爲了做好一些事情,你不能把性能問題外包給操作系統來解決,你必須自己做。

3.回顧一下C10K問題

10年前,開發人員處理C10K可擴展性問題時,儘量避免服務器處理超過1萬個的併發連接。通過改進操作系統內核以及用事件驅動服務器(典型技術實現如:Nginx和Node)代替線程服務器(典型代表:Apache),使得這個問題已經被解決。人們用十年的時間從Apache轉移到可擴展服務器,在近幾年,可擴展服務器的採用率增長得更快了。

以傳統網絡編程模型作爲代表的Apache爲例,我們來看看它在C10K問題上的侷限表現在哪些方面,並針對性的討論對應的解決方法。

Apache的問題在於服務器的性能會隨着連接數的增多而變差,實際上性能和可擴展性並不是一回事。當人們談論規模時,他們往往是在談論性能,但是規模和性能是不同的,比如Apache。持續幾秒的短期連接:比如快速事務,如果每秒處理1000個事務,只能有約1000個併發連接到服務器。如果事務延長到10秒,要維持每秒1000個事務則必須打開1萬個併發連接。這種情況下:儘管你不顧DoS攻擊,Apache也會性能陡降,同時大量的下載操作也會使Apache崩潰。

如果每秒處理的連接從5千增加到1萬,你會怎麼做?比方說,你升級硬件並且提高處理器速度到原來的2倍。到底發生了什麼?你得到兩倍的性能,但你沒有得到兩倍的處理規模。每秒處理的連接可能只達到了6000。你繼續提高速度,情況也沒有改善。甚至16倍的性能時,仍然不能處理1萬個併發連接。所以說性能和可擴展性是不一樣的。

問題在於Apache會創建一個CGI進程,然後關閉,這個步驟並沒有擴展。爲什麼呢?內核使用的O(N^2)算法使服務器無法處理1萬個併發連接。

OS內核中的兩個基本問題:

連接數=線程數/進程數:當一個數據包進來,內核會遍歷其所有進程以決定由哪個進程來處理這個數據包。

連接數=選擇數/輪詢次數(單線程):同樣的可擴展性問題,每個包都要走一遭列表上所有的socket。

通過上述針對Apache所表現出的問題,實際上徹底解決併發性能問題的解決方法的根本就是改進OS內核使其在常數時間內查找,使線程切換時間與線程數量無關,使用一個新的可擴展epoll()/IOCompletionPort常數時間去做socket查詢。

因爲線程調度並沒有得到擴展,所以服務器大規模對socket使用epoll方法,這樣就導致需要使用異步編程模式,而這些編程模式正是Nginx和Node類型服務器具有的。所以當從Apache遷移到Nginx和Node類型服務器時,即使在一個配置較低的服務器上增加連接數,性能也不會突降。所以在處理C10K連接時,一臺筆記本電腦的速度甚至超過了16核的服務器。這也是前一個10年解決C10K問題的普遍方法。

4. 實現C10M意味着什麼?

實現10M(即1千萬)的併發連接挑戰意味着什麼:

  • 1千萬的併發連接數
  • 100萬個連接/秒:每個連接以這個速率持續約10秒;
  • 10GB/秒的連接:快速連接到互聯網;
  • 1千萬個數據包/秒:據估計目前的服務器每秒處理50K數據包,以後會更多;
  • 10微秒的延遲:可擴展服務器也許可以處理這個規模(但延遲可能會飆升);
  • 10微秒的抖動:限制最大延遲;
  • 併發10核技術:軟件應支持更多核的服務器(通常情況下,軟件能輕鬆擴展到四核,服務器可以擴展到更多核,因此需要重寫軟件,以支持更多核的服務器)。

5. 爲什麼說實現C10M的挑戰不在硬件而在軟件?

硬件不是10M問題的性能瓶頸所在處,真正的問題出在軟件上,尤其是*nux操作系統。理由如下面這幾點:

首先:最初的設計是讓Unix成爲一個電話網絡的控制系統,而不是成爲一個服務器操作系統。對於控制系統而言,針對的主要目標是用戶和任務,而並沒有針對作爲協助功能的數據處理做特別設計,也就是既沒有所謂的快速路徑、慢速路徑,也沒有各種數據服務處理的優先級差別。

其次:傳統的CPU,因爲只有一個核,操作系統代碼以多線程或多任務的形式來提升整體性能。而現在,4核、8核、32核、64核和100核,都已經是真實存在的CPU芯片,如何提高多核的性能可擴展性,是一個必須面對的問題。比如讓同一任務分割在多個核心上執行,以避免CPU的空閒浪費,當然,這裏面要解決的技術點有任務分割、任務同步和異步等。

再次:核心緩存大小與內存速度是一個關鍵問題。現在,內存已經變得非常的便宜,隨便一臺普通的筆記本電腦,內存至少也就是4G以上,高端服務器的內存上24G那是相當的平常。但是,內存的訪問速度仍然很慢,CPU訪問一次內存需要約60~100納秒,相比很久以前的內存訪問速度,這基本沒有增長多少。對於在一個帶有1GHZ主頻CPU的電腦硬件裏,如果要實現10M性能,那麼平均每一個包只有100納秒,如果存在兩次CPU訪問內存,那麼10M性能就達不到了。核心緩存,也就是CPU L1/L2/LL Cache,雖然訪問速度會快些,但大小仍然不夠,我之前接觸到的高端至強,LLC容量大小貌似也就是12M。

解決這些問題的關鍵在於如何將功能邏輯做好恰當的劃分,比如專門負責控制邏輯的控制面和專門負責數據邏輯的數據面。數據面專門負責數據的處理,屬於資源消耗的主要因素,壓力巨大,而相比如此,控制面只負責一些偶爾纔有非業務邏輯,比如與外部用戶的交互、信息的統計等等。我之前接觸過幾種網絡數據處理框架,比如Intel的DPDK、6wind、windriver,它們都針對Linux系統做了特別的補充設計,增加了數據面、快速路徑等等特性,其性能的提升自然是相當巨大。

看一下這些高性能框架的共同特點:

  • 數據包直接傳遞到業務邏輯

而不是經過Linux內核協議棧。這是很明顯的事情,因爲我們知道,Linux協議棧是複雜和繁瑣的,數據包經過它無非會導致性能的巨大下降,並且會佔用大量的內存資源,之前有同事測試過,Linux內核要吃掉2.5KB內存/socket。我研究過很長一段時間的DPDK源碼,其提供的82576和82599網卡驅動就直接運行在應用層,將接管網卡收到的數據包直接傳遞到應用層的業務邏輯裏進行處理,而無需經過Linux內核協議棧。當然,發往本服務器的非業務邏輯數據包還是要經過Linux內核協議棧的,比如用戶的SSH遠程登錄操作連接等。

  • 多線程的核間綁定

一個具有8核心的設備,一般會有1個控制面線程和7個或8個數據面線程,每一個線程綁定到一個處理核心(其中可能會存在一個控制面線程和一個數據面線程都綁定到同一個處理核心的情況)。這樣做的好處是最大化核心CACHE利用、實現無鎖設計、避免進程切換消耗等等。

  • 內存是另外一個核心要素

常見的內存池設計必須在這裏得以切實應用。有幾個考慮點,首先,可以在Linux系統啓動時把業務所需內存直接預留出來,脫離Linux內核的管理。其次,Linux一般採用4K每頁,而我們可以採用更大內存分頁,比如2M,這樣能在一定程度上減少地址轉換等的性能消耗。

內存的邏輯地址到物理地址的轉換的過程

(1)首先邏輯地址需要轉化爲線性地址。邏輯地址是以"段寄存器:偏移地址"形式存在的。通過段寄存器裏的索引,可以從段描述符表裏找到段的基址。 然後用基址加上段內的偏移量,就得到了對應的線性地址。(2)將線性地址再轉化爲物理地址。線性地址可以分爲三部分:頁目錄索引,頁表索引,和字節偏移索引。通過頁目錄索引和CR3寄存器指定的頁目錄基址之和,可以查詢到對應的頁表基址。再通過頁表索引和頁表基址之和, 可以得到對應的頁框地址,頁框地址再加上頁內字節偏移,就最終獲得了對應的物理地址。

關於Intel的DPDK框架介紹:

隨着網絡技術的不斷創新和市場的發展,越來越多的網絡設備基礎架構開始向基於通用處理器平臺的架構方向融合,期望用更低的成本和更短的產品開發週期來提供多樣的網絡單元和豐富的功能,如應用處理、控制處理、包處理、信號處理等。爲了適應這一新的產業趨勢, Intel推出了基於Intel x86架構DPDK (Data Plane Development Kit,數據平面開發套件) 實現了高效靈活的包處理解決方案。經過近6年的發展,DPDK已經發展成支持多種高性能網卡和多通用處理器平臺的開源軟件工具包。

有興趣的技術同行,也許可以參考下DPDK的源代碼,目前DPDK已經完全開源並且可以網絡下載了。

6. 解決C10M問題的思路總結

綜上所述,解決C10M問題的關鍵主要是從下面幾個方面入手:

網卡問題:通過內核工作效率不高

解決方案:使用自己的驅動程序並管理它們,使適配器遠離操作系統。

 

CPU問題:使用傳統的內核方法來協調你的應用程序是行不通的。

解決方案:Linux管理前兩個CPU,你的應用程序管理其餘的CPU,中斷只發生在你允許的CPU上。

 

內存問題:內存需要特別關注,以求高效。

解決方案:在系統啓動時就分配大部分內存給你管理的大內存頁。

 

以Linux爲例,解決的思咯就是將控制層交給Linux,應用程序管理數據。應用程序與內核之間沒有交互、沒有線程調度、沒有系統調用、沒有中斷,什麼都沒有。 然而,你有的是在Linux上運行的代碼,你可以正常調試,這不是某種怪異的硬件系統,需要特定的工程師。你需要定製的硬件在數據層提升性能,但是必須是在你熟悉的編程和開發環境上進行。

7. 來源

1 高性能網絡編程(三):下一個10年,是時候考慮C10M併發問題了

http://www.52im.net/thread-568-1-1.html 本文的原始來源

2 內存邏輯地址到物理地址轉化

http://blog.csdn.net/u012713968/article/details/50481748

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章