Redis中的Reactor 模型的工作機制
首先,我們來看看什麼是 Reactor 模型。實際上,Reactor 模型就是網路伺服器端用來處理
高併發
網路 IO 請求的一種程式設計模型。我把這個模型的特徵用兩個“三”來總結,也就是:
三類處理事件,即連線事件、寫事件、讀事件;
三個關鍵角色,即 reactor、acceptor、handler。
那麼,Reactor 模型是如何基於這三類事件和三個角色來處理高併發請求的呢?下面我們就來具體瞭解下。
事件型別與關鍵角色
我們先來看看這三類事件和 Reactor 模型的關係。
其實,Reactor 模型處理的是客戶端和伺服器端的互動過程,而這三類事件正好對應了客戶端和伺服器端互動過程中,不同類請求在伺服器端引發的待處理事件:
當一個客戶端要和伺服器端進行互動時,客戶端會向伺服器端傳送連線請求,以建立連線,這就對應了伺服器端的一個連線事件。
一旦連線建立後,客戶端會給伺服器端傳送讀請求,以便讀取資料。伺服器端在處理讀請求時,需要向客戶端寫回資料,這對應了伺服器端的寫事件。
無論客戶端給伺服器端傳送讀或寫請求,伺服器端都需要從客戶端讀取請求內容,所以在這裡,讀或寫請求的讀取就對應了伺服器端的讀事件。
如下所示的圖例中,就展示了客戶端和伺服器端在互動過程中,不同類請求和 Reactor 模型事件的對應關係,你可以看下。
好,在瞭解了 Reactor 模型的三類事件後,你現在可能還有一個疑問:這三類事件是由誰來處理的呢?這其實就是模型中三個關鍵角色的作用了:
首先,連線事件由 acceptor 來處理,負責接收連線;acceptor 在接收連線後,會建立 handler,用於網路連線上對後續讀寫事件的處理;
其次,讀寫事件由 handler 處理;
最後,在高併發場景中,連線事件、讀寫事件會同時發生,所以,我們需要有一個角色專門監聽和分配事件,這就是 reactor 角色。
當有連線請求時,reactor 將產生的連線事件交由 acceptor 處理;當有讀寫請求時,reactor 將讀寫事件交由
handler
處理。
下圖就展示了這三個角色之間的關係,以及它們和事件的關係,你可以看下。
事實上,這三個角色都是 Reactor 模型中要實現的功能的抽象。當我們遵循 Reactor 模型開發伺服器端的網路框架時,就需要在程式設計的時候,在程式碼功能模組中實現 reactor、acceptor 和 handler 的邏輯。那麼,現在我們已經知道,這三個角色是圍繞事件的監聽、轉發和處理來進行互動的,那麼在程式設計時,我們又該如何實現這三者的互動呢?這就離不開事件驅動框架了。
事件驅動框架
所謂的事件驅動框架,就是在實現 Reactor 模型時,需要實現的程式碼整體控制邏輯。簡單來說,事件驅動框架包括了兩部分:
一是
事件初始化
;
二是
事件捕獲、分發和處理主迴圈
。
事件初始化是在伺服器程式啟動時就執行的,它的作用主要是建立需要監聽的事件型別,以及該類事件對應的 handler。而一旦伺服器完成初始化後,事件初始化也就相應完成了,伺服器程式就需要進入到事件捕獲、分發和處理的主迴圈中。
在開發程式碼時,我們通常會用一個
while 迴圈
來作為這個主迴圈。然後在這個主迴圈中,我們需要捕獲發生的事件、判斷事件型別,並根據事件型別,呼叫在初始化時建立好的事件 handler 來實際處理事件。
比如說,當有連線事件發生時,伺服器程式需要呼叫 acceptor 處理函式,建立和客戶端的連線。而當有讀事件發生時,就表明有讀或寫請求傳送到了伺服器端,伺服器程式就要呼叫具體的請求處理函式,從客戶端連線中讀取請求內容,進而就完成了讀事件的處理。這裡你可以參考下面給出的圖例,其中顯示了事件驅動框架的基本執行過程:
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 條件分支,如下所示:
這三個分支分別對應了以下三種情況:
情況一:既沒有時間事件,也沒有網路事件;
情況二:有 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 的呼叫,這裡我也放了一張示意圖,你可以看看整個呼叫鏈是如何工作和實現的。
OK,現在我們就已經在 aeMain 函式中,看到了 aeProcessEvents 函式被呼叫,並用於捕獲和分發事件的基本處理邏輯。
那麼,事件具體是由哪個函式來處理的呢?這就和框架中的 aeCreateFileEvents 函式有關了。
事件註冊:aeCreateFileEvent 函式
我們知道,當 Redis 啟動後,伺服器程式的
main
函式會呼叫
initSever
函式來進行初始化,而在初始化的過程中,
aeCreateFileEvent
就會被
initServer
函式呼叫,用於註冊要監聽的事件,以及相應的事件處理函式。
具體來說,在 initServer 函式的執行過程中,
initServer
函式會根據啟用的 IP 埠個數,為每個 IP 埠上的網路事件,呼叫
aeCreateFileEvent
,建立對
AE_READABLE
事件的監聽,並且註冊
AE_READABLE
事件的處理 handler,也就是
acceptTcpHandler
函式。這一過程如下圖所示:
所以這裡我們可以看到,
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 程式碼結構中的哪裡被呼叫。你可以使用這張表格,來鞏固今天這節課學習的事件驅動框架。
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 獲取
Linux伺服器開發/後臺架構師學習
下一篇:使壞的黑烏鴉的故事