C10k簡介

NO IMAGE

1、前言

對於高效能即時通訊技術(或者說網際網路程式設計)比較關注的開發者,對C10K問題(即單機1萬個併發連線問題)應該都有所瞭解。“C10K”概念最早由Dan Kegel釋出於其個人站點,即出自其經典的《The C10K problem英文PDF版中文譯文)》一文。

正如你所料,過去的10年裡,高效能網路程式設計技術領域裡經過眾多開發者的努力,已很好地解決了C10K問題,大家已開始關注並著手解決下一個十年要面對的C10M問題(即單機1千萬個併發連線問題,C10M相關技術討論和學習將在本系列文章的下篇中開始展開,本文不作深入介紹)。

雖然C10K問題已被妥善解決,但對於即時通訊應用(或其它網路程式設計方面)的開發者而言,研究C10K問題仍然價值巨大,因為技術的發展都是有規律和線索可循的,瞭解C10K問題及其解決思路,通過舉一反三,或許可以為你以後面對類似問題提供更多可借鑑的思想和解決問題的實踐思路。而這,也正是撰寫本文的目的所在。

2、學習交流

即時通訊開發交流群:215891622 [推薦]

移動端IM開發推薦文章:《新手入門一篇就夠:從零開發移動端IM

3、C10K問題系列文章

本文是C10K問題系列文章中的第2篇,總目錄如下:

高效能網路程式設計(一):單臺伺服器併發TCP連線數到底可以有多少

高效能網路程式設計(二):上一個10年,著名的C10K併發連線問題》(本文)

高效能網路程式設計(三):下一個10年,是時候考慮C10M併發問題了

高效能網路程式設計經典:《The C10K problem(英文)》[附件下載]

4、C10K問題的提出者

Dan Kegel:軟體工程師

目前工作在美國的洛杉磯,當前受僱於Google公司。從1978年起開始接觸計算機程式設計,是Winetricks的作者、也是Wine 1.0的管理員,同時也是Crosstool( 一個讓 gcc/glibc 編譯器更易用的工具套件)的作者。發表了著名的《The C10K problem》技術文章,是Java JSR-51規範的提交者並參與編寫了Java平臺的NIO和檔案鎖,同時參與了RFC 5128標準中有關NAT 穿越(P2P打洞)技術的描述和定義。

5、C10K問題的由來

大家都知道網際網路的基礎就是網路通訊,早期的網際網路可以說是一個小群體的集合。網際網路還不夠普及,使用者也不多,一臺伺服器同時線上100個使用者估計在當時已經算是大型應用了,所以並不存在什麼 C10K 的難題。網際網路的爆發期應該是在www網站,瀏覽器,雅虎出現後。最早的網際網路稱之為Web1.0,網際網路大部分的使用場景是下載一個HTML頁面,使用者在瀏覽器中檢視網頁上的資訊,這個時期也不存在C10K問題。

Web2.0時代到來後就不同了,一方面是普及率大大提高了,使用者群體幾何倍增長。另一方面是網際網路不再是單純的瀏覽全球資訊網網頁,逐漸開始進行互動,而且應用程式的邏輯也變的更復雜,從簡單的表單提交,到即時通訊和線上實時互動,C10K的問題才體現出來了。因為每一個使用者都必須與伺服器保持TCP連線才能進行實時的資料互動,諸如Facebook這樣的網站同一時間的併發TCP連線很可能已經過億。

早期的騰訊QQ也同樣面臨C10K問題,只不過他們是用了UDP這種原始的包交換協議來實現的,繞開了這個難題,當然過程肯定是痛苦的。如果當時有epoll技術,他們肯定會用TCP。眾所周之,後來的手機QQ、微信都採用TCP協議。

實際上當時也有非同步模式,如:select/poll模型,這些技術都有一定的缺點:如selelct最大不能超過1024、poll沒有限制,但每次收到資料需要遍歷每一個連線檢視哪個連線有資料請求。

這時候問題就來了,最初的伺服器都是基於程序/執行緒模型的,新到來一個TCP連線,就需要分配1個程序(或者執行緒)。而程序又是作業系統最昂貴的資源,一臺機器無法建立很多程序。如果是C10K就要建立1萬個程序,那麼單機而言作業系統是無法承受的(往往出現效率低下甚至完全癱瘓)。如果是採用分散式系統,維持1億使用者線上需要10萬臺伺服器,成本巨大,也只有Facebook、Google、雅虎等巨頭才有財力購買如此多的伺服器。

基於上述考慮,如何突破單機效能侷限,是高效能網路程式設計所必須要直面的問題。這些侷限和問題最早被Dan Kegel 進行了歸納和總結,並首次成系統地分析和提出解決方案,後來這種普遍的網路現象和技術侷限都被大家稱為 C10K 問題。

6、技術解讀C10K問題

C10K 問題的最大特點是:設計不夠良好的程式,其效能和連線數及機器效能的關係往往是非線性的。

舉個例子:如果沒有考慮過 C10K 問題,一個經典的基於 select 的程式能在舊伺服器上很好處理 1000 併發的吞吐量,它在 2 倍效能新伺服器上往往處理不了併發 2000 的吞吐量。這是因為在策略不當時,大量操作的消耗和當前連線數 n 成線性相關。會導致單個任務的資源消耗和當前連線數的關係會是 O(n)。而服務程式需要同時對數以萬計的socket 進行 I/O 處理,積累下來的資源消耗會相當可觀,這顯然會導致系統吞吐量不能和機器效能匹配。

以上這就是典型的C10K問題在技術層面的表現。這也是為何同樣的功能,大多數開發人員都能很容易地從功能上實現,但一旦放到大併發場景下,初級與高階開發者對同一個功能的技術實現所體現出的實際應用效果,則是截然不同的。

所以說,一些沒有太多大併發實踐經驗的技術同行,所實現的諸如即時通訊應用在內的網路應用,所謂的理論負載動不動就宣稱能支援單機上萬、上十萬甚至上百萬的情況,是經不起檢驗和考驗的。

7、C10K問題的本質

C10K問題本質上是作業系統的問題。對於Web1.0/2.0時代的作業系統而言, 傳統的同步阻塞I/O模型都是一樣的,處理的方式都是requests per second,併發10K和100的區別關鍵在於CPU。

建立的程序執行緒多了,資料拷貝頻繁(快取I/O、核心將資料拷貝到使用者程序空間、阻塞), 程序/執行緒上下文切換消耗大, 導致作業系統崩潰,這就是C10K問題的本質!

可見,解決C10K問題的關鍵就是儘可能減少這些CPU等核心計算資源消耗,從而榨乾單臺伺服器的效能,突破C10K問題所描述的瓶頸。

8、C10K問題的解決方案探討

要解決這一問題,從純網路程式設計技術角度看,主要思路有兩個:

[1] 一個是對於每個連線處理分配一個獨立的程序/執行緒;

[2] 另一個思路是用同一程序/執行緒來同時處理若干連線。

8.1 思路一:每個程序/執行緒處理一個連線

這一思路最為直接。但是由於申請程序/執行緒會佔用相當可觀的系統資源,同時對於多程序/執行緒的管理會對系統造成壓力,因此這種方案不具備良好的可擴充套件性。

因此,這一思路在伺服器資源還沒有富裕到足夠程度的時候,是不可行的。即便資源足夠富裕,效率也不夠高。總之,此思路技術實現會使得資源佔用過多,可擴充套件性差。

到這裡我還是明白的,再往下,我就開始迷糊了,說明IO複用,說明select等,一臉懵逼

8.2 思路二:每個程序/執行緒同時處理多個連線(IO多路複用)

IO多路複用從技術實現上又分很多種,我們逐一來看看下述各種實現方式的優劣。

● 實現方式1:傳統思路最簡單的方法是迴圈挨個處理各個連線,每個連線對應一個 socket,當所有 socket 都有資料的時候,這種方法是可行的。但是當應用讀取某個 socket 的檔案資料不 ready 的時候,整個應用會阻塞在這裡等待該檔案控制代碼,即使別的檔案控制代碼 ready,也無法往下處理。

實現小結:直接迴圈處理多個連線。

問題歸納:任一檔案控制代碼的不成功會阻塞住整個應用。

● 實現方式2:select要解決上面阻塞的問題,思路很簡單,如果我在讀取檔案控制代碼之前,先查下它的狀態,ready 了就進行處理,不 ready 就不進行處理,這不就解決了這個問題了嘛?於是有了 select 方案。用一個 fd_set 結構體來告訴核心同時監控多個檔案控制代碼,當其中有檔案控制代碼的狀態發生指定變化(例如某控制代碼由不可用變為可用)或超時,則呼叫返回。之後應用可以使用 FD_ISSET 來逐個檢視是哪個檔案控制代碼的狀態發生了變化。這樣做,小規模的連線問題不大,但當連線數很多(檔案控制代碼個數很多)的時候,逐個檢查狀態就很慢了。因此,select 往往存在管理的控制代碼上限(FD_SETSIZE)。同時,在使用上,因為只有一個欄位記錄關注和發生事件,每次呼叫之前要重新初始化 fd_set 結構體。

intselect(intnfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,structtimeval *timeout);

實現小結:有連線請求抵達了再檢查處理。
問題歸納:控制代碼上限 重複初始化 逐個排查所有檔案控制代碼狀態效率不高。

● 實現方式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平臺上移植。


(adsbygoogle = window.adsbygoogle || []).push({});

function googleAdJSAtOnload() {
var element = document.createElement(“script”);
element.src = “//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js”;
element.async = true;
document.body.appendChild(element);
}
if (window.addEventListener) {
window.addEventListener(“load”, googleAdJSAtOnload, false);
} else if (window.attachEvent) {
window.attachEvent(“onload”, googleAdJSAtOnload);
} else {
window.onload = googleAdJSAtOnload;
}