一、前言<ignore_js_op> 對於高性能即時通信技術(或者說互聯網編程)比較關注的開發者,對C10K問題(即單機1萬個併發鏈接問題)應該都有所瞭解。「C10K」概念最先由Dan Kegel發佈於其我的站點,即出自其經典的《The C10K problem (英文PDF版、中文譯文)》一文。 正如你所料,過去的10年裏,高性能網絡編程技術領域裏通過衆多開發者的努力,已很好地解決了C10K問題,你們已開始關注並着手解決下一個十年要面對的C10M問題(即單機1千萬個併發鏈接問題,C10M相關技術討論和學習將在本系列文章的下篇中開始展開,本文不做深刻介紹)。 雖然C10K問題已被妥善解決,但對於即時通信應用(或其它網絡編程方面)的開發者而言,研究C10K問題仍然價值巨大,由於技術的發展都是有規律和線索可循的,瞭解C10K問題及其解決思路,經過觸類旁通,或許能夠爲你之後面對相似問題提供更多可借鑑的思想和解決問題的實踐思路。而這,也正是撰寫本文的目的所在。 二、C10K問題系列文章本文是C10K問題系列文章中的第2篇,總目錄以下:
三、C10K問題的提出者<ignore_js_op> Dan Kegel:軟件工程師 目前工做在美國的洛杉磯,當前受僱於Google公司。從1978年起開始接觸計算機編程,是Winetricks的做者、也是Wine 1.0的管理員,同時也是Crosstool( 一個讓 gcc/glibc 編譯器更易用的工具套件)的做者。發表了著名的《The C10K problem》技術文章,是Java JSR-51規範的提交者並參與編寫了Java平臺的NIO和文件鎖,同時參與了RFC 5128標準中有關NAT 穿越(P2P打洞)技術的描述和定義。 四、C10K問題的由來你們都知道互聯網的基礎就是網絡通訊,早期的互聯網能夠說是一個小羣體的集合。互聯網還不夠普及,用戶也很少,一臺服務器同時在線100個用戶估計在當時已經算是大型應用了,因此並不存在什麼 C10K 的難題。互聯網的爆發期應該是在www網站,瀏覽器,雅虎出現後。最先的互聯網稱之爲Web1.0,互聯網大部分的使用場景是下載一個HTML頁面,用戶在瀏覽器中查看網頁上的信息,這個時期也不存在C10K問題。 Web2.0時代到來後就不一樣了,一方面是普及率大大提升了,用戶羣體幾何倍增加。另外一方面是互聯網再也不是單純的瀏覽萬維網網頁,逐漸開始進行交互,並且應用程序的邏輯也變的更復雜,從簡單的表單提交,到即時通訊和在線實時互動,C10K的問題才體現出來了。由於每個用戶都必須與服務器保持TCP鏈接才能進行實時的數據交互,諸如Facebook這樣的網站同一時間的併發TCP鏈接極可能已通過億。 早期的騰訊QQ也一樣面臨C10K問題,只不過他們是用了UDP這種原始的包交換協議來實現的,繞開了這個難題,固然過程確定是痛苦的。若是當時有epoll技術,他們確定會用TCP。衆所周之,後來的手機QQ、微信都採用TCP協議。 這時候問題就來了,最初的服務器都是基於進程/線程模型的,新到來一個TCP鏈接,就須要分配1個進程(或者線程)。而進程又是操做系統最昂貴的資源,一臺機器沒法建立不少進程。若是是C10K就要建立1萬個進程,那麼單機而言操做系統是沒法承受的(每每出現效率低下甚至徹底癱瘓)。若是是採用分佈式系統,維持1億用戶在線須要10萬臺服務器,成本巨大,也只有Facebook、Google、雅虎等巨頭纔有財力購買如此多的服務器。 基於上述考慮,如何突破單機性能侷限,是高性能網絡編程所必需要直面的問題。這些侷限和問題最先被Dan Kegel 進行了概括和總結,並首次成系統地分析和提出解決方案,後來這種廣泛的網絡現象和技術侷限都被你們稱爲 C10K 問題。 五、技術解讀C10K問題C10K 問題的最大特色是:設計不夠良好的程序,其性能和鏈接數及機器性能的關係每每是非線性的。 舉個例子:若是沒有考慮過 C10K 問題,一個經典的基於 select 的程序能在舊服務器上很好處理 1000 併發的吞吐量,它在 2 倍性能新服務器上每每處理不了併發 2000 的吞吐量。這是由於在策略不當時,大量操做的消耗和當前鏈接數 n 成線性相關。會致使單個任務的資源消耗和當前鏈接數的關係會是 O(n)。而服務程序須要同時對數以萬計的socket 進行 I/O 處理,積累下來的資源消耗會至關可觀,這顯然會致使系統吞吐量不能和機器性能匹配。 以上這就是典型的C10K問題在技術層面的表現。這也是爲什麼一樣的功能,大多數開發人員都能很容易地從功能上實現,但一旦放到大併發場景下,初級與高級開發者對同一個功能的技術實現所體現出的實際應用效果,則是大相徑庭的。 因此說,一些沒有太多大併發實踐經驗的技術同行,所實現的諸如即時通信應用在內的網絡應用,所謂的理論負載動不動就宣稱能支持單機上萬、上十萬甚至上百萬的狀況,是經不起檢驗和考驗的。 六、C10K問題的本質C10K問題本質上是操做系統的問題。對於Web1.0/2.0時代的操做系統而言, 傳統的同步阻塞I/O模型都是同樣的,處理的方式都是requests per second,併發10K和100的區別關鍵在於CPU。 建立的進程線程多了,數據拷貝頻繁(緩存I/O、內核將數據拷貝到用戶進程空間、阻塞), 進程/線程上下文切換消耗大, 致使操做系統崩潰,這就是C10K問題的本質! 可見,解決C10K問題的關鍵就是儘量減小這些CPU等核心計算資源消耗,從而榨乾單臺服務器的性能,突破C10K問題所描述的瓶頸。 七、C10K問題的解決方案探討要解決這一問題,從純網絡編程技術角度看,主要思路有兩個:
1思路一:每一個進程/線程處理一個鏈接這一思路最爲直接。可是因爲申請進程/線程會佔用至關可觀的系統資源,同時對於多進程/線程的管理會對系統形成壓力,所以這種方案不具有良好的可擴展性。 所以,這一思路在服務器資源尚未富裕到足夠程度的時候,是不可行的。即使資源足夠富裕,效率也不夠高。總之,此思路技術實現會使得資源佔用過多,可擴展性差。 2思路二:每一個進程/線程同時處理多個鏈接(IO多路複用)IO多路複用從技術實現上又分不少種,咱們逐一來看看下述各類實現方式的優劣。 ● 實現方式1:傳統思路最簡單的方法是循環挨個處理各個鏈接,每一個鏈接對應一個 socket,當全部 socket 都有數據的時候,這種方法是可行的。可是當應用讀取某個 socket 的文件數據不 ready 的時候,整個應用會阻塞在這裏等待該文件句柄,即便別的文件句柄 ready,也沒法往下處理。 實現小結:直接循環處理多個鏈接。 問題概括:任一文件句柄的不成功會阻塞住整個應用。 ● 實現方式2:select要解決上面阻塞的問題,思路很簡單,若是我在讀取文件句柄以前,先查下它的狀態,ready 了就進行處理,不 ready 就不進行處理,這不就解決了這個問題了嘛?因而有了 select 方案。用一個 fd_set 結構體來告訴內核同時監控多個文件句柄,當其中有文件句柄的狀態發生指定變化(例如某句柄由不可用變爲可用)或超時,則調用返回。以後應用可使用 FD_ISSET 來逐個查看是哪一個文件句柄的狀態發生了變化。這樣作,小規模的鏈接問題不大,但當鏈接數不少(文件句柄個數不少)的時候,逐個檢查狀態就很慢了。所以,select 每每存在管理的句柄上限(FD_SETSIZE)。同時,在使用上,由於只有一個字段記錄關注和發生事件,每次調用以前要從新初始化 fd_set 結構體。
問題概括:句柄上限+重複初始化+逐個排查全部文件句柄狀態效率不高。 ● 實現方式3:poll 主要解決 select 的前兩個問題:經過一個 pollfd 數組向內核傳遞須要關注的事件消除文件句柄上限,同時使用不一樣字段分別標註關注事件和發生事件,來避免重複初始化。 實現小結:設計新的數據結構提供使用效率。 問題概括:逐個排查全部文件句柄狀態效率不高。 ● 實現方式4:epoll既然逐個排查全部文件句柄狀態效率不高,很天然的,若是調用返回的時候只給應用提供發生了狀態變化(極可能是數據 ready)的文件句柄,進行排查的效率不就高多了麼。epoll 採用了這種設計,適用於大規模的應用場景。實驗代表,當文件句柄數目超過 10 以後,epoll 性能將優於 select 和 poll;當文件句柄數目達到 10K 的時候,epoll 已經超過 select 和 poll 兩個數量級。 實現小結:只返回狀態變化的文件句柄。 問題概括:依賴特定平臺(Linux)。 由於Linux是互聯網企業中使用率最高的操做系統,Epoll就成爲C10K killer、高併發、高性能、異步非阻塞這些技術的代名詞了。FreeBSD推出了kqueue,Linux推出了epoll,Windows推出了IOCP,Solaris推出了/dev/poll。這些操做系統提供的功能就是爲了解決C10K問題。epoll技術的編程模型就是異步非阻塞回調,也能夠叫作Reactor,事件驅動,事件輪循(EventLoop)。Nginx,libevent,node.js這些就是Epoll時代的產物。 ● 實現方式5:因爲epoll, kqueue, IOCP每一個接口都有本身的特色,程序移植很是困難,因而須要對這些接口進行封裝,以讓它們易於使用和移植,其中libevent庫就是其中之一。跨平臺,封裝底層平臺的調用,提供統一的 API,但底層在不一樣平臺上自動選擇合適的調用。按照libevent的官方網站,libevent庫提供瞭如下功能:當一個文件描述符的特定事件(如可讀,可寫或出錯)發生了,或一個定時事件發生了,libevent就會自動執行用戶指定的回調函數,來處理事件。目前,libevent已支持如下接口/dev/poll, kqueue, event ports, select, poll 和 epoll。Libevent的內部事件機制徹底是基於所使用的接口的。所以libevent很是容易移植,也使它的擴展性很是容易。目前,libevent已在如下操做系統中編譯經過:Linux,BSD,Mac OS X,Solaris和Windows。使用libevent庫進行開發很是簡單,也很容易在各類unix平臺上移植。一個簡單的使用libevent庫的程序以下: <ignore_js_op> 八、參考資料[1] 爲何QQ用的是UDP協議而不是TCP協議? [2] 移動端IM/推送系統的協議選型:UDP仍是TCP? [3] 高性能網絡編程經典:《The C10K problem(英文)》[附件下載] [4] 高性能網絡編程(一):單臺服務器併發TCP鏈接數到底能夠有多少 [5] 《The C10K problem (英文在線閱讀、英文PDF版下載、中文譯文)》 [6] <ignore_js_op> 搜狗實驗室技術交流文檔《C10K問題探討》(52im.net).pdf [7] [通俗易懂]深刻理解TCP協議(上):理論基礎 [8] [通俗易懂]深刻理解TCP協議(下):RTT、滑動窗口、擁塞處理 [9] 《TCP/IP詳解 卷1:協議 (在線閱讀版)》 九、更多資料《TCP/IP詳解 - 第11章·UDP:用戶數據報協議》 《TCP/IP詳解 - 第17章·TCP:傳輸控制協議》 《TCP/IP詳解 - 第18章·TCP鏈接的創建與終止》 《TCP/IP詳解 - 第21章·TCP的超時與重傳》 《技術往事:改變世界的TCP/IP協議(珍貴多圖、手機慎點)》 《通俗易懂-深刻理解TCP協議(上):理論基礎》 《通俗易懂-深刻理解TCP協議(下):RTT、滑動窗口、擁塞處理》 《理論經典:TCP協議的3次握手與4次揮手過程詳解》 《理論聯繫實際:Wireshark抓包分析TCP 3次握手、4次揮手過程》 《計算機網絡通信協議關係圖(中文珍藏版)》 《UDP中一個包的大小最大能多大?》 《Java新一代網絡編程模型AIO原理及Linux系統AIO介紹》 《NIO框架入門(一):服務端基於Netty4的UDP雙向通訊Demo演示》 《NIO框架入門(二):服務端基於MINA2的UDP雙向通訊Demo演示》 《NIO框架入門(三):iOS與MINA二、Netty4的跨平臺UDP雙向通訊實戰》 《NIO框架入門(四):Android與MINA二、Netty4的跨平臺UDP雙向通訊實戰》 《P2P技術詳解(一):NAT詳解——詳細原理、P2P簡介》 《P2P技術詳解(二):P2P中的NAT穿越(打洞)方案詳解》 《P2P技術詳解(三):P2P技術之STUN、TURN、ICE詳解》 《高性能網絡編程(一):單臺服務器併發TCP鏈接數到底能夠有多少》 《高性能網絡編程(二):上一個10年,著名的C10K併發鏈接問題》 《高性能網絡編程(三):下一個10年,是時候考慮C10M併發問題了》 《高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索》 >> 更多同類文章 …… |