epoll 與 Redis 單線程架構

這篇筆記記錄了 epoll 的基本概念,以及它如何幫助單線程的 Redis 達到極高的效能。

什麼是 epoll?

epoll 是 Linux 核心提供的一種 I/O 多工(I/O Multiplexing)機制。它主要用來解決傳統 selectpoll 效能瓶頸的問題。

在傳統的 select / poll 中,每當檢查是否有事件發生時,必須將所有的檔案描述符(File Descriptor, FD)全部輪詢(O(N) 複雜度)一遍。當連線數非常巨大時,這個輪詢的開銷會變得非常可觀。

補充:什麼是檔案描述符(File Descriptor, FD)?

在 Linux 作業系統中,「一切皆檔案(Everything is a file)」。不管是普通的文字檔、目錄,還是網路連線(Socket)、硬體設備,作業系統都把它們當作「檔案」來處理。

當一個程式開啟一個檔案或建立一個網路連線時,作業系統核心會回傳一個非負整數(例如:3, 4, 5…)給這個程式。這個數字就是「檔案描述符(FD)」。它就像是這個檔案或連線的「身分證字號」或是「號碼牌」。

生活例子:

想像你是一間餐廳的服務生,作業系統是帶位櫃檯。 當有新客人(一個網路連線請求)進來時,櫃檯會給這桌客人分配一個「桌號」(例如:桌號 3)。 這個「桌號 3」就是 FD。之後你(應用程式)要幫這桌客人點餐或上菜(讀寫資料),都是透過告訴櫃檯:「我要處理『桌號 3』的餐點」,而不是告訴櫃檯客人的名字。

因此,在網路通訊的語境中,所謂的「輪詢所有的檔案描述符」,就是在輪詢「所有正在連線中的 Socket」,看看哪一條連線有新的資料傳進來。

epoll 改變了這個作法:

  • 它基於事件驅動:只有當某些 FD 狀態改變(例如有資料可讀或可寫)時,核心才會主動將這些就緒的 FD 加入到一個就緒清單中。
  • 當應用程式呼叫 epoll_wait 時,只需要獲取這些就緒的 FD,而不需要遍歷所有的 FD(O(1) 複雜度)。這讓它在面對大量連線(C10K 問題)時依舊能保持優異的效能。

補充:什麼是 C10K 問題?

C10K 代表 “Client 10,000”(一萬個客戶端並發連線)。這是在 1999 年由 Dan Kegel 提出的一個知名網路伺服器效能瓶頸問題。 在早期的網路架構與作業系統(使用 selectpoll)中,當伺服器同時處理的網路連線數達到 10,000 個時,因為 O(N) 的輪詢開銷與大量的執行緒/行程建立,伺服器的 CPU 資源會被耗費在無意義的等待與切換上,導致系統效能急遽下降,無法回應請求。

epoll 的出現,將獲取活躍連線的複雜度降到 O(1),正是 Linux 能夠跨越 C10K 門檻(甚至邁向 C10M)的關鍵技術。

簡單來說,select 像是老師每次點名都要把全班名單從頭到尾念一遍問「誰有問題?」;而 epoll 像是老師只看舉手的學生,直接處理他們的問題。

為什麼 Redis 單線程可以如此快?

Redis 主要依賴「單線程(Single-threaded)」來處理命令,卻能達到每秒數萬甚至十萬級別的 QPS。這歸功於以下幾個核心設計:

  1. 完全基於記憶體操作:絕大部分的資料讀寫都是在記憶體中進行,這是 Redis 效能如此高的根本原因。記憶體的操作速度遠快於磁碟 I/O。
  2. 優秀的資料結構:Redis 底層設計了許多高效的資料結構(如 Hash, SkipList 等),讓各種操作的時間複雜度降到最低。
  3. 避免了執行緒切換開銷與競爭:單線程架構避免了多執行緒間頻繁的 Context Switch 開銷,同時也不需要處理複雜的鎖(Lock)問題,排除了死結或競態條件造成的效能損耗。
  4. 非阻塞 I/O 多工模型:也就是 Redis 廣泛使用的 epoll(在 Linux 環境下),讓單線程能同時處理數以萬計的並發連線。

[!IMPORTANT] Redis 的「單線程」指的是網路請求解析與資料讀寫的執行由單一主執行緒負責。在較新的版本(如 Redis 6.0 之後),已經引入了多執行緒來處理網路 I/O 的讀寫,但核心的指令執行依然是單線程的。

Redis 使用 epoll 解決了什麼問題?

如果沒有 epoll 等 I/O 多工機制,Redis 的單線程如果去讀取一個還沒有資料傳來的網路 Socket,就會被阻塞(Blocked)。一旦主執行緒被阻塞,其他所有客戶端的請求也就無法處理了。

Redis 使用 epoll 解決了以下問題:

  1. 解決阻塞問題:透過 epoll,Redis 可以將所有的 Socket 交給作業系統監控。主執行緒可以專心執行指令,當有 Socket 準備好可讀或可寫時,epoll 會通知 Redis 進行後續處理。這樣一來,單線程就不會因為等待某個連線的資料而被卡死。
  2. 高效處理海量連線:面對成千上萬的連線,epoll 能夠極為快速地篩選出活躍(有資料進出)的連線,讓 Redis 可以集中 CPU 資源去處理這些真實的請求,而不是浪費時間在無效的連線輪詢上。

總結來說,epoll 作為底層的網路事件多路復用引擎,是 Redis 得以在單線程架構下依然能支撐極高併發的核心關鍵。


This site uses Just the Docs, a documentation theme for Jekyll.