千萬級併發實現的祕密:內核不是解決方案,而是問題所在!

轉自:https://www.csdn.net/article/2013-05-16/2815317-The-Secret-to-10M-Concurrent-Connections

html

既然咱們已經解決了 C10K併發鏈接問題,應該如何提升水平支持千萬級併發鏈接?你可能會說不可能。不,如今系統已經在用你可能不熟悉甚至激進的方式支持千萬級別的併發鏈接。 

要知道它是如何作到的,咱們首先要了解Errata Security的CEO Robert Graham,以及他在Shmoocon 2013大會上的「無稽之談」—— C10M Defending The Internet At Scale。 

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

也就是他所說的——關鍵要理解內核不是解決辦法,內核是問題所在。 

算法

這意味着: 編程

  • 不要讓內核執行全部繁重的任務。將數據包處理,內存管理,處理器調度等任務從內核轉移到應用程序高效地完成。讓Linux只處理控制層,數據層徹底交給應用程序來處理。

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

面向數據層的系統能夠每秒處理1千萬個數據包,面向控制層的系統,每秒只能處理1百萬個數據包。服務器

這彷佛很極端,請記住一句老話:可擴展性是專業化的。爲了作好一些事情,你不能把性能問題外包給操做系統來解決,你必須本身作。 
如今,讓咱們學習Robert如何建立一個可以處理千萬級別併發鏈接的系統。 網絡

C10K問題——最近十年  

數據結構

十年前,工程師處理C10K可擴展性問題時,儘可能避免服務器處理超過1萬個的併發鏈接。經過改進操做系統內核以及用事件驅動服務器(如Nginx和Node)代替線程服務器(Apache),這個問題已經被解決。人們用十年的時間從Apache轉移到可擴展服務器,在近幾年,可擴展服務器的採用率增加得更快了。多線程

Apache的問題 架構

  • Apache的問題在於服務器的性能會隨着鏈接數的增多而變差
  • 關鍵點:性能和可擴展性並非一回事。當人們談論規模時,他們每每是在談論性能,可是規模和性能是不一樣的,好比Apache。
  • 持續幾秒的短時間鏈接,好比快速事務,若是每秒處理1000個事務,只有約1000個併發鏈接到服務器。
  • 事務延長到10秒,要維持每秒1000個事務,必須打開1萬個併發鏈接。這種狀況下:儘管你不顧DoS攻擊,Apache也會性能陡降;同時大量的下載操做也會使Apache崩潰。
  • 若是每秒處理的鏈接從5千增長到1萬,你會怎麼作?比方說,你升級硬件而且提升處理器速度到原來的2倍。發生了什麼?你獲得兩倍的性能,但你沒有獲得兩倍的處理規模。每秒處理的鏈接可能只達到了6000。你繼續提升速度,狀況也沒有改善。甚至16倍的性能時,仍然不能處理1萬個併發鏈接。因此說性能和可擴展性是不同的。
  • 問題在於Apache會建立一個CGI進程,而後關閉,這個步驟並無擴展。
  • 爲何呢?內核使用的O(N^2)算法使服務器沒法處理1萬個併發鏈接。
  • 內核中的兩個基本問題:
  • 鏈接數=線程數/進程數。當一個數據包進來,內核會遍歷其全部進程以決定由哪一個進程來處理這個數據包。
  • 鏈接數=選擇數/輪詢次數(單線程)。一樣的可擴展性問題,每一個包都要走一遭列表上全部的socket。
  • 解決方法:改進內核使其在常數時間內查找。
  • 使線程切換時間與線程數量無關。
  • 使用一個新的可擴展epoll()/IOCompletionPort常數時間去作socket查詢。
  • 由於線程調度並無獲得擴展,因此服務器大規模對socket使用epoll方法,這樣就致使須要使用異步編程模式,而這些編程模式正是Nginx和Node類型服務器具備的;因此當從Apache遷移到Nginx和Node類型服務器時,即便在一個配置較低的服務器上增長鏈接數,性能也不會突降;因此在10K鏈接時,一臺筆記本電腦的速度甚至超過了16核的服務器。

C10M問題——將來十年

不遠的未來,服務器將要處理數百萬的併發鏈接。IPv6協議下,每一個服務器的潛在鏈接數都是數以百萬級的,因此處理規模須要升級。

  • 如IDS / IPS這類應用程序須要支持這種規模,由於它們鏈接到一個服務器骨幹網。其餘例子:DNS根服務器,TOR節點,互聯網Nmap,視頻流,銀行,Carrier NAT,VoIP PBX,負載均衡器,網頁緩存,防火牆,電子郵件接收,垃圾郵件過濾。
  • 一般人們將互聯網規模問題歸根於應用程序而不是服務器,由於他們賣的是硬件+軟件。你買設備,並將其應用到你的數據中心。這些設備可能包含一塊Intel主板或網絡處理器以及用來加密和檢測數據包的專用芯片等。
  • 截至2013年2月,40gpbs, 32-cores, 256gigs RAM的X86服務器在Newegg網站上的報價是5000美圓。該服務器能夠處理1萬個以上的併發鏈接,若是它們不能,那是由於你選擇了錯誤的軟件,而不是底層硬件的問題。這個硬件能夠很容易地擴展到1千萬個併發鏈接。 

10M的併發鏈接挑戰意味着什麼: 

 

  1. 1千萬的併發鏈接數 
  2. 100萬個鏈接/秒——每一個鏈接以這個速率持續約10秒 
  3. 10GB/秒的鏈接——快速鏈接到互聯網。 
  4. 1千萬個數據包/秒——據估計目前的服務器每秒處理50K的數據包,之後會更多。過去服務器每秒能夠處理100K的中斷,而且每個數據包都產生中斷。 
  5. 10微秒的延遲——可擴展服務器也許能夠處理這個規模,但延遲可能會飆升。 
  6. 10微秒的抖動——限制最大延遲 
  7. 併發10核技術——軟件應支持更多核的服務器。一般狀況下,軟件能輕鬆擴展到四核。服務器能夠擴展到更多核,所以須要重寫軟件,以支持更多核的服務器。 

 

咱們所學的是Unix而不是網絡編程 

  • 不少程序員經過W. Richard Stevens所著的《Unix網絡編程》學習網絡編程技術。問題是,這本書是關於Unix的,而不僅是網絡編程。它告訴你,讓Unix作全部繁重的工做,你只須要在Unix的上層寫一個小服務器。但內核規模不夠,解決的辦法是儘量將業務移動到內核以外,而且本身處理全部繁重的業務。
  • 這方面有影響的一個例子是Apache每一個鏈接線程的模型。這意味着線程調度程序根據將要到來的數據肯定接下來調用哪個read()函數,也就是把線程調度系統看成數據包調度系統來用。(我真的很喜歡這一點,歷來沒有想過這樣的說法)。
  • Nginx宣稱,它不把線程調度看成數據包調度程序,而是本身進行數據包調度。使用select找到socket,咱們知道數據來了,就能夠當即讀取並處理數據,數據也不會堵塞。
  • 經驗:讓Unix處理網絡堆棧,但以後的業務由你來處理。

怎樣編寫規模較大的軟件?

如何改變你的軟件,使其規模化?許多隻提高硬件性能去支撐項目擴展的經驗都是錯誤的,咱們須要知道性能的實際狀況。 

要達到到更高的水平,須要解決的問題以下: 

 

  1. 數據包的可擴展性 
  2. 多核的可擴展性 
  3. 內存的可擴展性 

 

實現數據包可擴展——編寫本身的個性化驅動來繞過堆棧 

  • 數據包的問題是它們需經Unix內核的處理。網絡堆棧複雜緩慢,數據包最好直接到達應用程序,而非通過操做系統處理以後。
  • 作到這一點的方法是編寫本身的驅動程序。全部驅動程序將數據包直接發送到應用程序,而不是經過堆棧。你能夠找到這種驅動程序:PF_RING,NETMAP,Intel DPDK(數據層開發套件)。Intel不是開源的,但有不少相關的技術支持。
  • 速度有多快?Intel的基準是在一個至關輕量級的服務器上,每秒處理8000萬個數據包(每一個數據包200個時鐘週期)。這也是經過用戶模式。將數據包向上傳遞,使用用戶模式,處理完畢後再返回。Linux每秒處理的數據包個數不超過百萬個,將UDP數據包提升到用戶模式,再次出去。客戶驅動程序和Linux的性能比是80:1。
  • 對於每秒1000萬個數據包的目標,若是200個時鐘週期被用來獲取數據包,將留下1400個時鐘週期實現相似DNS / IDS的功能。
  • 經過PF_RING獲得的是原始數據包,因此你必須作你的TCP堆棧。人們所作的是用戶模式棧。Intel有現成的可擴展TCP堆棧

多核的可擴展性 

多核可擴展性不一樣於多線程可擴展性。咱們都熟知這個理念:處理器的速度並無變快,咱們只是靠增長數量來達到目的。 
大多數的代碼都未實現4核以上的並行。當咱們添加更多內核時,降低的不只僅是性能等級,處理速度可能也會變得愈來愈慢,這是軟件的問題。咱們但願軟件的提升速度同內核的增長接近線性正相關。 
多線程編程不一樣於多核編程

  • 多線程
  • 每一個CPU內核中不止一個線程
  • 用鎖來協調線程(經過系統調用)
  • 每一個線程有不一樣的任務
  • 多核
  • 每一個CPU內核中只有一個線程
  • 當兩個線程/內核訪問同一個數據時,不能停下來互相等待
  • 同一個任務的不一樣線程
  • 要解決的問題是怎樣將一個應用程序分佈到多個內核中去
  • Unix中的鎖在內核實現。4內核使用鎖的狀況是大多數軟件開始等待其餘線程解鎖。所以,增長內核所得到的收益遠遠低於等待中的性能損耗。
  • 咱們須要這樣一個架構,它更像高速公路而不是紅綠燈控制的十字路口,無需等待,每一個人都以本身的節奏行進,儘量節省開銷。
  • 解決方案:
  • 在每一個核心中保存數據結構,而後聚合的對數據進行讀取。
  • 原子性。CPU支持能夠經過C語言調用的指令,保證原子性,避免衝突發生。開銷很大,因此不要到處使用。
  • 無鎖的數據結構。線程無需等待便可訪問,在不一樣的架構下都是複雜的工做,請不要本身作。
  • 線程模型,即流水線與工做線程模型。這不僅是同步的問題,而是你的線程如何架構。
  • 處理器關聯。告訴操做系統優先使用前兩個內核,而後設置線程運行在哪個內核上,你也能夠經過中斷到達這個目的。因此,CPU由你來控制而不是Linux。

內存的可擴展性 

  • 若是你有20G的RAM,假設每次鏈接佔用2K的內存,若是你還有20M的三級緩存,緩存中會沒有數據。數據轉移到主存中處理花費300個時鐘週期,此時CPU沒有作任何事情。
  • 每一個數據包要有1400個時鐘週期(DNS / IDS的功能)和200個時鐘週期(獲取數據包)的開銷,每一個數據包咱們只有4個高速緩存缺失,這是一個問題。
  • 聯合定位數據
  • 不要經過指針在滿內存亂放數據。每次你跟蹤一個指針,都會是一個高速緩存缺失:[hash pointer] -> [Task Control Block] -> [Socket] -> [App],這是四個高速緩存缺失。
  • 保持全部的數據在一個內存塊:[TCB |socket| APP]。給全部塊預分配內存,將高速緩存缺失從4減小到1。
  • 分頁
  • 32GB的數據需佔用64MB的分頁表,不適合都存儲在高速緩存。因此存在兩個高速緩存缺失——分頁表和它所指向的數據。這是開發可擴展的軟件不能忽略的細節。
  • 解決方案:壓縮數據,使用有不少內存訪問的高速緩存架構,而不是二叉搜索樹
  • NUMA架構加倍了主存訪問時間。內存可能不在本地socket,而是另外一個socket上。
  • 內存池
  • 啓動時當即預先分配全部的內存
  • 在對象,線程和socket的基礎上進行分配。
  • 超線程
  • 每一個網絡處理器最多能夠運行4個線程,英特爾只能運行2個。
  • 在適當的狀況下,咱們還須要掩蓋延時,好比內存訪問中一個線程在等待另外一個全速的線程。
  • 大內存頁
  • 減少頁表規模。從一開始就預留內存,讓你的應用程序管理內存。

總結

  • 網卡
  • 問題:經過內核工做效率不高
  • 解決方案:使用本身的驅動程序並管理它們,使適配器遠離操做系統。
  • CPU
  • 問題:使用傳統的內核方法來協調你的應用程序是行不通的。
  • 解決方案:Linux管理前兩個CPU,你的應用程序管理其他的CPU。中斷只發生在你容許的CPU上。
  • 內存
  • 問題:內存須要特別關注,以求高效。
  • 解決方案:在系統啓動時就分配大部份內存給你管理的大內存頁

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

原文鏈接:The Secret To 10 Million Concurrent Connections -The Kernel Is The Problem, Not The Solution (文/周小璐,審校/仲浩)

相關文章
相關標籤/搜索