svc C10K 問題引發的技術變革

2022-03-21 22:38:02 字數 2411 閱讀 6265

c10k 問題引發的技術變革

伺服器應用領域很古老很出名的乙個問題,大意是說單台伺服器要同時支援併發 10k 量級的連線,這些連線可能是保持存活狀態的。

解決這一問題,主要思路有兩個:乙個是對於每個連線處理分配乙個獨立的程序/執行緒;另乙個思路是用同一程序/執行緒來同時處理若干連線。

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

因此,這一思路在伺服器資源還沒有富裕到足夠程度的時候,是不可行的;即便資源足夠富裕,效率也不夠高。

問題:資源占用過多,可擴充套件性差。

最簡單的方法是迴圈挨個處理各個連線,每個連線對應乙個 socket,當所有 socket 都有資料的時候,這種方法是可行的。

但是當應用讀取某個 socket 的檔案資料不 ready 的時候,整個應用會阻塞在這裡等待該檔案控制代碼,即使別的檔案控制代碼 ready,也無法往下處理。

要解決上面阻塞的問題,思路很簡單,如果我在讀取檔案控制代碼之前,先查下它的狀態,ready 了就進行處理,不 ready 就不進行處理,這不就解決了這個問題了嘛?

於是有了 select 方案。用乙個 fd_set 結構體來告訴核心同時監控多個檔案控制代碼,當其中有檔案控制代碼的狀態發生指定變化(例如某控制代碼由不可用變為可用)或超時,則呼叫返回。之後應用可以使用 fd_isset 來逐個檢視是哪個檔案控制代碼的狀態發生了變化。

這樣做,小規模的連線問題不大,但當連線數很多(檔案控制代碼個數很多)的時候,逐個檢查狀態就很慢了。因此,select 往往存在管理的控制代碼上限(fd_setsize)。同時,在使用上,因為只有乙個字段記錄關注和發生事件,每次呼叫之前要重新初始化 fd_set 結構體。

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

思路:有連線請求抵達了再檢查處理。

問題:控制代碼上限+重複初始化+逐個排查所有檔案控制代碼狀態效率不高。

poll 主要解決 select 的前兩個問題:通過乙個 pollfd 陣列向核心傳遞需要關注的事件消除檔案控制代碼上限,同時使用不同字段分別標註關注事件和發生事件,來避免重複初始化。

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

思路:設計新的資料結構提供使用效率。

問題:逐個排查所有檔案控制代碼狀態效率不高。

既然逐個排查所有檔案控制代碼狀態效率不高,很自然的,如果呼叫返回的時候只給應用提供發生了狀態變化(很可能是資料 ready)的檔案控制代碼,進行排查的效率不就高多了麼。

epoll 採用了這種設計,適用於大規模的應用場景。

實驗表明,當檔案控制代碼數目超過 10 之後,epoll 效能將優於 select 和 poll;當檔案控制代碼數目達到 10k 的時候,epoll 已經超過 select 和 poll 兩個數量級。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

思路:只返回狀態變化的檔案控制代碼。

問題:依賴特定平台(linux)。

跨平台,封裝底層平台的呼叫,提供統一的 api,但底層在不同平台上自動選擇合適的呼叫。

隨著技術的演進,epoll 已經可以較好的處理 c10k 問題,但是如果要進一步的擴充套件,例如支援 10m 規模的併發連線,原有的技術就無能為力了。

那麼,新的瓶頸在**呢?

從前面的演化過程中,我們可以看到,根本的思路是要高效的去阻塞,讓 cpu 可以幹核心的任務。

當連線很多時,首先需要大量的程序/執行緒來做事。同時系統中的應用程序/執行緒們可能大量的都處於 ready 狀態,需要系統去不斷的進行快速切換,而我們知道系統上下文的切換是有代價的。雖然現在 linux 系統的排程演算法已經設計的很高效了,但對於 10m 這樣大規模的場景仍然力有不足。

所以我們面臨的瓶頸有兩個,乙個是程序/執行緒作為處理單元還是太厚重了;另乙個是系統排程的代價太高了。

很自然地,我們會想到,如果有一種更輕量級的程序/執行緒作為處理單元,而且它們的排程可以做到很快(最好不需要鎖),那就完美了。

這樣的技術現在在某些語言中已經有了一些實現,它們就是 coroutine(協程),或協作式例程。具體的,python、lua 語言中的 coroutine(協程)模型,go 語言中的 goroutine(go 程)模型,都是類似的乙個概念。實際上,多種語言(甚至 c 語言)都可以實現類似的模型。

它們在實現上都是試圖用一組少量的執行緒來實現多個任務,一旦某個任務阻塞,則可能用同一執行緒繼續執行其他任務,避免大量上下文的切換。每個協程所獨佔的系統資源往往只有棧部分。而且,各個協程之間的切換,往往是使用者通過**來顯式指定的(跟各種 callback 類似),不需要核心參與,可以很方便的實現非同步。

C10K 問題原文

編寫連線數巨大的高負載伺服器程式時,經典的多執行緒模式和select模式都不再適用。應當拋棄它們,採用epoll,kqueue,dev poll來捕獲i o事件。最後簡要介紹了aio。網路服務在處理數以萬計的客戶端連線時,往往出現效率低下甚至完全癱瘓,這被稱為c10k問題。隨著網際網路的迅速發展,越...

C10K 問題原文

編寫連線數巨大的高負載伺服器程式時,經典的多執行緒模式和select模式都不再適用。應當拋棄它們,採用epoll,kqueue,dev poll來捕獲i o事件。最後簡要介紹了aio。網路服務在處理數以萬計的客戶端連線時,往往出現效率低下甚至完全癱瘓,這被稱為c10k問題。隨著網際網路的迅速發展,越...

K選擇問題

從n個數當中選出第k個最大者。最簡單的兩種演算法 優先佇列基本模型 優先佇列的簡單實現 方法a,鍊錶 表頭插入 遍歷鍊錶刪除最小元。時間複雜度o 1 o n 方法b,二叉查詢樹。時間複雜度o logn 優先佇列更好的實現方案 二叉堆 簡稱堆 a.二叉堆的結構性質 堆 完全填滿的二叉樹。底層元素從左到...