您當前的位置:首頁 > 寵物

Redis中的Reactor 模型的工作機制

作者:由 linux技術棧 發表于 寵物時間:2022-12-27

首先,我們來看看什麼是 Reactor 模型。實際上,Reactor 模型就是網路伺服器端用來處理

高併發

網路 IO 請求的一種程式設計模型。我把這個模型的特徵用兩個“三”來總結,也就是:

三類處理事件,即連線事件、寫事件、讀事件;

三個關鍵角色,即 reactor、acceptor、handler。

那麼,Reactor 模型是如何基於這三類事件和三個角色來處理高併發請求的呢?下面我們就來具體瞭解下。

事件型別與關鍵角色

我們先來看看這三類事件和 Reactor 模型的關係。

其實,Reactor 模型處理的是客戶端和伺服器端的互動過程,而這三類事件正好對應了客戶端和伺服器端互動過程中,不同類請求在伺服器端引發的待處理事件:

當一個客戶端要和伺服器端進行互動時,客戶端會向伺服器端傳送連線請求,以建立連線,這就對應了伺服器端的一個連線事件。

一旦連線建立後,客戶端會給伺服器端傳送讀請求,以便讀取資料。伺服器端在處理讀請求時,需要向客戶端寫回資料,這對應了伺服器端的寫事件。

無論客戶端給伺服器端傳送讀或寫請求,伺服器端都需要從客戶端讀取請求內容,所以在這裡,讀或寫請求的讀取就對應了伺服器端的讀事件。

如下所示的圖例中,就展示了客戶端和伺服器端在互動過程中,不同類請求和 Reactor 模型事件的對應關係,你可以看下。

Redis中的Reactor 模型的工作機制

好,在瞭解了 Reactor 模型的三類事件後,你現在可能還有一個疑問:這三類事件是由誰來處理的呢?這其實就是模型中三個關鍵角色的作用了:

首先,連線事件由 acceptor 來處理,負責接收連線;acceptor 在接收連線後,會建立 handler,用於網路連線上對後續讀寫事件的處理;

其次,讀寫事件由 handler 處理;

最後,在高併發場景中,連線事件、讀寫事件會同時發生,所以,我們需要有一個角色專門監聽和分配事件,這就是 reactor 角色。

當有連線請求時,reactor 將產生的連線事件交由 acceptor 處理;當有讀寫請求時,reactor 將讀寫事件交由

handler

處理。

下圖就展示了這三個角色之間的關係,以及它們和事件的關係,你可以看下。

Redis中的Reactor 模型的工作機制

事實上,這三個角色都是 Reactor 模型中要實現的功能的抽象。當我們遵循 Reactor 模型開發伺服器端的網路框架時,就需要在程式設計的時候,在程式碼功能模組中實現 reactor、acceptor 和 handler 的邏輯。那麼,現在我們已經知道,這三個角色是圍繞事件的監聽、轉發和處理來進行互動的,那麼在程式設計時,我們又該如何實現這三者的互動呢?這就離不開事件驅動框架了。

事件驅動框架

所謂的事件驅動框架,就是在實現 Reactor 模型時,需要實現的程式碼整體控制邏輯。簡單來說,事件驅動框架包括了兩部分:

一是

事件初始化

二是

事件捕獲、分發和處理主迴圈

事件初始化是在伺服器程式啟動時就執行的,它的作用主要是建立需要監聽的事件型別,以及該類事件對應的 handler。而一旦伺服器完成初始化後,事件初始化也就相應完成了,伺服器程式就需要進入到事件捕獲、分發和處理的主迴圈中。

在開發程式碼時,我們通常會用一個

while 迴圈

來作為這個主迴圈。然後在這個主迴圈中,我們需要捕獲發生的事件、判斷事件型別,並根據事件型別,呼叫在初始化時建立好的事件 handler 來實際處理事件。

比如說,當有連線事件發生時,伺服器程式需要呼叫 acceptor 處理函式,建立和客戶端的連線。而當有讀事件發生時,就表明有讀或寫請求傳送到了伺服器端,伺服器程式就要呼叫具體的請求處理函式,從客戶端連線中讀取請求內容,進而就完成了讀事件的處理。這裡你可以參考下面給出的圖例,其中顯示了事件驅動框架的基本執行過程:

Redis中的Reactor 模型的工作機制

Reactor 模型的基本工作機制

:客戶端的不同類請求會在伺服器端觸發連線、讀、寫三類事件,這三類事件的監聽、分發和處理又是由 reactor、acceptor、handler 三類角色來完成的,然後這三類角色會透過事件驅動框架來實現互動和事件處理。

所以可見,實現一個 Reactor 模型的關鍵,就是要實現事件驅動框架。那麼,如何開發實現一個事件驅動框架呢?

Redis 提供了一個簡潔但有效的參考實現,非常值得我們學習,而且也可以用於自己的網路系統開發。下面,我們就一起來學習下 Redis 中對 Reactor 模型的實現。Redis 對 Reactor 模型的實現。

Redis 對 Reactor 模型的實現

首先我們要知道的是,Redis 的網路框架實現了 Reactor 模型,並且自行開發實現了一個事件驅動框架。這個框架對應的 Redis 程式碼實現檔案是

ae.c

,對應的標頭檔案是

ae.h

前面我們已經知道,事件驅動框架的實現離不開事件的定義,以及事件註冊、捕獲、分發和處理等一系列操作。當然,對於整個框架來說,還需要能一直執行,持續地響應發生的事件。

那麼由此,我們從 ae。h 標頭檔案中就可以看到,Redis 為了實現事件驅動框架,相應地定義了

事件的資料結構、框架主迴圈函式、事件捕獲分發函式、事件和 handler 註冊函式

。所以接下來,我們就依次來了解學習下。

事件的資料結構定義:以 aeFileEvent 為例

首先,我們要明確一點,就是在 Redis 事件驅動框架的實現當中,事件的資料結構是關聯事件型別和事件處理函式的關鍵要素。而 Redis 的事件驅動框架定義了兩類事件:

IO 事件和時間事件

,分別對應了客戶端傳送的

網路請求和 Redis 自身的週期性操作

。這也就是說,

不同型別事件的資料結構定義是不一樣的

不過,由於這節課我們主要關注的是事件框架的整體設計與實現,所以對於不同型別事件的差異和具體處理,我會在下節課給你詳細介紹。那麼在今天的課程中,為了讓你能夠理解事件資料結構對框架的作用,我就以 IO 事件 aeFileEvent 為例,給你介紹下它的資料結構定義。

aeFileEvent 是一個結構體,它定義了 4 個成員變數

mask、rfileProce、wfileProce 和 clientData

,如下所示:

/* File event structure */

typedef

struct

aeFileEvent

{

int

mask

/* one of AE_(READABLE|WRITABLE|BARRIER) */

aeFileProc

*

rfileProc

aeFileProc

*

wfileProc

void

*

clientData

}

aeFileEvent

mask

是用來表示事件型別的掩碼。對於網路通訊的事件來說,主要有

AE_READABLE、AE_WRITABLE 和 AE_BARRIER

三種類型事件。框架在分發事件時,依賴的就是結構體中的事件型別;

rfileProc

wfileProc

分別是指向 AE_READABLE 和 AE_WRITABLE 這兩類事件的處理函式,也就是 Reactor 模型中的 handler。框架在分發事件後,就需要呼叫結構體中定義的函式進行事件處理;

最後一個成員變數

clientData

是用來指向客戶端私有資料的指標。

除了事件的資料結構以外,前面我還提到 Redis 在 ae。h 檔案中,定義了支撐框架執行的主要函式,包括框架主迴圈的 aeMain 函式、負責事件捕獲與分發的 aeProcessEvents 函式,以及負責事件和 handler 註冊的 aeCreateFileEvent 函式,它們的原型定義如下:

void aeMain(aeEventLoop *eventLoop);

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData);

int aeProcessEvents(aeEventLoop *eventLoop, int flags);

而這三個函式的實現,都是在對應的 ae。c 檔案中,那麼接下來,我就給你具體介紹下這三個函式的主體邏輯和關鍵流程。

主迴圈:aeMain 函式

我們先來看下 aeMain 函式。

aeMain 函式的邏輯很簡單,就是

用一個迴圈不停地判斷事件迴圈的停止標記

。如果事件迴圈的停止標記被設定為 true,那麼針對事件捕獲、分發和處理的整個主迴圈就停止了;否則,主迴圈會一直執行。aeMain 函式的主體程式碼如下所示:

ae。c檔案中可以檢視

void aeMain(aeEventLoop *eventLoop) {

eventLoop->stop = 0;

while (!eventLoop->stop) {

aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_BEFORE_SLEEP|AE_CALL_AFTER_SLEEP);

}

}

那麼這裡你可能要問了,aeMain 函式是在哪裡被呼叫的呢?

按照事件驅動框架的程式設計規範來說,框架主迴圈是在伺服器程式初始化完成後,就會開始執行。因此,如果我們把目光轉向 Redis 伺服器初始化的函式,就會發現伺服器程式的 main 函式在完成 Redis server 的初始化後,會呼叫

aeMain

函式開始執行事件驅動框架。如果你想具體檢視 main 函式,main 函式在

server.c

檔案中,我們在第 8 講中介紹過該檔案,

server.c

主要用於初始化伺服器和執行伺服器整體控制流程,你可以回顧下。

不過,既然 aeMain 函式包含了事件框架的主迴圈,那麼在主迴圈中,事件又是如何被捕獲、分發和處理呢?這就是由 aeProcessEvents 函式來完成的了。

事件捕獲與分發:aeProcessEvents 函式

aeProcessEvents 函式實現的主要功能,包括捕獲事件、判斷事件型別和呼叫具體的事件處理函式,從而實現事件的處理。從 aeProcessEvents 函式的主體結構中,我們可以看到主要有三個 if 條件分支,如下所示:

Redis中的Reactor 模型的工作機制

這三個分支分別對應了以下三種情況:

情況一:既沒有時間事件,也沒有網路事件;

情況二:有 IO 事件或者有需要緊急處理的時間事件;

情況三:只有普通的時間事件。

那麼對於第一種情況來說,因為沒有任何事件需要處理,aeProcessEvents 函式就會直接返回到 aeMain 的主迴圈,開始下一輪的迴圈;而對於第三種情況來說,該情況發生時只有普通時間事件發生,所以 aeMain 函式會呼叫專門處理時間事件的函式 processTimeEvents,對時間事件進行處理。

現在,我們再來看看第二種情況。

首先,當該情況發生時,Redis 需要捕獲發生的網路事件,並進行相應的處理。那麼從 Redis 原始碼中我們可以分析得到,在這種情況下,

aeApiPoll

函式會被呼叫,用來捕獲事件,如下所示:

int aeProcessEvents(aeEventLoop *eventLoop, int flags){

。。。

if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {

。。。

//呼叫aeApiPoll函式捕獲事件

numevents = aeApiPoll(eventLoop, tvp);

。。。

}

。。。

}

那麼,aeApiPoll 是如何捕獲事件呢?

實際上,Redis 是依賴於作業系統底層提供的

IO 多路複用機制

,來實現事件捕獲,檢查是否有新的連線、讀寫事件發生。為了適配不同的作業系統,Redis 對不同作業系統實現的網路 IO 多路複用函式,都進行了統一的封裝,封裝後的程式碼分別透過以下四個檔案中實現:

ae_epoll.c

,對應 Linux 上的 IO 複用函式 epoll;

ae_evport.c

,對應 Solaris 上的 IO 複用函式 evport;

ae_kqueue.c

,對應 macOS 或 FreeBSD 上的 IO 複用函式 kqueue;

ae_select.c

,對應 Linux(或 Windows)的 IO 複用函式 select。

這樣,在有了這些封裝程式碼後,Redis 在不同的作業系統上呼叫 IO 多路複用 API 時,就可以透過統一的介面來進行呼叫了。不過看到這裡,你可能還是不太明白 Redis 封裝的具體操作,所以這裡,我就以在伺服器端最常用的 Linux 作業系統為例,給你介紹下 Redis 是如何封裝 Linux 上提供的 IO 複用 API 的。

首先,Linux 上提供了

epoll_wait

API,用於檢測核心中發生的網路 IO 事件。在ae_epoll。c檔案中,

aeApiPoll

函式就是封裝了對

epoll_wait

的呼叫。

這個封裝程式如下所示,其中你可以看到,在

aeApiPoll

函式中直接呼叫了

epoll_wait

函式,並將 epoll 返回的事件資訊儲存起來的邏輯:

ae_epoll。c檔案中檢視

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {

aeApiState *state = eventLoop->apidata;

int retval, numevents = 0;

// 呼叫epoll_wait獲取監聽到的事件

retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,

tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/1000) : -1);

if (retval > 0) {

int j;

// 獲得監聽到的事件數量

numevents = retval;

// 針對每一個事件,進行處理

for (j = 0; j < numevents; j++) {

int mask = 0;

struct epoll_event *e = state->events+j;

// 讀請求

if (e->events & EPOLLIN) mask |= AE_READABLE;

// 寫請求

if (e->events & EPOLLOUT) mask |= AE_WRITABLE;

// 錯誤請求

if (e->events & EPOLLERR) mask |= AE_WRITABLE|AE_READABLE;

// 掛起請求

if (e->events & EPOLLHUP) mask |= AE_WRITABLE|AE_READABLE;

eventLoop->fired[j]。fd = e->data。fd;

eventLoop->fired[j]。mask = mask;

}

}

return numevents;

}

為了讓你更加清晰地理解,事件驅動框架是如何實現最終對 epoll_wait 的呼叫,這裡我也放了一張示意圖,你可以看看整個呼叫鏈是如何工作和實現的。

Redis中的Reactor 模型的工作機制

OK,現在我們就已經在 aeMain 函式中,看到了 aeProcessEvents 函式被呼叫,並用於捕獲和分發事件的基本處理邏輯。

那麼,事件具體是由哪個函式來處理的呢?這就和框架中的 aeCreateFileEvents 函式有關了。

事件註冊:aeCreateFileEvent 函式

我們知道,當 Redis 啟動後,伺服器程式的

main

函式會呼叫

initSever

函式來進行初始化,而在初始化的過程中,

aeCreateFileEvent

就會被

initServer

函式呼叫,用於註冊要監聽的事件,以及相應的事件處理函式。

具體來說,在 initServer 函式的執行過程中,

initServer

函式會根據啟用的 IP 埠個數,為每個 IP 埠上的網路事件,呼叫

aeCreateFileEvent

,建立對

AE_READABLE

事件的監聽,並且註冊

AE_READABLE

事件的處理 handler,也就是

acceptTcpHandler

函式。這一過程如下圖所示:

Redis中的Reactor 模型的工作機制

所以這裡我們可以看到,

AE_READABLE 事件就是客戶端的網路連線事件,而對應的處理函式就是接收 TCP 連線請求

。下面的示例程式碼中,顯示了 initServer 中呼叫 aeCreateFileEvent 的部分片段,你可以看下:

void initServer(void) {

if (server。sofd > 0 && aeCreateFileEvent(server。el,server。sofd,AE_READABLE,

acceptUnixHandler,NULL) == AE_ERR) serverPanic(“Unrecoverable error creating server。sofd file event。”);

/* Register a readable event for the pipe used to awake the event loop

* when a blocked client in a module needs attention。 */

if (aeCreateFileEvent(server。el, server。module_blocked_pipe[0], AE_READABLE,

moduleBlockedClientPipeReadable,NULL) == AE_ERR) {

serverPanic(

“Error registering the readable event for the module ”

“blocked clients subsystem。”);

}

}

那麼,aeCreateFileEvent 如何實現事件和處理函式的註冊呢?

這就和剛才我介紹的 Redis 對底層 IO 多路複用函式封裝有關了,下面我仍然以 Linux 系統為例,來給你說明一下。

首先,Linux 提供了

epoll_ctl API

,用於

增加新的觀察事件

。而 Redis 在此基礎上,封裝了

aeApiAddEvent

函式,對 epoll_ctl 進行呼叫。

所以這樣一來,

aeCreateFileEvent

就會呼叫

aeApiAddEvent

,然後

aeApiAddEvent

再透過呼叫

epoll_ctl

,來註冊希望監聽的事件和相應的處理函式。等到

aeProcessEvents

函式捕獲到實際事件時,它就會呼叫註冊的函式對事件進行處理了。

好了,到這裡,我們就已經全部瞭解了 Redis 中實現事件驅動框架的三個關鍵函式:aeMain、aeProcessEvents,以及 aeCreateFileEvent。當你要去實現一個事件驅動框架時,Redis 的設計思想就具有很好的參考意義。

最後我再帶你來簡單地回顧下,在實現事件驅動框架的時候,

首先,需要先實現一個主迴圈函式(對應 aeMain),負責一直執行框架。

其次,需要編寫事件註冊函式(對應 aeCreateFileEvent),用來註冊監聽的事件和事件對應的處理函式。

只有對事件和處理函式進行了註冊,才能在事件發生時呼叫相應的函式進行處理。

最後,需要編寫事件監聽、分發函式(對應 aeProcessEvents),負責呼叫作業系統底層函式來捕獲網路連線、讀、寫事件,並分發給不同處理函式進一步處理。

小結

Redis 一直被稱為單執行緒架構,按照我們通常的理解,單個執行緒只能處理單個客戶端的請求,但是在實際使用時,我們會看到 Redis 能同時和成百上千個客戶端進行互動,這就是因為 Redis 基於 Reactor 模型,實現了高效能的網路框架,

透過事件驅動框架,Redis 可以使用一個迴圈來不斷捕獲、分發和處理客戶端產生的網路連線、資料讀寫事件。

為了方便你從程式碼層面掌握 Redis 事件驅動框架的實現,我總結了一個表格,其中列出了 Redis 事件驅動框架的主要函式和功能、它們所屬的 C 檔案,以及這些函式本身是在 Redis 程式碼結構中的哪裡被呼叫。你可以使用這張表格,來鞏固今天這節課學習的事件驅動框架。

Redis中的Reactor 模型的工作機制

1、為了高效處理網路 IO 的「連線事件」、「讀事件」、「寫事件」,演化出了 Reactor 模型

2、Reactor 模型主要有 reactor、acceptor、handler 三類角色:

- reactor:分配事件

- acceptor:接收連線請求

- handler:處理業務邏輯

3、Reactor 模型又分為 3 類:

- 單 Reactor 單執行緒:accept -> read -> 處理業務邏輯 -> write 都在一個執行緒

- 單 Reactor 多執行緒:accept/read/write 在一個執行緒,處理業務邏輯在另一個執行緒

- 多 Reactor 多執行緒 / 程序:accept 在一個執行緒/程序,read/處理業務邏輯/write 在另一個執行緒/程序

4、Redis 6。0 以下版本,屬於單 Reactor 單執行緒模型,監聽請求、讀取資料、處理請求、寫回資料都在一個執行緒中執行,這樣會有 3 個問題:

- 單執行緒無法利用多核

- 處理請求發生耗時,會阻塞整個執行緒,影響整體效能

- 併發請求過高,讀取/寫回資料存在瓶頸

5、針對問題 3,Redis 6。0 進行了最佳化,引入了 IO 多執行緒,把讀寫請求資料的邏輯,用多執行緒處理,提升併發效能,但處理請求的邏輯依舊是單執行緒處理

6、 Reactor模型可以分為3種:

單執行緒Reactor模式

一個執行緒:

單執行緒:建立連線(Acceptor)、監聽accept、read、write事件(Reactor)、處理事件(Handler)都只用一個單執行緒。

多執行緒Reactor模式

一個執行緒 + 一個執行緒池:

單執行緒:建立連線(Acceptor)和 監聽accept、read、write事件(Reactor),複用一個執行緒。

工作執行緒池:處理事件(Handler),由一個工作執行緒池來執行業務邏輯,包括資料就緒後,使用者態的資料讀寫。

主從Reactor模式

三個執行緒池:

主執行緒池:建立連線(Acceptor),並且將accept事件註冊到從執行緒池。

從執行緒池:監聽accept、read、write事件(Reactor),包括等待資料就緒時,核心態的資料I讀寫。

工作執行緒池:處理事件(Handler),由一個工作執行緒池來執行業務邏輯,包括資料就緒後,使用者態的資料讀寫

具體的可以參考併發大神 doug lea 關於Reactor的文章。

http://

gee。cs。oswego。edu/dl/cp

jslides/nio。pdf

再提一點,使用了多路複用,不一定是使用了Reacto模型,Mysql使用了select(為什麼不使用epoll,因為Mysql的瓶頸不是網路,是磁碟IO),但是並不是Reactor模型

除了 Redis,你還了解什麼軟體系統使用了 Reactor 模型嗎?

Netty、Memcached 採用多 Reactor 多執行緒模型。

Nginx 採用多 Reactor 多程序模型,不過與標準的多 Reactor 多程序模型有些許差異。Nginx 的主程序只用來初始化 socket,不會 accept 連線,而是由子程序 accept 連線,之後這個連線的所有處理都在子程序中完成。

nginx:nginx是多程序模型,master程序不處理網路IO,每個Wroker程序是一個獨立的單Reacotr單執行緒模型。

netty:通訊絕對的王者,預設是多Reactor,主Reacotr只負責建立連線,然後把建立好的連線給到從Reactor,從Reactor負責IO讀寫。當然可以專門調整為單Reactor。

kafka:kafka也是多Reactor,但是因為Kafka主要與磁碟IO互動,因此真正的讀寫資料不是從Reactor處理的,而是有一個worker執行緒池,專門處理磁碟IO,從Reactor負責網路IO,然後把任務交給worker執行緒池處理。

轉載自:柯南二號 本文連結:https://blog.csdn.net/qq_41688840/article/details/122182339

Redis reactor網路程式設計要點、網路模組封裝以及處理

LinuxC/C++伺服器開發/架構師面試題、學習資料、教學影片和學習路線圖,免費分享有需要的可以自行新增學習交流群973961276 獲取

Redis中的Reactor 模型的工作機制

Linux伺服器開發/後臺架構師學習

標簽: 事件  Reactor  函式  Redis  AE