1. 程式人生 > >Redis 網路架構及單執行緒模型

Redis 網路架構及單執行緒模型

最近略有閒暇時間,於是對Redis進行了一些學習,學習途徑除了官方文件還有Redis原始碼,我看的版本是2.8.13,Redis原始碼總行數不到5W行,不同元件拆分非常細緻,閱讀起來也很清晰。這篇部落格主要介紹我對Redis網路層架構以及執行緒模型的一些瞭解,希望能對大家有所幫助。

Redis網路基礎架構

網路程式設計離不開Socket,網路I/O模型最常用的無非是同步阻塞、同步非阻塞、非同步阻塞、非同步非阻塞,高效能網路伺服器最常見的執行緒模型也就是基於EventLoop模式的單執行緒模型。我們看看Redis的網路架構是怎麼樣的:

Redis基礎組建結構

這裡解釋下上圖涉及的元件,Redis網路層基礎元件主要包括四個部分:

  1. EventLoop事件輪訓器,這部分實現在AE裡面。
  2. 提供Socket控制代碼事件的多路複用器,這部分分別對於不同平臺提供了不同的實現,比如epoll和select可以用於linux平臺、kqueue可以用於蘋果平臺、evpoll可以用於Solaris平臺,這裡並沒有看到iocp,也就是Redis對於Windows支援並不是很好。
  3. 包括網路事件處理器實現的networking,這部分主要包括兩個重要的今天要講的事件處理器:acceptTcpHandler和acceptCommonHandler。
  4. 處理網路比較底層的部分,比如網路控制代碼建立、網路的讀寫等。

Redis單執行緒模型

要理解Redis的單執行緒模型,我們先丟擲一些問題,當我們有多個客戶端同時去跟Redis Server建立連線,之後又同時對某個key進行操作,這個過程中發生了什麼呢?會不會有併發問題?

網路初始化

好了,這些問題先丟在這了,我們看看Redis啟動初始化的過程中會做什麼事情,這裡儘量省略了與本文無關的部分:

  1. 初始化Redis Server引數,這部分程式碼通過initServerConfig實現。
  2. 初始化Redis Server,這部分程式碼在initServer裡面。
  3. 啟動事件輪訓器。

對,這裡我們就把Redis的啟動部分簡化為三步,跟網路操作有關的主要在第二步和第三步裡面,來看看initServer裡面發生了什麼:

initServer流程

initServer裡面首先建立了一個EventLoop,然後監聽Server的IP對應的埠號,假設我們監聽的是127.0.0.1:3333這個IP:埠對,我們得到的一個Server Socket控制代碼,最後通過createFileEvent將我們得到的Server Socket控制代碼和我們關心的網路事件mask註冊到EventLoop上面。EventLoop是什麼呢,我們看看它的定義:

123456789101112typedefstructaeEventLoop{intmaxfd;/* highest file descriptor currently registered */intsetsize;/* max number of file descriptors tracked */longlongtimeEventNextId;time_t lastTime;/* Used to detect system clock skew */aeFileEvent *events;/* Registered events */aeFiredEvent *fired;/* Fired events */aeTimeEvent *timeEventHead;intstop;void*apidata;/* This is used for polling API specific data */aeBeforeSleepProc *beforesleep;}aeEventLoop;

上面我們關注的主要是兩個東西:events和fired。他們分別是兩個陣列,events用於存放被註冊的事件以及相應的控制代碼,fired用於存放當EventLoop執行緒從多路複用器輪訓到有事件的控制代碼的時候,EventLoop執行緒會把它放入fired數組裡面,然後處理。

事件註冊示意圖

我用上面的示意圖描述createFileEvent做的事情,就是將Server Socket控制代碼和關心的事件mask以及當事件產生的時候的事件處理器accptHandler生成一個aeFileEvent註冊到EventLoop的events的數組裡面,當然在這之前會首先將事件註冊到多路複用器上,也就是epoll、kqueue等這些元件上。事件註冊完之後需要對多路複用器進行輪訓,來分離我們關心切發生的事件,那就是最後一步,啟動事件輪詢器。

接收網路連線

上面的步驟完成了服務端的網路初始化,而且事件輪詢器已經開始工作了,事件輪詢器做什麼事情呢,就是不斷輪訓多路複用器,看看之前註冊的事件有沒有發生,如果有發生,則將會將事件分離出來,放入EventLoop的fired陣列中,然後處理這些事件。

很顯然,上面註冊的事件是客戶端建立連線這個事件,因此當有兩個客戶端同時連線Redis伺服器的時候,事件輪詢器會從多路複用器上面分離出這個事件,同時呼叫acceptHandler來處理。acceptHandler做的事情主要是accept客戶端的連線,建立socket控制代碼,然後將socket控制代碼和讀事件註冊到EventLoop的events數組裡面,不一樣的是對於客戶端的事件處理器是readQueryClient。

accept客戶端連線以及註冊客戶端連線控制代碼示意圖

上面示意圖表示了acceptHandler處理客戶端連線,得到控制代碼之後再將這個控制代碼註冊到多路複用器以及EventLoop上的示意圖。之後再同樣再處理下一個客戶端的連線,這些都是序列的。

事件輪訓

上面接收客戶端這部分其實都發生在事件輪訓的主迴圈裡面:

1 2 3 4 5 6 7 8 voidaeMain(aeEventLoop *eventLoop){ eventLoop->stop=0; while(!eventLoop->stop){ if(eventLoop->beforesleep!=NULL) eventLoop->beforesleep(eventLoop); aeProcessEvents(eventLoop,AE_ALL_EVENTS); } }

Redis會不斷的輪訓多路複用器,將網路事件分離出來,如果是accept事件,則新接收客戶端連線並將其註冊到多路複用器以及EventLoop中,如果是查詢事件,則通過讀取客戶端的命令進行相應的處理,這一切都是單執行緒,順序的執行的,因此不會發生併發問題。

應用分析

Redis官網對Redis的讀寫效能測試結果達到10左右,這是非常吸引人的。Redis的單執行緒的行為主要是對記憶體的讀寫,這些操作其實用不了多少時間,因此瓶頸在網路I/O上面,我們一般提供較好的網路環境就可以提升Redis的吞吐量,比如提高網路頻寬,除此之外還可以通過合併命令提交批處理請求來代替單條命令一次次請求從而減少網路開銷,提高吞吐量。

參考文獻

《Redis原始碼》