reactor模式以及reactor模式在netty中的應用
一、概述
在上一篇部落格《Java 傳統IO和NIO》 中,我講述了java傳統IO與NIO之間的區別以及NIO給我們帶來的在網路程式設計上的效能的提升。然而,Java NIO以及後面要介紹的netty網路框架都是有一套理論在背後支撐的,那就是reactor模式的應用。
二、什麼是reactor模式?
reactor模式翻譯過來叫做反應器模式,通常我們都直接叫做reactor模式。
reactor模式是一種事件驅動模式,用於處理一個或者多個客戶端發過來的請求,服務端會有一個處理器對到來的請求進行分離,並且將這些請求分發給對應的請求處理器進行處理。reactor的結構圖大概如下圖所示:
三、reactor模式的角色構成
從上面的結構圖可以看出,Reactor模式由五中角色構成。分別是Handle(控制代碼或者描述符)、Synchronous Event Demultiplexer(同步事件分離器)、Event Handler(事件處理器)、Concrete Event Handler(具體事件處理器)、Initiation Dispatcher(初始分發器)。
- Handle(控制代碼或者描述符):本質上表示一種資源,是由作業系統提供的;該資源用於表示一個個事件,比如說檔案描述符,或者針對於網路程式設計當中的Socket描述符。事件既可以來自於外部,也可以來自於內部;外部事件比如說客戶端的連結請求,客戶端發過來的資料等;內部事件比如說作業系統產生的定時器事件等。它本質上就是一個檔案描述符。Handle是事件產生的發源地。
- Synchronous Event Demultiplexer(同步事件分離器):它本身是一個系統呼叫,用於等待事件的發生(事件有可能是一個,也有可能是多個)。呼叫方在呼叫它的時候會被阻塞,一直阻塞到同步時間分離器上有時間產生為止。對於linux來說,同步事件分離器直接就是常用的I/O多路複用機制,比如說select、poll、epoll等。在Java NIO領域中,同步事件分離器對應的元件就是Selector;對應的阻塞方法就是select()方法。
- Event Handler(事件處理器):本身由多個回撥方法構成,這些回撥方法構成了與應用相關的對於某個事件的反饋機制。
- Concrete Event Handler(具體事件處理器)
- Initiation Dispatcher(初始分發器):它本身定義了一些規範,這些規範用於控制事件的排程方式,同時又提供了應用進行事件處理器的註冊、刪除等操作。它本身是整個事件處理器的核心所在,Initiation Dispatcher會通過同步事件分離器來等待事件的發生。一旦事件發生,Initiation Dispatcher首先會分離出每一個事件,然後呼叫事件處理器,最後呼叫相關的回撥方法來處理這些事件。
四、reactor模式的流程
還是根據上面的圖,可以看出reactor模式的流程:
-
當應用向Initiation Dispatcher註冊具體的事件處理器時,應用會標示出該事件處理器希望Initiation Dispatcher在某個事件發生時向其通知的該事件,該事件與Handle關聯。
-
Initiation Dispatcher會要求每個事件處理器向其傳遞內部的Handle。該Handle向作業系統標示了事件處理器。
-
當所有的事件處理器註冊完畢後,應用會呼叫handle_events方法來啟動Initiation Dispatcher的事件迴圈。這時,Initiation Dispatcher會將每個註冊的事件處理器的Handle合併起來,並使用同步事件分離器等待這些事件的發生。比如說,TCP協議層會使用select同步事件分離器操作來等待客戶端傳送的資料到達連線的socket handle上。
-
當與某個事件源對應的Handle變為ready狀態時,(比如說TCP socket變為等待讀狀態時),同步事件分離器就會通知Initiation Dispatcher。
-
Initiation Dispatcher會觸發事件處理器的回撥方法,從而響應這個處於ready狀態的Handle。當事件發生時,Initiation Dispatcher會將被事件源啟用的Handle作為[key]來尋找並分發恰當的事件處理器回撥方法。
-
Initiation Dispatcher會回撥事件處理器的handle_event回撥方法來執行特定於應用的功能(開發者自己所編寫的功能),從而響應這個事件。所發生的事件型別可以作為該方法引數並被該方法內部使用來執行額外的特定於服務的分離與分發。
五、reactor在java領域的應用
1. 傳統的java網路程式設計模型
採用java OIO的網路程式設計模型,客戶端與服務端建立好連線過後,服務端對每一個建立好的連線使用一個handler來處理,而每個handler都會繫結一個執行緒。
這樣做在連線的客戶端不多的情況下,也算是個不錯的選擇。而且連線的客戶端很多的情況下就會出現問題。
1) 每一個連線服務端都會產生一個執行緒,當併發量比較高的情況下,會產生大量的執行緒。
2) 在服務端很多執行緒的情況下,大量的執行緒的上下文切換是一個很大的開銷,會比較影響效能。
3) 與服務端連線建立後,連線上未必是時時刻刻都有資料進行傳輸的,但是建立的執行緒一直都在,會造成服務端執行緒資源的一個極大的浪費。
2. 經典的reactor模式設計
由於java OIO的網路程式設計模型在客戶端很多的情況下回產生服務端執行緒數過多的問題,因此根據reactor模式做出了改進。
根據上圖,reactor角色對IO事件進行監聽和分發。當事件產生式,reactor會分發給對應的處理器進行處理。OIO存在的一個問題是IO阻塞,因此一個socket開啟一個執行緒來處理以防止一個連線IO的阻塞影響到其他的連線的處理。而這裡通過對於IO事件的監聽和分發,很好的解決了這個問題。服務端只需要一個IO執行緒就能處理多個客戶端的連線。這就解決了OIO的阻塞問題和多個客戶端連線而導致服務端執行緒數過多的問題。
但是這種模型還是有缺陷的,那就是所有的客戶端的請求都由一個執行緒來進行處理,當併發量比較大的情況下,服務端的處理效能肯定會下降,因為服務端每次只能處理一個請求,其他的請求只能等待。
3. 多執行緒版本的Reactor模式設計
上面的單執行緒reactor模式因為服務端只有一個執行緒處理IO和業務邏輯,服務端效能肯定受到限制。因此就有了多執行緒版本:
如上圖所示,reactor還是一個執行緒,負責監聽IO事件以及分發。只不過業務邏輯處理部分使用了一個執行緒池來進行處理。這樣就解決了服務端單執行緒處理請求而帶來的效能瓶頸。
但是這樣還是有問題,這樣會把效能的瓶頸轉移到IO處理上。因為IO事件的監聽和分發採用的還是單個執行緒,在併發量比較高的情況下,這個也是比較影響效能的。這是否還有繼續優化的空間呢?
4. 多reactor的多執行緒版本
我們知道reactor主要是負責IO事件的監聽和分發。單個reactor單個執行緒這種模式在併發量比較高的情況下,會存在效能瓶頸。那麼改進的方案顯然就是採用多個reactor。
上圖所示的mainReactor和subReactor都可能包含多個Selector(Java NIO),而每個Selector都是跟一個執行緒繫結的。這樣就解決了單個reactor單個執行緒所帶來的效能問題。實際上netty就是採用的這種設計,雖然具體的實現細節有些不一樣,但是總體思想是一樣的。
六、reactor模式在java NIO和netty中的應用
1. NIO與reactor模式的對應
我們知道,NIO的網路程式設計中,會有一個死迴圈執行Selector.select()操作,選擇出注冊到Selector上的Channel已經準備好的感興趣的IO事件,然後再對這些事件進行處理。而NIO中的Selector元件對應的就是Reactor模式的同步事件分離器。選擇過後得到的SelectionKey,其實就對應的是上面的控制代碼,也就是代表的一個個的IO事件。而NIO中並沒有進行事件分發和封裝處理器,因此Reactor模式中的其他元件NIO並沒有給出實現。
2. Reactor模式在netty中的應用
上面java NIO實現了reactor模式的兩個角色,而剩餘的三個角色Netty給出了實現。學習過netty的應當知道,netty服務端的程式設計有一個bossGroup一個workerGroup,還需要編寫自己的ChannelHandler。而bossGroup和workerGroup都是一個事件迴圈組(EventLoopGroup,一般我們用的是NIOEventLoopGroup),每個事件迴圈組有多個事件迴圈(EventLoop,NIO對應的是NIOEventLoop)。而bossGroup中的某一個事件迴圈就充當了Initiation Dispatcher(初始分發器)的角色,Netty中我們需要實現的Handler的頂層介面ChannelHandler對應的就是Event Handler(事件處理器)角色,而我們新增進去的一個個的Handler對應的就是Concrete Event Handler(具體事件處理器)。
結構上的對應:
Initiation Dispatcher ———— NioEventLoop
Synchronous EventDemultiplexer ———— Selector
Handle———— SelectionKey
Event Handler ———— ChannelHandler
ConcreteEventHandler ———— 具體的ChannelHandler的實現
上面分析的最後一個模型圖上的角色對應:
Netty服務端使用了“多Reactor執行緒模式”
mainReactor ———— bossGroup(NioEventLoopGroup) 中的某個NioEventLoop
subReactor ———— workerGroup(NioEventLoopGroup) 中的某個NioEventLoop
acceptor ———— ServerBootstrapAcceptor
ThreadPool ———— 使用者自定義執行緒池或者EventLoopGroup
七、總結
本部落格介紹了reactor模式的理論基礎以及reactor在java 在netty中的實現。特別是在netty中,reactor模式得到了很好的實現。而netty也是一個非常優秀的網路通訊框架,無論是從框架本身的設計,還是其對於效能上的極致壓縮來說,都是非常棒的。後面我會陸續寫一些部落格來對netty進行介紹。