首頁>技術>

C10K

C10K 和 C1000K 的首字母 C 是 Client 的縮寫。C10K 就是單機同時處理 1 萬個請求(併發連線 1 萬)的問題,而 C1000K 也就是單機支援處理 100 萬個請求(併發連線 100 萬)的問題。

I/O 的模型,在 C10K 以前,Linux 中網路處理都用同步阻塞的方式,也就是每個請求都分配一個程序或者執行緒。請求數只有 100 個時,這種方式自然沒問題,但增加到 10000 個請求時,10000 個程序或執行緒的排程、上下文切換乃至它們佔用的記憶體,都會成為瓶頸。

I/O 模型最佳化

檔案描述符使用套接字介面的時候,是要分配一個檔案描述符,然後後續所有的I/O都透過這個檔案描述符來操作(包括IO模型中要判斷可讀寫狀態)。

兩種 I/O 事件通知的方式:水平觸發和邊緣觸發,它們常用在套接字介面的檔案描述符中。

水平觸發:只要檔案描述符可以非阻塞地執行 I/O ,就會觸發通知。也就是說,應用程式可以隨時檢查檔案描述符的狀態,然後再根據狀態,進行 I/O 操作。邊緣觸發:只有在檔案描述符的狀態發生改變(也就是 I/O 請求達到)時,才傳送一次通知。這時候,應用程式需要儘可能多地執行 I/O,直到無法繼續讀寫,才可以停止。如果 I/O 沒執行完,或者因為某種原因沒來得及處理,那麼這次通知也就丟失了I/O 多路複用的方法使用非阻塞 I/O 和水平觸發通知,比如使用 select 或者 poll。

根據水平觸發原理,select 和 poll 需要從檔案描述符列表中,找出哪些可以執行 I/O ,然後進行真正的網路 I/O 讀寫。由於 I/O 是非阻塞的,一個執行緒中就可以同時監控一批套接字的檔案描述符,這樣就達到了單執行緒處理多請求的目的。這種方式的最大優點,是對應用程式比較友好,它的 API 非常簡單。

select 使用固定長度的位相量,表示檔案描述符的集合,因此會有最大描述符數量的限制。比如,在 32 位系統中,預設限制是 1024。並且,在 select 內部,檢查套接字狀態是用輪詢的方法,再加上應用軟體使用時的輪詢,就變成了一個 O(n^2) 的關係。

而 poll 改進了 select 的表示方法,換成了一個沒有固定長度的陣列,這樣就沒有了最大描述符數量的限制(當然還會受到系統檔案描述符限制)。但應用程式在使用 poll 時,同樣需要對檔案描述符列表進行輪詢,這樣,處理耗時跟描述符數量就是 O(N) 的關係。

除此之外,應用程式每次呼叫 select 和 poll 時,還需要把檔案描述符的集合,從使用者空間傳入核心空間,由核心修改後,再傳出到使用者空間中。這一來一回的核心空間與使用者空間切換,也增加了處理成本。

使用非阻塞 I/O 和邊緣觸發通知,比如 epoll。

epoll 使用紅黑樹,在核心中管理檔案描述符的集合,這樣,就不需要應用程式在每次操作時都傳入、傳出這個集合。

epoll 使用事件驅動的機制,只關注有 I/O 事件發生的檔案描述符,不需要輪詢掃描整個集合。

使用非同步 I/O(Asynchronous I/O,簡稱為 AIO)

非同步 I/O 允許應用程式同時發起很多 I/O 操作,而不用等待這些操作完成。而在 I/O 完成後,系統會用事件通知(比如訊號或者回調函式)的方式,告訴應用程式。這時,應用程式才會去查詢 I/O 操作的結果。

工作模型

使用 I/O 多路複用後,就可以在一個程序或執行緒中處理多個請求,其中,又有下面兩種不同的工作模型。

主程序 + 多個 worker 子程序,這也是最常用的一種模型

通用工作模式就是:

主程序執行 bind() + listen() 後,建立多個子程序在每個子程序中,都透過 accept() 或 epoll_wait() ,來處理相同的套接字。

比如,最常用的反向代理伺服器 Nginx 就是這麼工作的。它也是由主程序和多個 worker 程序組成。主程序主要用來初始化套接字,並管理子程序的生命週期;而 worker 程序,則負責實際的請求處理。

這些 worker 程序,實際上並不需要經常建立和銷燬,而是在沒任務時休眠,有任務時喚醒。只有在 worker 由於某些異常退出時,主程序才需要建立新的程序來代替它。(也可以用執行緒代替程序)

監聽到相同埠的多程序模型

在這種方式下,所有的程序都監聽相同的介面,並且開啟 SO_REUSEPORT 選項,由核心負責將請求負載均衡到這些監聽程序中去。

C1000K

從物理資源使用上來說,100 萬個請求需要大量的系統資源。比如,

假設每個請求需要 16KB 記憶體的話,那麼總共就需要大約 15 GB 記憶體。而從頻寬上來說,假設只有 20% 活躍連線,即使每個連線只需要 1KB/s 的吞吐量,總共也需要 1.6 Gb/s 的吞吐量。千兆網絡卡顯然滿足不了這麼大的吞吐量,所以還需要配置萬兆網絡卡,或者基於多網絡卡 Bonding 承載更大的吞吐量。

從軟體資源上來說,大量的連線也會佔用大量的軟體資源,比如檔案描述符的數量、連線狀態的跟蹤(CONNTRACK)、網路協議棧的快取大小(比如套接字讀寫快取、TCP 讀寫快取)等等。

大量請求帶來的中斷處理,也會帶來非常高的處理成本。 這樣,就需要多佇列網絡卡、中斷負載均衡、CPU 繫結、RPS/RFS(軟中斷負載均衡到多個 CPU 核上),以及將網路包的處理解除安裝(Offload)到網路裝置(如 TSO/GSO、LRO/GRO、VXLAN OFFLOAD)等各種硬體和軟體的最佳化。

C1000K 的解決方法,本質上還是構建在 epoll 的非阻塞 I/O 模型上。只不過,除了 I/O 模型之外,還需要從應用程式到 Linux 核心、再到 CPU、記憶體和網路等各個層次的深度最佳化,特別是需要藉助硬體,來解除安裝那些原來透過軟體處理的大量功能。

C10M

無論怎麼最佳化應用程式和核心中的各種網路引數,想實現 1000 萬請求的併發,都是極其困難的。

究其根本,還是 Linux 核心協議棧做了太多太繁重的工作。從網絡卡中斷帶來的硬中斷處理程式開始,到軟中斷中的各層網路協議處理,最後再到應用程式,這個路徑實在是太長了,就會導致網路包的處理最佳化,到了一定程度後,就無法更進一步了。

要解決這個問題,最重要就是跳過核心協議棧的冗長路徑,把網路包直接送到要處理的應用程式那裡去。這裡有兩種常見的機制,DPDK 和 XDP。

DPDK

DPDK是使用者態網路的標準。它跳過核心協議棧,直接由使用者態程序透過輪詢的方式,來處理網路接收。

DPDK 透過大頁、CPU 繫結、記憶體對齊、流水線併發等多種機制,最佳化網路包的處理效率。

XDP

XDP(eXpress Data Path),則是 Linux 核心提供的一種高效能網路資料路徑。它允許網路包,在進入核心協議棧之前,就進行處理,也可以帶來更高的效能。XDP 底層跟我們之前用到的 bcc-tools 一樣,都是基於 Linux 核心的 eBPF 機制實現的。

XDP 對核心的要求比較高,需要的是 Linux 4.8 以上版本,並且它也不提供快取佇列。基於 XDP 的應用程式通常是專用的網路應用,常見的有 IDS(入侵檢測系統)、DDoS 防禦、 cilium 容器網路外掛等。

總結

C10K 問題的根源,一方面在於系統有限的資源;另一方面,也是更重要的因素,是同步阻塞的 I/O 模型以及輪詢的套接字介面,限制了網路事件的處理效率。Linux 2.6 中引入的 epoll ,完美解決了 C10K 的問題,現在的高效能網路方案都基於 epoll。

從 C10K 到 C100K ,可能只需要增加系統的物理資源就可以滿足;但從 C100K 到 C1000K ,就不僅僅是增加物理資源就能解決的問題了。這時,就需要多方面的最佳化工作了,從硬體的中斷處理和網路功能解除安裝、到網路協議棧的檔案描述符數量、連線狀態跟蹤、快取佇列等核心的最佳化,再到應用程式的工作模型最佳化,都是考慮的重點。

再進一步,要實現 C10M ,就不只是增加物理資源,或者最佳化核心和應用程式可以解決的問題了。這時候,就需要用 XDP 的方式,在核心協議棧之前處理網路包;或者用 DPDK 直接跳過網路協議棧,在使用者空間透過輪詢的方式直接處理網路包。

28
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • Leetcode經典面試Java演算法73將矩陣置零