前言
隨着IO多路複用技術的出現,出現了很多事件處理模式,其中Reactor/Proactor模式是其中的佼佼者。
Reactor模式是非阻塞同步的I/O模型,Proactor模式是非阻塞異步I/O模型。
平時接觸的開源產品如Netty、Mina、Redis、ACE,事件模型都使用的Reactor模式;
而同樣做事件處理的Proactor,由於缺少操作系統支持的原因,相關的開源產品也少;這裏學習下其模型結構,重點對比下兩者的異同點;
1 Reactor事件處理模型
我們來回顧一下《【I/O設計總結一】五種I/O模型總結》中學習的IO多路複用模型:
同時,我們在《【I/O設計總結二】詳解IO多路複用和其三種模式——select/poll/epoll》一文中,我們介紹了linux系統的select,poll和epoll三個事件的分發函數,我們可以直接編寫代碼,讓應用線程去調用這三個函數中的某一個,完成最簡單的IO多路複用模型,也就是如上圖所示的這般流程。
這樣原始版本的IO多路複用模型好嗎?顯然不好,直接調用無法併發,效率太低,如果當前的請求沒有處理完,那麼後面的請求只能被阻塞,服務器的吞吐量太低。
利用線程池技術稍加改進,我們想到了經典的connection per thread,每一個連接用一個線程處理,對於每一個請求都分發給一個線程,每個線程中都獨自處理I/O操作。tomcat服務器的早期版本確實是這樣實現的。
一連接一線程的方式當然有很多優點,但缺點也很明顯:對於資源要求太高,系統中創建線程是需要比較高的系統資源的,如果連接數太高,系統無法承受,而且,線程的反覆創建-銷燬也需要代價。
這時,我們採用了基於事件驅動的設計,當有事件觸發時,纔會調用處理器進行數據處理。Reactor模式應運而生。
Reactor是“事件反應”的意思,可以通俗地理解爲“來了一個事件Reactor就有相應的反應”,具體的反應就是我們寫的業務代碼,Reactor會根據事件類型來調用相應的代碼進行處理。
Reactor模式也叫Dispatcher模式(在很多開源的系統裏面會看到這個名稱的類,其實就是實現Reactor模式的),分發確實更加貼近模式本身的含義,即I/O多路複用統一監聽事件,收到事件後分發(Dispatch)給某個進程。
歸根結底,Reactor就是基於事件驅動設計,利用回調和線程池技術來高效率使用select等函數的設計。
1.1 Reactor模型的架構
模型架構如上圖所示,我們來一一解釋這些控件:
- Handle:句柄,用來封裝或標識socket連接或是打開文件,你可以理解爲在模型中,它代表一個連接或者I/O流文件。
- Event Handler:事件處理接口,用來綁定某個handle和應用程序所提供的特定事件處理邏輯。Concrete Event HandlerA和Concrete Event HandlerB是它的具體實現類,應用程序可針對不同的連接(handle)定製不同的處理邏輯(event)。
- Synchronous Event Demultiplexer:同步事件多路分解器,由操作系統內核實現的一個函數(如Linux的select/poll/epoll);用於阻塞等待發生在句柄集合上的一個或多個事件;返回就緒的Event Handler集合。Java NIO的Selector就是一個Demultiplexer。
- Initiation Dispatcher:分發器,定義一個接口,實現以下功能:
- register_handle():供應用程序註冊它定義的Event Handler。即應用程序通過該方法將Event Handler加入Reactor的Synchronous Event Demultiplexer。
- remove_handle():供應用程序刪除Synchronous Event Demultiplexer中關注的Event Handler。
- handle_events():核心方法,也是Reactor模式的發動機,這個方法的核心邏輯如下:
- 首先通過同步事件多路選擇器提供的select()方法監聽網絡事件。
- 當有網絡事件就緒後,就遍歷註冊的Event Handler,找到對應的Event Handler來處理該網絡事件。
- handle_events()是非阻塞的,主程序可以調用handle_events()後繼續其他的操作,handle_events()內的邏輯會觸發輪詢,回調等操作。調用一次handle_events只會觸發一次輪詢檢查。
- 如果主程序是服務端的話,由於網絡事件是源源不斷的,主程序一般會不停調用Dispatcher的handle_events()。比如一個server服務端,開闢一個線程,循環調用handle_events(),非阻塞同步性的處理多個客戶端的連接。
Demultiplexer和Dispatcher是Reactor的核心,如果我們看到有人說Reactor負責監聽和分發事件,那麼其實就是將Demultiplexer和Dispatcher整合成一個Reactor組件來描述。
下文的圖,以及描述中出現的單Reactor和多Reactor,指的就是Demultiplexer+Dispatcher的組合提,即Reactor=Demultiplexer+Dispatcher
由圖可以看到,Reactor模型的簡單流程就是:
- 每當有一個客戶端連接進來,被服務端接收到,服務端就會將這個連接封裝爲一個handle,同時會創建的一個個Event Handler,將handle封裝進去,並在Event Handler內設定響應的回調函數。
- 服務端調用Dispatcher的register_handle()方法,將Event Handler註冊進來。
- 服務端會不停的調用Dispatcher的handle_events()方法,而handle_events()方法會調用Demultiplexer的select()方法,得到就緒的Event Handler。然後調用這個Event Handler的handle_event()方法,執行回調函數。
1.2 Reactor模型的實現方案
Reactor模式的設計,一般和資源池(進程池或線程池)相配合。其中Reactor組件包含Demultiplexer和Dispatcher這兩個組件,包含數量不固定,分別負責監聽和分配事件,處理資源池負責調度來處理事件。
初看Reactor的實現是比較簡單的,但實際上結合不同的業務場景,Reactor模式的具體實現方案靈活多變,主要體現在:
- Reactor的數量可以變化:可以是一個Reactor,也可以是多個Reactor。
- 資源池的數量可以變化:以進程爲例,可以是單個進程,也可以是多個進程,線程同理。
將上面兩個因素排列組合一下,理論上可以有 4 種選擇,但由於多Reactor單進程實現方案相比單Reactor單進程方案,既複雜又沒有性能優勢,因此多Reactor單進程方案僅僅是一個理論上的方案,實際沒有應用。
最終Reactor模式有這三種典型的實現方案:
- 單Reactor單進程/線程。
- 單Reactor多線程。
- 多Reactor多進程/線程。
以上方案具體選擇進程還是線程,更多地是和編程語言及平臺相關。例如,Java語言一般使用線程(例如,Netty),C語言使用進程和線程都可以。例如,Nginx使用進程,Memcache使用線程。
1.2.1 非Reactor的傳統模型
爲了比較Reactor模型的優勢,我們先來介紹一下Reactor模型出現以前的傳統模型,Java OIO(old IO)時代,這種模型經常被使用:客戶端與服務端建立好連接過後,服務端對每一個建立好的連接使用一個handler來處理,而每個handler都會綁定一個線程。
圖中的acceptor是註冊的一個特殊的Event Handler,負責創建連接的請求,如果Dispatcher接收到某個客戶端請求,發現是創建連接的請求,那麼就會直接將其轉發給acceptor來處理。
這樣做在連接的客戶端不多的情況下,也算是個不錯的選擇。但在連接的客戶端很多的情況下就會出現問題:
- 每一個連接服務端都會產生一個線程,當併發量比較高的情況下,會產生大量的線程。
- 在服務端很多線程的情況下,大量的線程的上下文切換是一個很大的開銷,會比較影響性能。
- 與服務端連接建立後,連接上未必是時時刻刻都有數據進行傳輸的,但是創建的線程一直都在,會造成服務端線程資源的一個極大的浪費。
1.2.2 單線程Reactor模型
介紹了非Reactor的傳統模型,我們再來介紹Reactor模型的樸素原型——單線程Reactor模型。這是Java NIO常用的模型。
由於Java OIO的網絡編程模型在客戶端很多的情況下回產生服務端線程數過多的問題,因此根據Reactor模式做出了改進。
根據上圖,Reactor角色對IO事件進行監聽(Demultiplexer負責)和分發(Dispatcher負責)。當事件產生時,Dispatcher會將handle分發給對應的處理器Event Handler進行處理。
面對IO阻塞,傳統OIO使用多線程來消除阻塞的影響,一個socket開啓一個線程來處理以防止一個連接IO的阻塞影響到其他的連接的處理。
而在單線程Reactor模型中,通過Reactor對於IO事件的監聽和分發,服務端只需要一個IO線程就能處理多個客戶端的連接。這就解決了導致服務端線程數過多的問題。
Reactor的單線程模式的單線程主要是針對於IO操作而言,也就是所有的IO的accept()、read()、write()以及connect()操作都在一個線程上完成的。
這個線程可以是服務端調用Dispatcher.handle_events()的線程,也可以是Dispatcher自己無限循環調用handle_events()的線程。我們稱這個線程爲Reactor的IO線程。
這個IO線程會循環調用Dispatcher的handle_events()方法,handle_events()方法會繼續調用Demultiplexer的select方法,輪詢檢查註冊進來的Event Handler中的handle,如果發生事件,那麼回調Event Handler的handle_event(),執行包括read、decode、complete、encode、send的完整IO處理邏輯。
但是這種模型還是有缺陷的,那就是所有的客戶端的請求都由一個IO線程來進行處理。當併發量比較大的情況下,服務端的處理性能無法避免會下降,因爲服務端IO線程每次只能處理一個客戶端的請求,其他的請求只能等待。
1.2.3 多線程Reactor模型
在目前的單線程Reactor模式中,不僅IO操作在該Reactor的IO線程上,連非IO的業務操作也在該線程上進行處理了,這可能會大大延遲IO請求的響應。所以我們應該將非IO的業務邏輯操作從Reactor線程上卸載,以此來加速Reactor線程對IO請求的響應。
如上圖所示,Reactor還是一個IO線程,負責監聽IO事件以及分發。只不過在事件發生時,Dispatcher回調Event Handler的handle_event(),只會執行read和send操作,中間的業務邏輯處理部分,即decode、complete、encode,都使用了一個線程池來進行處理,這樣能夠提高Reactor線程的I/O響應,不至於因爲一些耗時的業務邏輯而延遲對後面I/O請求的處理,解決了服務端單線程處理請求而帶來的性能瓶頸。
但是這樣還是有問題,這樣會把性能的瓶頸轉移到IO處理上。因爲IO事件的監聽和分發採用的還是單個線程,在併發量比較高的情況下,這個也是比較影響性能的。這是否還有繼續優化的空間呢?
1.2.4 多線程多Reactor模型
雖然多線程Reactor模型將非I/O操作交給了線程池來處理,但是所有的I/O操作依然由Reactor單線程執行,在高負載、高併發或大數據量的應用場景,依然較容易成爲瓶頸。所以,對於Reactor的優化,又產生出下面的多Reactor模式。
對於多個CPU的機器,爲充分利用系統資源,將Reactor拆分爲兩部分,如上圖
mainReactor負責監聽server socket,用來處理網絡新連接的建立,將建立的socketChannel指定註冊給subReactor,通常一個線程就可以處理;
subReactor維護自己的Demultiplexer, 基於mainReactor註冊的Event Handler多路分離I/O讀寫事件;
對非I/O的操作,依然轉交給線程池(Thread Pool)執行。
此種模型中,每個模塊的工作更加專一,耦合度更低,性能和穩定性也大量的提升,支持的可併發客戶端數量可達到上百萬級別。關於此種模型的應用,目前有很多優秀的框架已經在應用了,比如mina、Nginx、Memcached和Netty等。
1.2.5 總結
3種模式可以用個比喻來理解:餐廳常常僱傭接待員負責迎接顧客,當顧客入坐後,侍應生專門爲這張桌子服務。
單Reactor單線程:接待員和侍應生是同一個人,全程爲顧客服務。 單Reactor多線程:1 個接待員,多個侍應生,接待員只負責接待。 主從Reactor多線程:多個接待員,多個侍應生。
1.3 Reactor模型的優劣
- 優點:
- Reactor實現相對簡單,對於鏈接多,但耗時短的處理場景高效;
- 操作系統可以在多個事件源上等待,並且避免了線程切換的性能開銷和編程複雜性;
- 事件的串行化對應用是透明的,可以順序的同步執行而不需要加鎖;
- 事務分離:將與應用無關的多路複用、分配機制和與應用相關的回調函數分離開來。
- 缺點:
- Reactor處理耗時長的操作會造成事件分發的阻塞,影響到後續事件的處理;
1.4 Reactor模型的應用
1.4.1 Reactor模型在Java NIO中
我們知道,Java NIO的網絡編程中,會有一個死循環執行Selector.select()操作,找出註冊到Selector上的Channel中已經準備好的IO事件,然後再對這些事件進行處理。
故而NIO中的Selector組件對應的就是Reactor模式的Synchronous Event Demultiplexer同步事件多路分解器。
選擇過後得到的SelectionKey,其實就對應的是上面的handle,也就是代表的一個個的IO事件。而NIO中並沒有進行事件分發和封裝處理器,因此Reactor模式中的其他組件NIO並沒有給出實現。
1.4.2 Reactor模式在Netty中
上面java NIO實現了reactor模式的兩個角色,Demultiplexer和handle。
而剩餘的三個角色,則由Netty給出了實現。
學習過Netty的應當知道,Netty服務端的編程有一個bossGroup和一個workerGroup,還需要編寫自己的ChannelHandler。
bossGroup和workerGroup都是一個事件循環組(EventLoopGroup,一般我們用的是NIOEventLoopGroup),每個事件循環組有多個事件循環(EventLoop,NIO對應的是NIOEventLoop)。
mainReactor對應Netty中配置的BossGroup線程組,主要負責接受客戶端連接的建立。一般只暴露一個服務端口,BossGroup線程組一般一個線程工作即可。
subReactor對應Netty中配置的WorkerGroup線程組,BossGroup線程組接受並建立完客戶端的連接後,將網絡socket轉交給WorkerGroup線程組,然後在WorkerGroup線程組內選擇一個線程,進行I/O的處理。WorkerGroup線程組主要處理I/O,一般設置2*CPU核數個線程。
bossGroup/workerGroup中的事件循環EventLoop就充當了Dispatcher的角色。
Netty中我們需要實現的Handler的頂層接口ChannelHandler對應的就是Event Handler角色,而我們添加進去的一個個的Handler對應的就是Concrete Event Handler。
注意,Netty的Handler是Concrete Event Handler的角色,Netty的SelectionKey纔是handle的角色。
最後我們總結一下Netty和Reactor角色的對應關係:
- Initiation Dispatcher ———— NioEventLoop
- Synchronous Event Demultiplexer ———— Selector
- Handle———— SelectionKey
- Event Handler ———— ChannelHandler
- ConcreteEventHandler ———— 具體的ChannelHandler的實現
- mainReactor ———— bossGroup(NioEventLoopGroup)線程組
- subReactor ———— workerGroup(NioEventLoopGroup)線程組
- acceptor ———— ServerBootstrapAcceptor
- ThreadPool ———— 用戶自定義線程池或者EventLoopGroup
2 Proactor事件處理模型
Reactor模式是非阻塞同步的I/O模型,Proactor模式是非阻塞異步I/O模型。它們的區別就在於異步二字,Proactor能實現真正的異步,但真正的異步IO也需要操作系統更強的支持。
在Reactor模型中,事件循環利用select等函數,主動地將已經就緒的handle找出,並通過回調通知給用戶線程,由用戶線程定義的回調函數自行讀取數據、處理數據。換句話說,用戶線程定義的回調函數中,要自行操作write(),將IO數據從內核空間寫到用戶空間。
而在Proactor模型中,就不是利用select等函數尋找就緒的handle了,要記得Proactor是異步IO模型,異步是什麼?我們不用主動調用select函數去尋找,而是直接等通知就行了。
並通過回調通知給用戶線程,但當用戶線程收到通知時,數據已經被內核讀取完畢,並放在了用戶線程指定的緩衝區內,內核在IO完成後通知用戶線程直接使用即可。也就是說,此時用戶線程定義的回調函數中,不需要自行操作write(),因爲IO數據已經從內核空間寫到用戶空間了。
Reactor中,write()操作還是同步的,由用戶線程自己解決,而Proactor中,真正做到了異步write(),它依賴於內部實現的異步操作處理器(Asynchronous Operation Processor)以及異步事件分離器(Asynchronous Event Demultiplexer)將IO操作與應用回調隔離。
相比於Reactor,Proactor並不十分常用,不少高性能併發服務程序使用IO多路複用模型+多線程任務處理的架構基本可以滿足需求。況且目前操作系統對異步IO的支持並非特別完善,更多的是採用Reactor模型模擬Proactor異步IO的方式:IO事件觸發時不直接通知用戶線程,而是非阻塞地將數據讀寫完畢後放到用戶指定的緩衝區中,再執行回調邏輯
Java7之後已經支持了異步IO,感興趣的讀者可以嘗試使用。
2.1 Proactor模型的架構
模型架構如上圖所示,我們來一一解釋這些控件:
其中Handle的含義,跟Reactor模型一致,Event Handler和Completion Handler也類似,它們都是事件的抽象封裝。
-
Handle:句柄,用來封裝或標識socket連接或是打開文件,你可以理解爲在模型中,它代表一個連接或者I/O流文件。
-
Completion Handler:完成事件接口,用來綁定某個handle和應用程序所提供的特定事件處理邏輯。Concrete Completion Handler是它的具體實現類,應用程序可針對不同的連接(handle)定製不同的回調函數(event)。
-
Completion Event Queue:完成事件的隊列;異步操作完成的結果放到隊列中等待後續使用。
-
Asynchronous Operation Processor:異步操作處理器;負責執行異步操作,一般由操作系統內核實現;綁定在Handle上,負責對監聽到的Handle事件進行回調喚醒對應的異步操作,生成對應的Completion Event並添加到完成事件的隊列中。
-
Asynchronous Operation:異步操作,主要用於處理程序中長時間持續操作;
-
Asynchronous Event Demultiplexer:異步事件多路分解器,和Reactor的Demultiplexer作用類似,但因爲Proactor是異步的,故而不需要Demultiplexer主動發起select輪詢,只要監視着完成事件的隊列,看是否有Completion Handler被異步插入到隊列中即可。
-
Proactor:Proactor模型的主動器,提供應用程序的事件循環,重複地從Demultiplexer中獲得就緒的Completion Handler,並調用其handle_event()方法。
-
Initiator:本地應用程序服務入口,初始化一個異步操作並註冊一個Completion Handler和一個帶有異步操作處理器的Proactor,當操作完成時通知它。
可以看到,Proactor角色的作用和Reactor的Dispatcher作用一致。它其實就是Proactor模型的Dispatcher,只不過叫法不一樣罷了。
也有部分文章將Dispatcher+Demultiplexer合併在Proactor角色裏,就像Reactor模型中有時用Reactor表示Dispatcher+Demultiplexer,如下圖:
2.2 Proactor模型的流程
由圖可以看到,Proactor模型的流程可以被歸納爲:
-
Initiator創建Proactor,Completion Event Queue和Completion Handler對象,並將其通過Asynchronous Operation Processor的exec_async_op()方法註冊到內核,並調用async_op()方法,開啓獨立的內核線程執行異步操作,實現真正的異步。
-
調用之後應用程序和異步操作處理就獨立運行;應用程序可以調用新的異步操作,而其它操作可以併發進行;
-
Initiator調用Proactor.handle_events()方法,啓動Proactor主動器,進行無限的事件循環,調用Demultiplexer.wait()方法,等待完成事件到來;
-
異步事件的就緒被Asynchronous Operation Processor監聽到,將其對應的Completion Handler加入Completion Event Queue隊列。
-
Demultiplexer監視着Completion Event Queue隊列,發現有數據,便將隊列中的Completion Handler返回。
-
Proactor從Demultiplexer得到就緒的Completion Handler,知道事件已經就緒,隨即調用Completion Handler.handle_event()方法。
雖然Proactor模式中每個異步操作都可以綁定一個Proactor對象(類似多Reactor的實現方式),但是一般在操作系統中,Proactor被實現爲Singleton模式,以便於集中化分發操作完成事件。
2.3 Proactor模型的優劣
- 優點:
- Proactor在理論上性能更高,能夠處理耗時長的併發場景。
- 缺點:
- Proactor實現邏輯複雜;依賴操作系統對異步的支持,目前實現了純異步操作的操作系統少,實現優秀的如windows IOCP,但由於其windows系統用於服務器的侷限性,目前應用範圍較小;而Unix/Linux系統對純異步的支持有限,應用事件驅動的主流還是通過select/epoll來實現。
2.4 Proactor模型的應用
Proactor因爲要有操作系統的支持,故而應用的場景並不多,Linux下高性能的網絡庫中大多使用的Reactor 模式去實現。比如Boost Asio在Linux下用epoll和select去模擬proactor模式。
除此之外,glibc實現的POSIX aio也是採用proactor模式
3 Reactor和Proactor的對比
3.1 區別
-
同步和異步
- Reactor無法實現真正的異步,他們所有操作都是同步的,所以有時爲了性能考慮,需要藉助線程池,將同步的操作併發化。
- Proactor基於操作系統的支持,可以實現真正的異步。
-
主動/被動的寫操作
- Reactor將handler放到select(),等待可寫就緒,事件就緒後,需要應用程序主動調用write(),將數據從內核空間寫入到用戶空間,寫完數據後再處理後續邏輯;
- Proactor調用aoi_write後立刻返回,由內核負責將數據從內核空間寫入到用戶空間,寫完後調用相應的回調函數處理後續邏輯。換句話說,Proactor通知應用線程的時候,數據已經在用戶空間就緒了。
-
主動/被動的處理方式
- Reactor模式被稱爲反應器,是一種被動的處理,即調用IO多路複用接口來做事件監聽(注意此時是在用戶空間調用select等函數),select等函數監聽事件就是一個等待的過程,有事件來了之後再“做出反應”。
- 而Proator模式的IO是系統級實現,是在內核中完成,讀的過程中,用戶空間的函數可以繼續處理,並沒有被阻塞;讀完之後調用相應用戶回調函數處理;
3.2 實現
Reactor實現了一個被動的事件分離和分發模型,服務等待請求事件的到來,再通過不受間斷的同步處理事件,從而做出反應;
Proactor實現了一個主動的事件分離和分發模型;這種設計允許多個任務併發的執行,從而提高吞吐量。
所以涉及到文件I/O或耗時I/O可以使用Proactor模式,或使用多線程模擬實現異步I/O的方式。
3.3 適用場景
Reactor:同時接收多個服務請求,並且依次同步的處理它們的事件驅動程序; Proactor:異步接收和同時處理多個服務請求的事件驅動程序。