Redis技術知識總結之七——Redis多路複用機制

接上篇《Redis技術知識總結之六——Redis持久化機制》

七. Redis 多路複用機制

參考地址:《Redis IO多路複用技術以及epoll實現原理》

redis 是一個單線程卻性能非常好的內存數據庫, 主要用來作爲緩存系統。 redis 採用網絡IO多路複用技術來保證在多連接的時候, 系統的高吞吐量。
爲什麼 Redis 中要使用 I/O 多路複用這種技術呢?
首先,Redis 是跑在單線程中的,所有的操作都是按照順序線性執行的,但是由於讀寫操作等待用戶輸入或輸出都是阻塞的,所以 I/O 操作在一般情況下往往不能直接返回,這會導致某一文件的 I/O 阻塞導致整個進程無法對其它客戶提供服務,而 I/O 多路複用就是爲了解決這個問題而出現的。
redis的io模型主要是基於epoll實現的,不過它也提供了 select和kqueue的實現,默認採用epoll。
那麼epoll到底是個什麼東西呢? 其實只是衆多i/o多路複用技術當中的一種而已,但是相比其他io多路複用技術(select, poll等等)。

7.1 epoll 與 select/poll 的區別

select,poll,epoll都是IO多路複用的機制。I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒,能夠通知程序進行相應的操作。
select 的本質是採用 32 個整數的 32 位,即 3232= 1024 來標識,fd值爲 1-1024。當 fd 的值超過 1024 限制時,就必須修改 FD_SETSIZE 的大小。這個時候就可以標識32max 值範圍的 fd。
poll 與 select 不同,通過一個 pollfd 數組向內核傳遞需要關注的事件,故沒有描述符個數的限制,pollfd 中的 events 字段和 revents 分別用於標識關注的事件和發生的事件,故 pollfd 數組只需要被初始化一次。
epoll 還是 poll 的一種優化,返回後不需要對所有的 fd 進行遍歷,在內核中維持了 fd 的列表。select 和 poll 是將這個內核列表維持在用戶態,然後傳遞到內核中;而與 poll/select 不同,epoll 不再是一個單獨的系統調用,而是由 epoll_create/epoll_ctl/epoll_wait 三個系統調用組成,後面將會看到這樣做的好處。

注:epoll 在 2.6 以後的內核才支持。

7.2 Epoll 的優點

epoll 有諸多優點

  1. epoll 沒有最大併發連接的限制,上限是最大可以打開文件的數目,這個數字一般遠大於 2048, 一般來說這個數目和系統內存關係很大 ,具體數目可以 cat /proc/sys/fs/file-max 察看。
  2. 效率提升, Epoll 最大的優點就在於它只管你“活躍”的連接 ,而跟連接總數無關,因此 IO 效率不隨 FD 數目增加而線性下降,在實際的網絡環境中, Epoll 的效率就會遠遠高於 select 和 poll 。
  3. 內存拷貝, Epoll 在這點上使用了“共享內存 ”,這個內存拷貝也省略了。
    • Epoll 使用了 mmap 加速內核與用戶空間的消息傳遞。這點涉及了 epoll 的具體實現。無論是select, poll,還是 epoll,都需要內核把 FD 消息通知給用戶空間,如何避免不必要的內存拷貝就很 重要。在這點上,Epoll 是通過內核與用戶空間 mmap 同一塊內存實現的。

select/poll的幾大缺點

  1. 每次調用 select/poll,都需要把 fd 集合從用戶態拷貝到內核態,這個開銷在 fd 很多的時候會很大;
  2. 同時每次調用 select/poll 都需要在內核遍歷傳遞進來的所有 fd,這個開銷在 fd 很多時也很大;
  3. 針對 select 支持的文件描述符數量太小了,默認是 1024;
  4. select 返回的是含有整個句柄的數組,應用程序需要遍歷整個數組才能發現哪些句柄發生了事件;
  5. select 的觸發方式是水平觸發,應用程序如果沒有完成對一個已經就緒的文件描述符進行 IO 操作,那麼之後每次 select 調用還是會將這些文件描述符通知進程。

相比 select模型,poll使用鏈表保存文件描述符,因此沒有了監視文件數量的限制,但其他三個缺點依然存在。

7.3 epoll IO 多路複用模型實現機制

由於 epoll 的實現機制與 select/poll 機制完全不同,上面所說的 select 的缺點在 epoll 上不復存在。
Epoll 沒有這個限制,它所支持的 FD 上限是最大可以打開文件的數目,這個數字一般遠大於 2048。舉個例子,在 1GB 內存的機器上大約是 10萬左右,設想一下如下場景:有 100 萬個客戶端同時與一個服務器進程保持着 TCP 連接。而每一時刻,通常只有幾百上千個 TCP 連接是活躍的(事實上大部分場景都是這種情況)。如何實現這樣的高併發?
select/poll 時代,主要實現方式是從用戶態複製句柄數據結構到內核態。服務器進程每次都把這 100 萬個連接告訴操作系統,讓操作系統內核去查詢這些套接字上是否有事件發生。輪詢完後,再將句柄數據複製到用戶態,讓服務器應用程序輪詢處理已發生的網絡事件,這一過程資源消耗較大,因此,select/poll一般只能處理幾千的併發連接。
此外,如果沒有 I/O 事件產生,我們的程序就會阻塞在 select 處。但是依然有個問題,我們從 select 那裏僅僅知道了,有 I/O 事件發生了,但卻並不知道是那幾個流(可能有一個,多個,甚至全部),我們只能無差別輪詢所有流,找出能讀出數據,或者寫入數據的流,對他們進行操作。但是使用 select,我們有 O(n) 的無差別輪詢複雜度,同時處理的流越多,每一次無差別輪詢時間就越長。
Epoll 的設計和實現與 select 完全不同。Epoll 通過在 Linux 內核中申請一個簡易的文件系統(文件系統一般用 B+樹實現),把原先的 select/poll 調用分成了3個部分:

  1. epoll_create():建立一個 epoll對象(在 Epoll 文件系統中,爲這個句柄對象分配資源);
  2. epoll_ctl():向 epoll 對象中添加這100萬個連接的套接字;
  3. epoll_wait():收集發生的事件的連接;

如此一來,要實現上面所說的場景,只需要在進程啓動時建立一個 epoll 對象,然後在需要的時候向這個 epoll 對象中添加或者刪除連接。同時,epoll_wait 的效率也非常高,因爲調用 epoll_wait 時,並沒有一股腦的向操作系統複製這100萬個連接的句柄數據,內核也不需要去遍歷全部的連接。

7.4 Epoll 底層實現

底層實現:
當某一進程調用 epoll_create 方法時,Linux 內核會創建一個 eventpoll 結構體,這個結構體中有兩個成員與 epoll 的使用方式密切相關。eventpoll 結構體如下所示:

struct eventpoll {
  //....
  // 紅黑樹的根節點,這棵樹中存儲着所有添加到 epoll 中的需要監控的事件
  struct rb_root rbr;
  // 雙鏈表中存放着將要通過 epoll_wait 返回給用戶的滿足條件的事件
  struct list_head rdlist;
  //....
}

7.4.1 socket 紅黑樹

每一個 epoll 對象都有一個獨立的 eventpoll 結構體,用於存放通過 epoll_ctl 方法向 epoll 對象中添加進來的事件,這些事件都會掛載在用於存儲上述的被監控 socket 的紅黑樹上,即上面源碼的 rb_root。當你調用 epoll_create 時,就會在 epoll 註冊的一個文件系統中創建一個 file 節點,這個 file 不是普通文件,它只服務於 epoll。epoll 在被內核初始化時(操作系統啓動),同時會開闢出 epoll 自己的內核高速緩存區,用於安置每一個我們想監控的 socket,這些 socket 會以紅黑樹的形式保存在內核緩存裏,紅黑樹的插入時間效率很高,對於高度爲 n 的紅黑樹,查找、插入、刪除的效率都是 lgn。如此重複添加的事件就可以通過紅黑樹高效的識別出來。

  • 注:這個內核高速緩存區,就是建立連續的物理內存頁,然後在之上建立 slab 層,簡單的說,就是物理上分配好你想要的 size 的內存對象,每次使用時都是使用空閒的已分配好的對象。

7.4.2 事件雙鏈表

所有添加到 epoll 中的事件都會與設備(網卡)驅動程序**建立回調關係,也就是說當相應的事件發生時,會調用這個回調方法。這個回調方法在內核中叫 ep_poll_callback,它會將發生的事件添加到 rdlist 雙鏈表中。
這個事件雙鏈表是怎麼維護的呢?當我們執行 epoll_ctl 時,除了把 socket 放到 epoll 文件系統裏 file 對象對應的紅黑樹上之外,還會
給內核中斷處理程序註冊一個回調函數**。告訴內核,如果這個句柄的中斷到了,就把它放到準備就緒 list 鏈表裏。所以,當一個 socket 上有數據到了,內核在把網卡上的數據 copy 到內核中,然後就把 socket 插入到準備就緒鏈表裏了。由此可見,epoll 的基礎就是回調
epoll 的每一個事件都會包含一個 epitem 結構體,如下所示:

struct epitem {
  // 紅黑樹節點
  struct rb_node rbn; 
  // 雙向鏈表節點
  struct list_head rdllink;
  // 事件句柄信息
  struct epoll_filefd ffd;
  // 指向所屬的 eventpoll 對象
  struct eventpoll *ep;
  // 期待發生的事件類型
  struct epoll_event event;
}

當調用 epoll_wait 檢查是否有事件發生時,只需要檢查 eventpoll 對象中的 rdlist 雙鏈表中是否有 epitem 元素即可。如果 rdlist 不爲空,則把發生的事件複製到用戶態,同時將事件數量返回給用戶。


綜上所述,epoll 的執行過程:

  1. 調用 epoll_create 時,內核幫我們在 epoll 文件系統裏建立 file 結點內核緩存中建立 socket 紅黑樹,除此之外,還會再建立一個用於存儲準備就緒事件的 list 鏈表
  2. 執行 epoll_ctl 時,如果增加就緒事件的 socket 句柄,則需要:
    • 檢查在紅黑樹中是否存在,存在立即返回,不存在則添加到樹幹上;
    • 然後向內核註冊回調函數,用於當中斷事件來臨時向準備就緒鏈表中插入數據。
  3. epoll_wait 調用時,僅僅觀察這個 list 鏈表裏有沒有數據即可,有數據就返回,沒有數據就 sleep,等到 timeout 時間到後,即使鏈表沒數據也返回。
  • epoll_wait 的執行過程相當於以往調用 select/poll,但 epoll 的效率高得多。

注:
epoll 獨有的兩種模式 LT 和 ET。無論是 LT 和 ET 模式,都適用於以上所說的流程。區別是,LT 模式下只要一個句柄上的事件一次沒有處理完,會在以後調用 epoll_wait 時次次返回這個句柄。而ET模式僅在第一次返回。
關於 LT 和 ET 有一端描述,LT 和 ET 都是電子裏面的術語,ET 是邊緣觸發,LT 是水平觸發,一個表示只有在變化的邊際觸發,一個表示在某個階段都會觸發。
對於 epoll 而言,當一個 socket 句柄上有事件時,內核會把該句柄插入上面所說的準備就緒鏈表,這時我們調用 epoll_wait,會把準備就緒的 socket 拷貝到用戶態內存,然後清空準備就緒鏈表。最後,epoll_wait 檢查這些 socket,如果不是 ET 模式(就是LT模式的句柄了),並且這些 socket 上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了。所以,非 ET 的句柄,只要它上面還有事件,epoll_wait 每次都會返回這個句柄。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章