高效能的Reactor和Proactor模式學習

MelonTe發表於2024-11-07

0、引言

在上一篇的筆記中,我們學習了作業系統提供的高效I/O管理技術,主要用於解決伺服器在高併發場景下的資源浪費和瓶頸問題。但是在實際的程式碼編寫中,要是我們都全部呼叫底層的I/O多路複用介面來編寫網路程式這種程序導向的方式,必然會導致開發的效率不高。於是在這一章節,我們來學習兩個重要的Reactor和Proactor模型,他們都借用了I/O多路複用機制來高效地管理和分發事件,並且更利於程式設計師進行程式開發的程式碼編寫。

1、Reactor和Proactor

基於物件導向的思想,大佬們對I/O多路複用作了一層封裝,讓使用者不用考慮底層網路介面的細節,只需要關注應用程式碼的編寫。

大佬們為這個模式取名為:Reactor模式,翻譯過來的意思為「反應堆」,這個反應堆指的是「對事件反應」,也就是說來一個事件,Reactor就有相對應的反應/響應。

Reactor模式也叫Dispatcher模式,即I/O多路複用監聽事件,收到事件後,根據事件型別分配(Dispatch)給某個程序/執行緒。

Reactor模式主要由Reactor和處理資源池這兩個核心部分組成,它們所負責的事情如下:

  • Reactor負責監聽和分發事件,事件型別包含連線事件、讀寫事件;
  • 處理資源池負責處理事件,如read->業務邏輯->send;

(說到這裡,我覺得就像是使用go的gin框架編寫web程式時,需要在router配置路由一樣,這就對應了Reactor的監聽事件,但是具體的處理流程就交給handler來處理一樣,這種思想具有相似性正是因為這種設計思想在面對複雜邏輯的時候具有更高效的併發處理效率。)

Reactor模式是靈活多變的,在理論上該模式有4種方案的選擇:

  • 單Reactor單程序/執行緒;
  • 單Reactor多程序/執行緒;
  • 多Reactor單程序/執行緒;
  • 多Reactor多程序/執行緒;

其中,「多Reactor單程序/執行緒」實現方案相比與「單Reactor單程序/執行緒」方案,不僅複雜而且沒有效能優勢,在實際中沒有應用。

省下的3個方案都是比較經典主流的。

方案具體使用程序還是執行緒,要看使用的程式語言以及平臺有關:

  • Java語言一般使用執行緒,比如Netty
  • C語言程序和執行緒都可以,例如Nginx使用的是程序,Memcache使用的是執行緒。

2、Reactor

2.1、單Reactor單程序/執行緒

下面是一張「單Reactor單程序」的方案示意圖

程序中有Reactor、Acceptor、Handler三個物件:

  • Reactor物件的作用是監聽和事件分發;
  • Acceptor物件的作用是獲取連線;
  • Handler物件的作用是處理業務;

這裡的select、accept、read、send是系統呼叫函式,dispatch和「業務處理」是需要完成的操作,其中dispatch是分發事件操作。

該方案的流程如下:

  • Reactor物件透過select(IO多路複用介面)監聽事件,收到事件後透過dispatch進行分發;
  • 如果是連線建立事件,則被分發到Acceptor物件進行處理,該物件會呼叫accept獲取連線並建立一個handler物件來處理後續的響應。
  • 如果不是,則交由Handler物件響應。
  • handler物件透過read->業務處理->send的流程來完成完整的業務流程。

該方案有兩個缺點:

  • 第一個缺點,因為只有一個程序,無法利用多核CPU的效能;
  • 第二個缺點,Handler物件在業務處理時,整個程序無法處理其他連線的事件,如果業務處理耗時過長,那麼造成響應的延遲就會更長;

所以單Reactor單程序的方案不適合計算機密集型的場景,只適用於業務處理非常快速的場景。

Redis是由C語言實現的,在6.0版本之間採用的就是這種方案。

2.2、單Reactor多程序/執行緒

為了彌補單程序/執行緒的缺點,於是引入了多程序/執行緒。

我們來看它的不同之處:

  • Handler不再去處理具體的業務邏輯,而是隻負責資料的接收和傳送。
  • 具體的業務邏輯透過分配一個字執行緒Processor去完成,將處理結果返回給Handler物件。

這種方案的優勢在於能夠充分利用多核CPU的效能,但是引入了多執行緒就自然帶來了執行緒競爭資源的問題。

我們需要使用互斥鎖來保證對共享資源的併發訪問問題。

單Reactor模式還有個問題,因為一個Reactor物件承擔所有事件的監聽和響應,而且只在主執行緒中執行,在面對瞬間高併發的場景時,容易成為效能的瓶頸的地方。

2.3、多Reactor多程序/執行緒

為了能夠面對瞬間高併發的場景,於是進而引用多Reactor。

它的方案大致步驟是:主執行緒的MainReactor物件透過select監控連線建立事件,收到事件後透過Acceptor物件獲取連線,此時子Reactor會將MainReactor建立的連線加入進select繼續監聽,在今後的事件發生中,轉而呼叫SubReactor對應的Handler物件來響應。

這種方案實現起來更加的簡單:

  • 主執行緒和子執行緒分工明確,主執行緒只負責接收新連線,子執行緒負責完成後續的業務處理。
  • 主執行緒和子執行緒的互動簡單,只需要把連線傳遞給子執行緒,無需等待返回資料。

3、Proactor

Reactor是非阻塞同步網路模式,而Proactor是非同步網路模式

在非阻塞I/O中,read請求在資料未準備完善的時候立刻返回,可以繼續往下執行,此時程序不斷地輪詢核心,直到資料準備好後,核心將資料複製到使用者快取區,read呼叫才可以獲取到結果,過程如下:

需要注意的是,這裡的最後一次read呼叫中,等待資料從核心緩衝區複製到使用者緩衝區的過程是需要等待的,也就是說這個步驟是「同步的」。

因此,無論read和send是阻塞I/O還是非阻塞I/O,都是同步呼叫。如果核心實現的複製效率不高,read呼叫就會在這個同步過程中等待比較長的時間。

而真正的非同步I/O是「核心資料準備好」和「資料從核心態複製到使用者態」這兩個過程都不需要等待。

當我們發起aio_read(非同步I/O)之後,就立即返回,核心自動將資料從核心空間複製到使用者空間,這個複製過程同樣是非同步的,應用程式並不需要主動發起複製動作

顯而易見的是,非同步I/O比同步I/O的效能更好。

Proatcor正是採用了非同步I/O技術,所以被稱為非同步網路模型。

我們再來對比一下Reactor和Proactor的區別:

  • Reactor是非阻塞同步網路模式,感知的是就緒可讀寫事件。在每次感知到有事件發生的時候,就需要應用程式主動呼叫read方法來完成資料的讀取,這個過程是同步的,讀取完資料後應用程序才能處理資料。
  • Proactor是非同步網路模式,感知的是已完成的讀寫事件。在發起非同步讀寫請求時,需要傳入資料緩衝區的地址等資訊,系統核心就可以自動幫我們將資料的讀寫請求工作完成,作業系統完成讀寫工作後,就會自動通知應用程序直接處理資料。

因此,Reactor可以理解為「來了事件作業系統通知應用程序,讓應用程序來處理」,而Proactor可以理解為「來了事件作業系統來處理,處理完再通知應用程序」。

無論是Reactor還是Proactor,都是基於一種「事件分發」的網路程式設計模式,區別在於Reactor模式是基於「待完成」的I/O事件,而Proactor模式則是基於「已完成」的I/O事件

Proactor的示意圖如下:

Proactor工作流程如下:

  • Proactor Initiator負責建立Proactor和Handler物件,並將Proactor和Handler透過Asychronous Operation Processor註冊到核心
  • Asychronous Operation Processor負責處理註冊請求,並處理I/O操作
  • Asychronous Operation Processor完成I/O操作後通知Proactor
  • Proactor回撥對應的Handler進行業務處理
  • Handler完成業務處理

可惜的是,在Linux下的非同步I/O是不完善的,aio系列函式是由POSIX定義的非同步操作介面,不是真正的作業系統級別的支援,而是在使用者空間模擬出來的非同步,並且僅支援基於本地檔案的aio非同步操作,網路程式設計中的socket是不支援的,這也使得Linux的高效能網路程式都是使用Reactor方案。

Windows裡實現了一套完整的支援socket的非同步程式設計介面,叫做IOCP。由作業系統級別實現的非同步I/O,是真正意義上的非同步I/O。

4、總結

常見的Reactor實現方案有三中。

第一種是單Reactor單程序/執行緒,不考慮程序間通訊以及資料同步的問題,實現起來簡單,但是無法利用多核CPU,而且處理業務邏輯的時間不能太長,所以只適用於業務處理快速的場景,Redis的6.0版本之前採用的是單Reactor單程序的方案。(6.0版本之後是單執行緒執行 + 多執行緒 I/O 最佳化的改進型 Reactor 模型)

第二種方案單Reactor多程序/執行緒,解決了方案一的缺陷,但是在面對瞬時高併發的場景時,單Reactor會成為效能瓶頸。

第三種方案是多Reactor多程序/執行緒,主Reactor只負責監聽事件,響應事件的工作交給了Reactor,Netty和Memcache都採用了「多Reactor多執行緒」的方案,Nigin採用了這種模式。

Reactor可以理解為「來了事件作業系統通知應用程序,讓應用程序來處理」,而Proactor為「來了事件作業系統來處理,處理完通知程序」。

無論是Proactor還是Reactor,都是一種基於「事件分發」的網路程式設計模式。

5、參考部落格

本篇部落格個人學習、總結、摘抄至:[小林coding](9.3 高效能網路模式:Reactor 和 Proactor | 小林coding)

相關文章