天天看點

Linux 【網絡】C10K 和 C1000K 回顧

文章目錄

  • ​​1. C10K​​
  • ​​2. I/O 模型優化​​
  • ​​3. 工作模型優化​​
  • ​​4. C1000K​​
  • ​​5. C10M​​
  • ​​6. 總結​​
  • ​​7. 讨論​​

回顧下經典的 C10K 和 C1000K 問題,以更好了解 Linux 網絡的工作原理,并進一步分析,如何做到單機支援 C10M,C10K 和 C1000K 的首字母 C 是 Client 的縮寫。C10K 就是單機同時處理 1 萬個請求(并發連接配接 1 萬)的問題,而 C1000K 也就是單機支援處理 100 萬個請求(并發連接配接 100 萬)的問題。

1. C10K

​​C10K​​ 問題最早由 Dan Kegel 在 1999 年提出。那時的伺服器還隻是 32 位系統,運作着 Linux 2.2 版本(後來又更新到了 2.4 和 2.6,而 2.6 才支援 x86_64),隻配置了很少的記憶體(2GB)和千兆網卡。怎麼在這樣的系統中支援并發 1 萬的請求呢?從資源上來說,對 2GB 記憶體和千兆網卡的伺服器來說,同時處理 10000 個請求,隻要每個請求處理占用不到 200KB(2GB/10000)的記憶體和 100Kbit (1000Mbit/10000)的網絡帶寬就可以。是以,實體資源是足夠的,接下來自然是軟體的問題,特别是網絡的 I/O 模型問題。

說到 I/O 的模型,我在檔案系統的原理中,曾經介紹過檔案 I/O,其實網絡 I/O 模型也類似。在 C10K 以前,Linux 中網絡處理都用同步阻塞的方式,也就是每個請求都配置設定一個程序或者線程。請求數隻有 100 個時,這種方式自然沒問題,但增加到 10000 個請求時,10000 個程序或線程的排程、上下文切換乃至它們占用的記憶體,都會成為瓶頸。既然每個請求配置設定一個線程的方式不合适,那麼,為了支援 10000 個并發請求,這裡就有兩個問題需要我們解決。

第一,怎樣在一個線程内處理多個請求,也就是要在一個線程内響應多個網絡 I/O。以前的同步阻塞方式下,一個線程隻能處理一個請求,到這裡不再适用,是不是可以用非阻塞 I/O 或者異步 I/O 來處理多個網絡請求呢?

第二,怎麼更節省資源地處理客戶請求,也就是要用更少的線程來服務這些請求。是不是可以繼續用原來的 100 個或者更少的線程,來服務現在的 10000 個請求呢?

2. I/O 模型優化

​異步​

​​、​

​非阻塞 I/O​

​ 的解決思路,你應該聽說過,其實就是我們在網絡程式設計中經常用到的 I/O 多路複用(I/O Multiplexing)。I/O 多路複用是什麼意思呢?

别急,詳細了解前,我先來講兩種 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 和 poll 時,需要對這些檔案描述符清單進行輪詢,這樣,請求數多的時候就會比較耗時。并且,select 和 poll 還有一些其他的限制。

select 使用固定長度的位相量,表示檔案描述符的集合,是以會有最大描述符數量的限制。比如,在 32 位系統中,預設限制是 1024。并且,在 select 内部,檢查套接字狀态是用輪詢的方法,處理耗時跟描述符數量是 O(N) 的關系。而 poll 改進了 select 的表示方法,換成了一個沒有固定長度的數組,這樣就沒有了最大描述符數量的限制(當然還會受到系統檔案描述符限制)。但應用程式在使用 poll 時,同樣需要對檔案描述符清單進行輪詢,這樣,處理耗時跟描述符數量就是 O(N) 的關系。

除此之外,應用程式每次調用 select 和 poll 時,還需要把檔案描述符的集合,從使用者空間傳入核心空間,由核心修改後,再傳出到使用者空間中。這一來一回的核心空間與使用者空間切換,也增加了處理成本。有沒有什麼更好的方式來處理呢?答案自然是肯定的。

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

既然 select 和 poll 有那麼多的問題,就需要繼續對其進行優化,而 epoll 就很好地解決了這些問題。

  • epoll 使用紅黑樹,在核心中管理檔案描述符的集合,這樣,就不需要應用程式在每次操作時都傳入、傳出這個集合。
  • epoll 使用事件驅動的機制,隻關注有 I/O 事件發生的檔案描述符,不需要輪詢掃描整個集合。

不過要注意,epoll 是在 ​

​Linux 2.6​

​ 中才新增的功能(2.4 雖然也有,但功能不完善)。由于邊緣觸發隻在檔案描述符可讀或可寫事件發生時才通知,那麼應用程式就需要盡可能多地執行 I/O,并要處理更多的異常事件。

**第三種,使用異步 I/O(Asynchronous I/O,簡稱為 AIO)。**在前面檔案系統原理的内容中,我曾介紹過異步 I/O 與同步 I/O 的差別。異步 I/O 允許應用程式同時發起很多 I/O 操作,而不用等待這些操作完成。而在 I/O 完成後,系統會用事件通知(比如信号或者回調函數)的方式,告訴應用程式。這時,應用程式才會去查詢 I/O 操作的結果。異步 I/O 也是到了 Linux 2.6 才支援的功能,并且在很長時間裡都處于不完善的狀态,比如 glibc 提供的異步 I/O 庫,就一直被社群诟病。同時,由于異步 I/O 跟我們的直覺邏輯不太一樣,想要使用的話,一定要小心設計,其使用難度比較高。

3. 工作模型優化

解了 I/O 模型後,請求處理的優化就比較直覺了。使用 I/O 多路複用後,就可以在一個程序或線程中處理多個請求,其中,又有下面兩種不同的工作模型。

第一種,主程序 + 多個 worker 子程序,這也是最常用的一種模型。這種方法的一個通用工作模式就是:

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

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

Linux 【網絡】C10K 和 C1000K 回顧

這裡要注意,accept() 和 epoll_wait() 調用,還存在一個驚群的問題。換句話說,當網絡 I/O 事件發生時,多個程序被同時喚醒,但實際上隻有一個程序來響應這個事件,其他被喚醒的程序都會重新休眠。

  • 其中,accept() 的驚群問題,已經在 Linux 2.6 中解決了;
  • 而 epoll 的問題,到了 Linux 4.5 ,才通過 EPOLLEXCLUSIVE 解決。

為了避免驚群問題, Nginx 在每個 worker 程序中,都增加一個了全局鎖(accept_mutex)。這些 worker 程序需要首先競争到鎖,隻有競争到鎖的程序,才會加入到 epoll 中,這樣就確定隻有一個 worker 子程序被喚醒。

不過,根據前面 CPU 子產品的學習,你應該還記得,程序的管理、排程、上下文切換的成本非常高。那為什麼使用多程序模式的 Nginx ,卻具有非常好的性能呢?這裡最主要的一個原因就是,這些 worker 程序,實際上并不需要經常建立和銷毀,而是在沒任務時休眠,有任務時喚醒。隻有在 worker 由于某些異常退出時,主程序才需要建立新的程序來代替它。

當然,你也可以用線程代替程序:主線程負責套接字初始化和子線程狀态的管理,而子線程則負責實際的請求處理。由于線程的排程和切換成本比較低,實際上你可以進一步把 epoll_wait() 都放到主線程中,保證每次事件都隻喚醒主線程,而子線程隻需要負責後續的請求處理。

第二種,監聽到相同端口的多程序模型。在這種方式下,所有的程序都監聽相同的接口,并且開啟 SO_REUSEPORT 選項,由核心負責将請求負載均衡到這些監聽程序中去。這一過程如下圖所示。

Linux 【網絡】C10K 和 C1000K 回顧

由于核心確定了隻有一個程序被喚醒,就不會出現驚群問題了。比如,Nginx 在 1.9.1 中就已經支援了這種模式。

Linux 【網絡】C10K 和 C1000K 回顧

不過要注意,想要使用 SO_REUSEPORT 選項,需要用 Linux 3.9 以上的版本才可以。

4. C1000K

基于 I/O 多路複用和請求處理的優化,C10K 問題很容易就可以解決。不過,随着摩爾定律帶來的伺服器性能提升,以及網際網路的普及,你并不難想到,新興服務會對性能提出更高的要求。很快,原來的 C10K 已經不能滿足需求,是以又有了 C100K 和 C1000K,也就是并發從原來的 1 萬增加到 10 萬、乃至 100 萬。從 1 萬到 10 萬,其實還是基于 C10K 的這些理論,epoll 配合線程池,再加上 CPU、記憶體和網絡接口的性能和容量提升。大部分情況下,C100K 很自然就可以達到。那麼,再進一步,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、記憶體和網絡等各個層次的深度優化,特别是需要借助硬體,來解除安裝那些原來通過軟體處理的大量功能。

5. C10M

究其根本,還是 Linux 核心協定棧做了太多太繁重的工作。從網卡中斷帶來的硬中斷處理程式開始,到軟中斷中的各層網絡協定處理,最後再到應用程式,這個路徑實在是太長了,就會導緻網絡包的處理優化,到了一定程度後,就無法更進一步了。要解決這個問題,最重要就是跳過核心協定棧的冗長路徑,把網絡包直接送到要處理的應用程式那裡去。

這裡有兩種常見的機制,DPDK 和 XDP。

第一種機制,DPDK,是使用者态網絡的标準。它跳過核心協定棧,直接由使用者态程序通過輪詢的方式,來處理網絡接收。

Linux 【網絡】C10K 和 C1000K 回顧

說起輪詢,你肯定會下意識認為它是低效的象征,但是進一步反問下自己,它的低效主要展現在哪裡呢?是查詢時間明顯多于實際工作時間的情況下吧!那麼,換個角度來想,如果每時每刻都有新的網絡包需要處理,輪詢的優勢就很明顯了。比如:

  • 在 PPS 非常高的場景中,查詢時間比實際工作時間少了很多,絕大部分時間都在處理網絡包;
  • 而跳過核心協定棧後,就省去了繁雜的硬中斷、軟中斷再到 Linux網絡協定棧逐層處理的過程,應用程式可以針對應用的實際場景,有針對性地優化網絡包的處理邏輯,而不需要關注所有的細節。

此外,DPDK 還通過大頁、CPU 綁定、記憶體對齊、流水線并發等多種機制,優化網絡包的處理效率。

第二種機制,XDP(eXpress Data Path),則是 Linux 核心提供的一種高性能網絡資料路徑。它允許網絡包,在進入核心協定棧之前,就進行處理,也可以帶來更高的性能。XDP 底層跟我們之前用到的 ​

​bcc-tools​

​ 一樣,都是基于 Linux 核心的 eBPF 機制實作的。

XDP 的原理如下圖所示:

Linux 【網絡】C10K 和 C1000K 回顧

你可以看到,XDP 對核心的要求比較高,需要的是 ​​​Linux 4.8 以上版本​​​,并且它也不提供緩存隊列。基于 XDP 的應用程式通常是專用的網絡應用,常見的有 IDS(入侵檢測系統)、DDoS 防禦、 ​​cilium​​ 容器網絡插件等。

6. 總結

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

從 C10K 到 C100K ,可能隻需要增加系統的實體資源就可以滿足;但從 C100K 到 C1000K ,就不僅僅是增加實體資源就能解決的問題了。這時,就需要多方面的優化工作了,從硬體的中斷處理和網絡功能解除安裝、到網絡協定棧的檔案描述符數量、連接配接狀态跟蹤、緩存隊列等核心的優化,再到應用程式的工作模型優化,都是考慮的重點。

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

7. 讨論

繼續閱讀