問題
1. C10K 就是單機同時處理 1 萬個請求(並發連接 1 萬)的問題
2. C1000K 也就是單機支持處理 100 萬個請求(並發連接 100 萬)的問題
C10K I/O 模型
兩種 I/O 事件通知的方式:
水平觸發和邊緣觸發
(1) 水平觸發:只要文件描述符可以非阻塞地執行 I/O ,就會觸發通知。
應用程序持續檢查文件描述符的狀態,根據狀態進行 I/O 操作。
(2) 邊緣觸發:只有在文件描述符的狀態發生改變(也就是 I/O 請求達到)時,才發送一次通知。
應用程序需要盡可能多地執行 I/O,直到無法繼續讀寫,才可以停止。如果 I/O 沒執行完,那么這次通知也就丟失了。
I/O 多路復用的方法1:
使用非阻塞 I/O 和水平觸發通知,比如使用 select 或者 poll。
(1) select 使用固定長度的位相量,會有最大描述符數量的限制,默認限制是 1024。
檢查套接字狀態是用輪詢,再加上應用軟件使用時的輪詢,復雜度是O(n平方)
(2) poll 換成了一個沒有固定長度的數組,沒有最大描述符數量的限制。
同樣需要對文件描述符列表進行輪詢,復雜度是O(n)
(3) 每次調用 select 和 poll 時,還需要把文件描述符的集合,從用戶空間傳入內核空間,由內核修改后,再傳出到用戶空間。
這一來一回的切換,也增加了處理成本。
I/O 多路復用的方法2:
使用非阻塞 I/O 和邊緣觸發通知,比如epoll。
(1) epoll 使用紅黑樹,在內核中管理文件描述符的集合,不需要應用程序在每次操作時傳入、傳出這個集合.
(2) epoll 使用事件驅動的機制,只關注有 I/O 事件發生的文件描述符,不需要輪詢掃描整個集合。復雜度是O(1)
I/O 多路復用的方法3:
使用異步 I/O, 比如glibc 提供的異步 I/O 庫
(1) 異步 I/O 允許應用程序同時發起很多 I/O 操作
(2) I/O 完成后,系統會用事件通知的方式,告訴應用程序。應用程序才會去查詢 I/O 操作的結果。
I/O 多路復用下的工作模型
主進程 + 多個 worker 子進程
(1) 主進程執行 bind() + listen() 后,創建多個子進程;
(2) 每個子進程都 accept() 或epoll_wait() ,來處理相同的套接字。
這里要注意,accept() 和 epoll_wait() 調用,還存在一個驚群的問題。換句話說,當網絡 I/O 事件發生時,多個進程被同時喚醒,但實際上只有一個進程來響應這個事件,其他被喚醒的進程都會重新休眠。
其中,accept() 的驚群問題,已經在 Linux 2.6 中解決了;
而 epoll 的問題,到了 Linux 4.5 ,才通過 EPOLLEXCLUSIVE 解決。
解決方案:
為了避免驚群問題, Nginx 在每個 worker 進程中,都增加一個了全局鎖(accept_mutex)。這些 worker 進程需要首先競爭到鎖,只有競爭到鎖的進程,才會加入到 epoll 中,這樣就確保只有一個 worker 子進程被喚醒
監聽到相同端口的多進程模型
在這種方式下,所有的進程都監聽相同的接口,並且開啟 SO_REUSEPORT 選項,由內核負責將請求負載均衡到這些監聽進程中去
由於內核確保了只有一個進程被喚醒,就不會出現驚群問題了
C1000K
C1000K 的解決方法,本質上還是構建在 epoll 的非阻塞 I/O 模型上。只不過,除了 I/O 模型之外,還需要從應用程序到 Linux 內核、再到 CPU、內存和網絡等各個層次的深度優化,特別是需要借助硬件,來卸載那些原來通過軟件處理的大量功能。
C10M
原因:
在 C1000K 問題中,各種軟件、硬件的優化很可能都已經做到頭了。特別是當升級完硬件(比如足夠多的內存、帶寬足夠大的網卡、更多的網絡功能卸載等)后,你可能會發現,無論你怎么優化應用程序和內核中的各種網絡參數,想實現 1000 萬請求的並發,都是極其困難的。
究其根本,還是 Linux 內核協議棧做了太多太繁重的工作。從網卡中斷帶來的硬中斷處理程序開始,到軟中斷中的各層網絡協議處理,最后再到應用程序,這個路徑實在是太長了,就會導致網絡包的處理優化,到了一定程度后,就無法更進一步了
要解決這個問題,最重要就是跳過內核協議棧的冗長路徑,把網絡包直接送到要處理的應用程序那里去。這里有兩種常見的機制,DPDK 和 XDP
第一種機制,DPDK
是用戶態網絡的標准。它跳過內核協議棧,直接由用戶態進程通過輪詢的方式,來處理網絡接收。
第二種機制,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 直接跳過網絡協議棧,在用戶空間通過輪詢的方式直接處理網絡包
當然了,實際上,在大多數場景中,我們並不需要單機並發 1000 萬的請求。通過調整系統架構,把這些請求分發到多台服務器中來處理,通常是更簡單和更容易擴展的方案
select/poll是LT模式,epoll缺省使用的也是水平觸發模式(LT)。
目前業界對於ET的最佳實踐大概就是Nginx了,單線程redis也是使用的LT
說下我對水平觸發(LT)和邊緣觸發(ET)我的理解。
LT:文件描述符准備就緒時(FD關聯的讀緩沖區不為空,可讀。寫緩沖區還沒滿,可寫),觸發通知。
也就是你文中表述的"只要文件描述符可以非阻塞地執行 I/O ,就會觸發通知..."
ET:當FD關聯的緩沖區發生變化時(例如:讀緩沖區由空變為非空,有新數據達到,可讀。寫緩沖區滿變有空間了,有數據被發送走,可寫),觸發通知,僅此一次
也就是你文中表述的"只有在文件描述符的狀態發生改變(也就是 I/O 請求達到)時"