千萬級併發連接的祕密

既然我們已經解決了過萬併發連接(C10K concurrent connection problem)的問題,現在如何升級到支持千萬級的併發連接?你會說:“不可能”。不,現在,一些系統通過使用一些不廣爲人知的先進技術,已經能夠提供千萬級的併發連接。

爲了明白這是如何實現,我們找到了Errata Security的CEO—— Robert Graham和他在Shmoocon 2013上精彩絕倫的演講—— C10M Defending The Internet At Scale(譯者注:翻牆的同學可以去看看)。

Robert解決這個問題的方法如此技藝高超,此前我從未聽說過。他首先講了一段Unix的歷史,他提到Unix系統最開始並不是設計成通用的服務器操作系統,而是設計成電話網絡中的控制系統。電話網絡中,控制面與數據面有明顯的區分,數據傳輸是在電話網絡中進行的。問題就在於我們現在把Unix服務器當成是用戶面的部分來使用,這是不應該的。如果我們設計內核時是爲了讓每臺服務器運行一個應用程序,那會與現在的多用戶內核有巨大得差別。

所以,他說關鍵是在於明白:

·內核並不是解決方法,內核是問題所在

意思是:

·不要讓內核做所有繁重的工作。將數據包處理,內存管理和線程調度等從內核中移出來,放到應用程序裏,使其處理得更加高效。讓Linux內核處理控制面,應用程序處理數據面。

這樣,系統在處理上千萬的併發連接時,200個時鐘週期用於數據包處理,1400個時鐘週期用於程序邏輯。由於內存訪問要使用300個時鐘週期,使用減少代碼和減少cache丟失的方法進行設計也是關鍵所在。

一個專門的數據面處理系統,可以處理每秒1千萬的數據包。而一個控制面的處理系統,每秒只能處理1百萬的數據包。

如果這看起來很極端,那麼記住一句老話:可擴展性是一門技術活。爲了做出成功的系統,千萬別把性能“外包”給操作系統。你必須親自去完成。

現在,然我們看一下Robert是如何構建一個具備支持千萬級別併發連接能力的系統……

C10K 問題 – 上一個十年

十年前,工程師們解決了C10K(concurrent 10,000)可擴展性問題。他們通過爲內核打補丁、從多線程服務器(如Apache)遷移到事件驅動服務器(如Nginx和Node)。人們從Apache到可擴展服務器遷移了10年。在最近幾年,我們看到了人們更快的採用可擴展服務器。

Apache的問題

·Apache的問題在於,隨着連接的增多,性能愈發下降。

·關鍵點:性能與可擴展性是正交的。這兩個是不同的概念。當人們討論擴展時他們常常會說到性能,但是這兩者間有着明顯的區別。

·對於只持續幾秒的短連接,我們稱之爲快事務(quick transaction),如果你能夠處理1000TPS(Transaction Per Second),那麼你的服務器只能支持1000的併發連接。

·如果事務的時長改爲10秒,現在在1000TPS的情況下你能夠支持10,000的連接。Apache的性能會急劇下降,即使這可能會觸發了DoS攻擊的檢測。通過大量的下載就能使Apache宕機。

·如果你能夠每秒處理5000連接,而你要怎麼做才能夠每秒處理10,000連接呢?假設你升級了硬件而且使處理器快了兩倍。會發生什麼?你能夠獲得兩倍的性能,但是你不能獲得兩倍的擴展。也許你只能夠處理每秒6000的連接。如果持續升級硬件,你會得到同樣的結果。性能與可擴展性是不一樣的。

·問題在於Apache會創建一個CGI進程然後殺掉他。這導致了其不可擴展。

·爲什麼?服務器不能處理10,000個併發連接是由於內核使用O(n)算法。

  ·內核中兩個基本問題:

  1)  連接數=線程數/進程數。一個數據包進來,內核要遍歷所有10,000個進程找到處理這個數據包的進程。

  2)  連接數=select數/poll數。同樣的可擴展問題,每一個數據包都要遍歷sockets列表。

  ·解決方法:爲內核打上補丁,使其查找時間爲常數。

  1)  現在無論線程數量多少,線程的切換時間是常數。

  2)  使用epoll()/IOCompeltionPort 可擴展的系統調用能夠在常數時間查找socket。

·線程調度仍不能夠擴展,所以服務器使用epoll的異步編程模型,在Node和Nginx中都體現了。即使一臺較慢的服務器,增加連接數時性能不會急劇下降。10,000的連接,筆記本都能比16核的服務器快。

C10M問題 -- 下一個十年

在不遠的未來,服務器將需要處理百萬級別的併發連接。隨着IPv6的普及,我們要開始下一個階段的擴展,使得服務器支持的連接數達到百萬。

·需要這樣的可擴展性應用包括:IDS/IPS,因爲他們連接到服務器的骨幹。其他應用如DNS根服務器,TOR節點,因特網的Nmap,視頻流,金融,NAT,Voip交換機,負載均衡服務器,web緩存,防火牆,郵件接收服務,垃圾郵件過濾等。

·其他遇到擴展問題的人包括設備供應商,因爲他們銷售軟硬一體的設備。你購買這些設備直接放置到數據中心裏使用。這些設備可能會有一塊專門用於加密,數據包解析等的Intel主板或者網絡處理器。

·在新蛋網上一臺40gbps,32核,256G內存的X86服務器的價格只要5000美金。這樣的服務器能夠處理超過10,000的連接。如果不行的話,是因爲你軟件設計不好,並不是硬件的問題。這樣的硬件可以很輕易擴展到千萬的併發連接。

千萬併發連接的挑戰意味着什麼:

1. 一千萬併發連接數

2. 每秒一百萬連接數 —— 每個連接持續時間大概是10秒

3. 每秒100億比特 —— 因特網的快速鏈接

4. 每秒1千萬個數據包 —— 預計,當前服務器每秒處理50,000個數據包,這將要提高一個層次。每個數據包會觸發一次中斷,而之前服務器每秒能處理100,000箇中斷。

5. 10毫秒的時延 —— 可擴展的服務器或許能夠解決得了擴展問題,但是時延並不行。

6. 10毫秒的抖動 —— 限制最大時延

7. 10個CPU —— 軟件將要擴展到多核的服務器。大多數軟件只可以輕易擴展到4個核,由於服務器要擴展到更多核的服務器,所以軟件也要重寫做相應的支持。

我們瞭解到Unix並不是用於網絡編程

·將近一代的程序員都學習過Richard Stevens編寫的<<Unix Network Programming>>。問題在於這本書是關於Unix,而不僅僅是網絡編程。他告訴你如何讓Unix完成繁重的工作,而你也只在Unix上編寫一個小小的服務器。但是內核並不是可擴展的。解決方法在於將繁重的工作從內核剝離,由自己完成。

·比如,考慮一下Apache每個連接一個線程的模型帶來的影響。這意味着線程調度器根據收到的數據包來決定調用哪一個線程的read()。這等於是將線程調度器當做數據包調度器來使用。(我真的很喜歡這個模型,以前從來沒想過這個方法。譯者注:這是反話吧)

·而Nginx並不把線程調度器當做數據包調度器來使用。而是自己完成數據包調度。使用select來查找socket,一旦發現數據馬上讀取,這樣就不會產生阻塞。

·讓Unix處理網絡協議棧,而你處理所有其他的事情。

如何編寫可擴展的軟件

如何修改軟件使其可擴展呢?許多關於硬件處理能力的經驗估計都是錯誤的。我們要知道實際的性能能力是什麼。

爲了更進一步,我們要解決一下幾個問題:

1. 數據包可擴展性

2. 多核可擴展性

3. 內存可擴展性

數據包擴展 —— 編寫自定義的驅動旁路內核協議棧

·數據包的問題在於他們需要穿過Unix內核。內核協議棧既複雜又慢。數據包到達你的程序的路徑應該更加直接。不要讓操作系統來處理數據包。

·編寫你自己的驅動方法是,驅動只需要將數據包發送給你的應用程序,而不要經過內核的協議棧。你可以找到的驅動:PF_RING, Netmap, Intel DPDK (數據包開發套件)。Intel是不開源的,但是其提供許多的支持。

·多快?Intel有一個基準,在一臺輕量級服務器上每秒能處理8千萬個數據包。這是在用戶態實現的。數據包到達用戶態,然後再發送出去。而使用Linux處理UDP的情況下,每秒只能達到1百萬個數據包。自定義的驅動是Linux的80倍。

·爲了實現每秒處理1千萬個數據包,200個時鐘週期用於獲取數據包,剩餘1400個時鐘週期用於實現應用程序功能。

·通過PF_RING收到的原生數據包,你必須自己實現TCP協議棧。很多人已經實現了用戶態的協議棧。比如Intel就提供了一個高性能可擴展的TCP協議棧。

多核的可擴展性

多核的擴展和多線程的擴展並不完全相同。我們知道,相比更快的處理器,獲取更多的處理器要來得容易。

大多數的代碼不能擴展超過4個核。當我們添加更多的核時,性能並沒有不斷提升。這是因爲軟件的問題。我們要使軟件能夠隨着核數線性的擴展,要使軟件通過增多核數來提高性能。

多線程編程並不是多核編程

·多線程:

  ·每個核超過一個線程

  ·線程通過鎖來同步

  ·每個線程一個任務

·多核:

  ·每個核一個線程

  ·當兩個線程訪問同一個數據的時候,他們不能停下來等待對方

  ·所有的線程做同一個任務

·我們的問題是如何擴展應用程序到多個核

·在Unix中鎖是在內核中實現。當核在等待線程釋放鎖時,內核開始佔用我們的CPU資源。 我們需要的架構是像高速公路而不是有交通燈控制的十字路口。我們要使每個線程都按照自己的步伐在運行,而不是等待。

·解決方法:

  ·每個核一個數據結構。

  ·原子性。使用C語言中CPU支持的原子指令。保證原子性,而絕不要發生衝突。但隨之而來的是昂貴的代價,所以不要到處使用。

  ·免鎖的數據結構。線程間訪問這些數據是不需要停止和等待。千萬不要自己去實現,因爲這樣的數據結構跨平臺的實現會非常複雜。

  ·線程模型。流水線和工作者線程模型。問題不僅僅在於同步,而且在於線程的設計。

  ·處理核附着。讓操作系統使用前兩個核。設置你的線程到其他處理核上。同樣的方法可以用到中斷上。這樣你就可以獨佔這些CPU,而不受Linux內核影響。

內存擴展

·問題在於,如果你有20G的內存,假設每個連接使用2K,在你只有20M的L3緩存的情況下,沒有任何連接數據是在緩存裏的。這會消耗300個時鐘週期用來到內存中取數據。

·仔細考慮我們每個數據包1400個時鐘週期的處理預算。記住那200個時鐘週期/每數據包的額外開銷。我們只可以允許4次cache丟失,這是個問題。

·使數據放置到一起

  ·不要通過指針胡亂的引用內存裏的數據。每一次你引用指針將會導致一次cache丟失:[hash pointer] -> [Task Control Block] -> [Socket] -> [App]。這就4次cache丟失。

  ·將數據放到同一個內存塊裏:[TCB|Socket|App]。通過預分配所有的內存塊來保護內存。這會將cache丟失次數從4減到1。

·分頁

  ·32G內存就需要64M的分頁表,cache裝不下。所以你會有兩次cache丟失,一次是分頁表,而另一次是頁表所指的內存。這是我們考慮可擴展軟件時不能忽略的細節。

  ·解決方法:壓縮數據;使用高效的緩存數據結構替代有大量內存訪問的二叉搜索樹。

·NUMA架構使得內存訪問時間加倍。

·內存池

  ·啓動時一次預分配所有的內存。

  ·基於每一個對象,每一個線程,每一個套接字進行分配。

·超線程

  ·網絡處理器上每個處理器可以跑4個線程,而Intel只可以跑2個。

  ·這可以掩蓋時延,比如內存訪問,因爲當一個線程在等待時,另一個可全速執行。

·超大頁

  ·減少分頁表的大小。從一開始就保留內存,然後由應用程序來管理。

總結

·NIC

  ·問題:數據包通過內核協議棧,效率不高

  ·解決方法:編寫自己的驅動來管理協議棧

·CPU

  ·問題:如果你使用傳統的方法在內核上構建應用程序,這並不高效

  ·解決方法:賦予Linux前兩個CPU,剩下的CPU由你的應用程序管理。這些CPU上不允許網卡中斷。

·內存

  ·問題:需要仔細考慮如何使其更加高效

  ·解決方法:在系統啓動時預分配大部分內存,由自己來管理。

控制面留給Linux,數據面跑在應用程序的代碼裏。他從不與內核交互,沒有線程調度,沒有系統調用,沒有中斷,什麼都沒有。

還有,你手上的是可以正常調試運行在Linux上的代碼,而不是由特定工程師構造的一些奇怪的硬件系統。你可以通過熟悉的編程語言和開發環境,達到你期望的特定硬件處理數據的性能。

 

 [英文原文: The Secret To 10 Million Concurrent Connections -The Kernel Is The Problem, Not The Solution]

http://www.cnblogs.com/wilsonwen/archive/2013/05/25/3098202.html

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