redis的事件處理機制

軒脈刃發表於2022-03-24

redis的事件處理機制

redis是單程式,單執行緒模型,與nginx的多程式不同,與golang的多協程也不同,“工作的工人”那麼少,可那麼為什麼redis能這麼快呢?

epoll多路複用

這裡重點要說的就是redis的IO程式設計模型,首先了解下

為什麼要有多路複用呢?

如果沒有多路複用,一個執行緒只能監聽一個埠的一個連線,這樣這個效率比較低。當然我們有幾種辦法可以破除這個,一個是使用多執行緒模型,我們還是監聽一個埠,但是一個請求進來,我們為其建立一個執行緒。但是這種消耗是比較大的。所以我們一直想辦法,有沒有辦法一個執行緒監聽多個埠,或者多個一個埠的多個連線(fd)。

這裡再說說fd, 檔案描述符(file descriptor)是核心為了高效管理已被開啟的檔案所建立的索引,其是一個非負整數(通常是小整數),用於指代被開啟的檔案,所有執行I/O操作(包括網路socket操作)的系統呼叫都通過檔案描述符。每個連線請求上來,都會建立一個連線套接字,一個連線使用一個連線套接字。

對於監聽埠,我們會有一個監聽套接字,對應監聽fd。我們所有的監聽業務都是從監聽這個套接字開始的。

那麼如果我一個程式能同時監聽多個連線套接字,是不是就很讚了。是的,這就是linux的io多路複用邏輯。但是這麼多連線套接字,傳遞資料等是斷斷續續的,A連線接收一個包,B連線再接收一個包,A連線再接收一個包,B連線再接收一個包....如果我等著A連線把包都接收完再處理B,那效率是非常慢的。所以,這裡我們就需要有一個通知機制,讓有收到包的時候通知下處理執行緒。

linux的IO多路複用邏輯主要有三種:select, poll, epoll。

select

select模型監聽的三個事件:讀資料事件,寫資料事件,異常事件。

使用select模型的步驟如下:

  • 我們確定要監聽的監聽fd列表
  • 呼叫select監聽所有監聽fd,阻塞執行緒。
  • select只有當有事件出現並且有事件的fd已經等待完畢
  • 如果是建立一個連線事件:
    • 建立一個連線套接字,連線fd
    • 將連線fd和監聽fd集合放在一起
  • 如果是一個讀寫事件:
    • 遍歷所有fd,判斷是否是準備好的fd
    • 如果是準備好的fd,進行業務讀寫邏輯
  • 迴圈進入select。

select一次可以監聽1024個檔案描述符。

poll模型

poll傳遞給核心的是:

  • 監聽的fd集合
  • 需要監聽的事件型別
  • 實際發生的事件型別

poll的模型邏輯是:

  • 我們確定要監聽的監聽fd列表
  • 呼叫poll監聽所有監聽fd,阻塞執行緒。
  • poll只有當有事件出現才解除阻塞
  • 如果是建立一個連線事件:
    • 建立一個連線套接字,連線fd
    • 將連線fd和監聽fd集合放在一起
  • 如果是一個讀寫事件:
    • 遍歷所有fd,判斷是否是有讀寫事件的fd
    • 如果fd有讀寫事件,進行業務讀寫邏輯
  • 迴圈進入poll。

poll比select優秀在它沒有了1024的限制了。但是還是有一些缺陷,就是必須要遍歷所有fd。

epoll

epoll的資料結構類似poll,但是在呼叫epoll的時候,它不是返回發生了事件的fd個數,而是返回了所有發生的事件,這個事件中可以查出發生事件的fd。

所以epoll的邏輯模型是:

  • 我們確定要監聽的監聽fd列表
  • 呼叫epoll監聽所有監聽fd,阻塞執行緒。
  • epoll只有當有事件出現才解除阻塞,並且返回事件列表
  • 遍歷事件列表:
  • 如果是建立一個連線事件:
    • 建立一個連線套接字,連線fd
    • 將連線fd和監聽fd集合放在一起,繼續epoll
  • 如果是一個讀寫事件:
    • 處理這個事件
  • 迴圈進入epoll。

說白了,epoll就是我們邏輯上能想到的最優的通知機制。一群人去排隊,有多個事件發生,警察來了,那麼就告訴警察有哪幾個列發生了什麼事件,警察一個個處理就行了。

Reactor模型

有了IO多路複用的機制,我們就可以實現一種模型,叫做Reactor模型了。Reactor模型的理論基礎起源於這篇論文:

https://www.dre.vanderbilt.edu/~schmidt/PDF/reactor-siemens.pdf

裡面最經典的一張圖就是這個

image-20220308091039775

reactor的五大角色:

  • Handle(控制程式碼或者是描述符)
  • Synchronous Event Demultiplexer(同步事件分離器)
  • Event Handler(事件處理器)
  • Concrete Event Handler(具體事件處理器)
  • Initiation Dispatcher(初始分發器)

簡要來說,Reactor就是我們現在最正常理解的“事件驅動”,對,就是字面理解的那種。比如訂閱一個kafka,我們會建立一個監聽程式,監聽kafka的某個topic,然後在監聽程式中掛載幾個處理不同訊息的處理程式,每當有一個事件從topic進入的時候,我們就會有通過這個監聽程式,通知我們的處理程式。處理程式來處理不同的訊息。

這種所謂的通知機制,就叫做reactor。

這個kafka的例子,裡面有一個監聽事件的程式,它一定是一個同步的,一條訊息來了,投遞一個訊息,就叫做 Synchronous Event Demultiplexer(同步事件分離器)。而這個訊息,就是Handle(控制程式碼或者是描述符)。我們需要將某個具體的事件處理函式,也就是上圖的Concrete Event Handler(具體事件處理器) 掛載到監聽的處理程式中。當然這裡的每個Concrete Event Handler(具體事件處理器) 都必須遵照某種格式,比如定義了handle_event和get_handle介面。這種格式我們統稱為Event Handler(事件處理器)。再回到監聽事件,監聽事件一定有一個掛載的具體map之類的結構,即哪個事件對應哪個處理程式,這個掛載的核心我們叫它Initiation Dispatcher(初始分發器)。

標準的處理流程描述如下:

  • 當應用向Initiation Dispatcher註冊Concrete Event Handler時,應用會標識出該事件處理器希望Initiation Dispatcher在某種型別的事件發生發生時向其通知,事件與handle關聯

  • Initiation Dispatcher要求註冊在其上面的Concrete Event Handler傳遞內部關聯的handle,該handle會向作業系統標識

  • 當所有的Concrete Event Handler都註冊到 Initiation Dispatcher上後,應用會呼叫handle_events方法來啟動Initiation Dispatcher的事件迴圈,這時Initiation Dispatcher會將每個Concrete Event Handler關聯的handle合併,並使用Synchronous Event Demultiplexer來等待這些handle上事件的發生

  • 當與某個事件源對應的handle變為ready時,Synchronous Event Demultiplexer便會通知 Initiation Dispatcher。比如tcp的socket變為ready for reading

  • Initiation Dispatcher會觸發事件處理器的回撥方法。當事件發生時, Initiation Dispatcher會將被一個“key”(表示一個啟用的handle)定位和分發給特定的Event Handler的回撥方法

  • Initiation Dispatcher呼叫特定的Concrete Event Handler的回撥方法來響應其關聯的handle上發生的事件

在這五種角色中,

其中的 Initiation Dispatcher(初始分發器) 是最重要的,我們也稱其為Reactor。它本身定義了一些規範,同時提供了Handler的一些序號產生器制。

而Synchronous Event Demultiplexer(同步事件分離器) 在IO場景下,一般是由作業系統底層實現的,就是說作業系統底層必須能有這個能力,才能基於這個能力實現Reactor模型。在我們這個場景下,就是前面提到的linux的多路複用機制。

Handle(控制程式碼或者是描述符)在IO場景下就是IO網路連線的fd。

而Event Handler(事件處理器) 和 Concrete Event Handler(具體事件處理器) 在IO場景下分為三種處理事件:連線事件,寫事件,讀事件。對於連線事件的處理器,我們稱之為acceptor,讀/寫事件的處理器,我們統稱為handler。

所以在IO場景下,Reactor 我們需要實現的三個關鍵角色為:reactor、acceptor、handler。

Redis的實現

在redis中,下面一張圖就能說明其實現邏輯。

image-20220308094748568

在redis中,有個reactor(叫做aeMain)接收客戶端的redis請求。而在這個reactor中除了監聽連線事件acceptor之外,還可以動態註冊各種handler (aeCreateFileEvent)。當一個客戶端請求進入的時候,呼叫 aeProcessEvents 來分發事件。

這個邏輯就很清晰了吧。整個就是redis的事件處理機制。

參考

https://cloud.tencent.com/developer/article/1420724

https://www.youtube.com/watch?v=PyiPA_liKKo

http://hxz.ink/2021/09/11/reactor-pattern/

https://time.geekbang.org/column/article/408491

相關文章