轉載自:http://www.blogjava.net/DLevin/archive/2015/09/02/427045.html
前記
第一次聽到Reactor模式是三年前的某個晚上,一個室友突然跑過來問我什麼是Reactor模式?我上網查了一下,很多人都是給出NIO中的 Selector的例子,而且就是NIO裡Selector多路複用模型,隻是給它起了一個比較fancy的名字而已,雖然它引入了EventLoop概 念,這對我來說是新的概念,但是代碼實作卻是一樣的,因而我并沒有很在意這個模式。然而最近開始讀Netty源碼,而Reactor模式是很多介紹Netty的文章中被大肆宣傳的模式,因而我再次問自己,什麼是Reactor模式?本文就是對這個問題關于我的一些了解和嘗試着來解答。
什麼是Reactor模式
要回答這個問題,首先當然是求助Google或Wikipedia,其中Wikipedia上說:“The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.”。從這個描述中,我們知道Reactor模式首先是 事件驅動的,有一個或多個并發輸入源,有一個Service Handler,有多個Request Handlers ;這個Service Handler會同步的将輸入的請求(Event)多路複用的分發給相應的Request Handler。如果用圖來表達:

從結構上,這有點類似生産者消費者模式,即有一個或多個生産者将事件放入一個Queue中,而一個或多個消費者主動的從這個Queue中Poll事件來處理;而Reactor模式則并沒有Queue來做緩沖,每當一個Event輸入到Service Handler之後,該Service Handler會主動的根據不同的Event類型将其分發給對應的Request Handler來處理。
更學術的,這篇文章( Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events )上說:“The Reactor design pattern handles service requests that are delivered concurrently to an application by one or more clients. Each service in an application may consistent of several methods and is represented by a separate event handler that is responsible for dispatching service-specific requests. Dispatching of event handlers is performed by an initiation dispatcher, which manages the registered event handlers. Demultiplexing of service requests is performed by a synchronous event demultiplexer. Also known as Dispatcher, Notifier ”。這段描述和Wikipedia上的描述類似,有多個輸入源,有多個不同的EventHandler(RequestHandler)來處理不同的請求,Initiation Dispatcher用于管理EventHander,EventHandler首先要注冊到Initiation Dispatcher中,然後Initiation Dispatcher根據輸入的Event分發給注冊的EventHandler;然而Initiation Dispatcher并不監聽Event的到來,這個工作交給Synchronous Event Demultiplexer來處理。
Reactor模式結構
在解決了什麼是Reactor模式後,我們來看看Reactor模式是由什麼子產品構成。圖是一種比較簡潔形象的表現方式,因而先上一張圖來表達各個子產品的名稱和他們之間的關系:
Handle: 即作業系統中的句柄,是對資源在作業系統層面上的一種抽象,它可以是打開的檔案、一個連接配接(Socket)、Timer等。由于Reactor模式一般使用在網絡程式設計中,因而這裡一般指Socket Handle,即一個網絡連接配接(Connection,在Java NIO中的Channel)。這個Channel注冊到Synchronous Event Demultiplexer中,以監聽Handle中發生的事件,對ServerSocketChannnel可以是CONNECT事件,對SocketChannel可以是READ、WRITE、CLOSE事件等。
Synchronous Event Demultiplexer: 阻塞等待一系列的Handle中的事件到來,如果阻塞等待傳回,即表示在傳回的Handle中可以不阻塞的執行傳回的事件類型。這個子產品一般使用作業系統的select來實作。在Java NIO中用Selector來封裝,當Selector.select()傳回時,可以調用Selector的selectedKeys()方法擷取Set<SelectionKey>,一個SelectionKey表達一個有事件發生的Channel以及該Channel上的事件類型。上圖的“Synchronous Event Demultiplexer ---notifies--> Handle”的流程如果是對的,那内部實作應該是select()方法在事件到來後會先設定Handle的狀态,然後傳回。不了解内部實作機制,因而保留原圖。
Initiation Dispatcher: 用于管理Event Handler,即EventHandler的容器,用以注冊、移除EventHandler等;另外,它還作為Reactor模式的入口調用Synchronous Event Demultiplexer的select方法以阻塞等待事件傳回,當阻塞等待傳回時,根據事件發生的Handle将其分發給對應的Event Handler處理,即回調EventHandler中的handle_event()方法。
Event Handler: 定義事件處理方法:handle_event(),以供InitiationDispatcher回調使用。
Concrete Event Handler: 事件EventHandler接口,實作特定事件處理邏輯。
Reactor模式子產品之間的互動
簡單描述一下Reactor各個子產品之間的互動流程,先從序列圖開始:
1. 初始化InitiationDispatcher,并初始化一個Handle到EventHandler的Map。
2. 注冊EventHandler到InitiationDispatcher中,每個EventHandler包含對相應Handle的引用,進而建立Handle到EventHandler的映射(Map)。
3. 調用InitiationDispatcher的handle_events()方法以啟動Event Loop。在Event Loop中,調用select()方法(Synchronous Event Demultiplexer)阻塞等待Event發生。
4. 當某個或某些Handle的Event發生後,select()方法傳回,InitiationDispatcher根據傳回的Handle找到注冊的EventHandler,并回調該EventHandler的handle_events()方法。
5. 在EventHandler的handle_events()方法中還可以向InitiationDispatcher中注冊新的Eventhandler,比如對AcceptorEventHandler來,當有新的client連接配接時,它會産生新的EventHandler以處理新的連接配接,并注冊到InitiationDispatcher中。
Reactor模式實作
在 Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events 中,一直以Logging Server來分析Reactor模式,這個Logging Server的實作完全遵循這裡對Reactor描述,因而放在這裡以做參考。Logging Server中的Reactor模式實作分兩個部分:Client連接配接到Logging Server和Client向Logging Server寫Log。因而對它的描述分成這兩個步驟。
Client連接配接到Logging Server
1. Logging Server注冊LoggingAcceptor到InitiationDispatcher。
2. Logging Server調用InitiationDispatcher的handle_events()方法啟動。
3. InitiationDispatcher内部調用select()方法(Synchronous Event Demultiplexer),阻塞等待Client連接配接。
4. Client連接配接到Logging Server。
5. InitiationDisptcher中的select()方法傳回,并通知LoggingAcceptor有新的連接配接到來。
6. LoggingAcceptor調用accept方法accept這個新連接配接。
7. LoggingAcceptor建立新的LoggingHandler。
8. 新的LoggingHandler注冊到InitiationDispatcher中(同時也注冊到Synchonous Event Demultiplexer中),等待Client發起寫log請求。
Client向Logging Server寫Log
1. Client發送log到Logging server。
2. InitiationDispatcher監測到相應的Handle中有事件發生,傳回阻塞等待,根據傳回的Handle找到LoggingHandler,并回調LoggingHandler中的handle_event()方法。
3. LoggingHandler中的handle_event()方法中讀取Handle中的log資訊。
4. 将接收到的log寫入到日志檔案、資料庫等裝置中。
3.4步驟循環直到目前日志處理完成。
5. 傳回到InitiationDispatcher等待下一次日志寫請求。
在 Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events 有對Reactor模式的C++的實作版本,多年不用C++,因而略過。
Java NIO對Reactor的實作
在Java的NIO中,對Reactor模式有無縫的支援,即使用Selector類封裝了作業系統提供的Synchronous Event Demultiplexer功能。這個Doug Lea已經在 Scalable IO In Java 中有非常深入的解釋了,因而不再贅述,另外 這篇文章 對Doug Lea的 Scalable IO In Java 有一些簡單解釋,至少它的代碼格式比Doug Lea的PPT要整潔一些。
需要指出的是,不同這裡使用InitiationDispatcher來管理EventHandler,在Doug Lea的版本中使用SelectionKey中的Attachment來存儲對應的EventHandler,因而不需要注冊EventHandler這個步驟,或者設定Attachment就是這裡的注冊。而且在這篇文章中,Doug Lea從單線程的Reactor、Acceptor、Handler實作這個模式出發;演化為将Handler中的處理邏輯多線程化,實作類似Proactor模式,此時所有的IO操作還是單線程的,因而再演化出一個Main Reactor來處理CONNECT事件(Acceptor),而多個Sub Reactor來處理READ、WRITE等事件(Handler),這些Sub Reactor可以分别再自己的線程中執行,進而IO操作也多線程化。這個最後一個模型正是Netty中使用的模型。并且在 Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events 的9.5 Determine the Number of Initiation Dispatchers in an Application中也有相應的描述。
EventHandler接口定義
對EventHandler的定義有兩種設計思路:single-method設計和multi-method設計:
A single-method interface: 它将Event封裝成一個Event Object,EventHandler隻定義一個handle_event(Event event)方法。這種設計的好處是有利于擴充,可以後來友善的添加新的Event類型,然而在子類的實作中,需要判斷不同的Event類型而再次擴充成 不同的處理方法,從這個角度上來說,它又不利于擴充。另外在Netty3的使用過程中,由于它不停的建立ChannelEvent類,因而會引起GC的不穩定。
A multi-method interface: 這種設計是将不同的Event類型在 EventHandler中定義相應的方法。這種設計就是Netty4中使用的政策,其中一個目的是避免ChannelEvent建立引起的GC不穩定, 另外一個好處是它可以避免在EventHandler實作時判斷不同的Event類型而有不同的實作,然而這種設計會給擴充新的Event類型時帶來非常 大的麻煩,因為它需要該接口。
關于Netty4對Netty3的改進可以參考 這裡 :
ChannelHandler with no event objectIn 3.x, every I/O operation created a
ChannelEvent
object. For each read / write, it additionally created a new
ChannelBuffer
. It simplified the internals of Netty quite a lot because it delegates resource management and buffer pooling to the JVM. However, it often was the root cause of GC pressure and uncertainty which are sometimes observed in a Netty-based application under high load.
4.0 removes event object creation almost completely by replacing the event objects with strongly typed method invocations. 3.x had catch-all event handler methods such as
handleUpstream()
and
handleDownstream()
, but this is not the case anymore. Every event type has its own handler method now:
為什麼使用Reactor模式
歸功與Netty和Java NIO對Reactor的宣傳,本文慕名而學習的Reactor模式,因而已經預設Reactor具有非常優秀的性能,然而慕名歸慕名,到這裡,我還是要不得不問自己Reactor模式的好處在哪裡?即為什麼要使用這個Reactor模式?在 Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events 中是這麼說的:
Reactor Pattern優點
Separation of concerns: The Reactor pattern decouples application-independent demultiplexing and dispatching mechanisms from application-specific hook method functionality. The application-independent mechanisms become reusable components that know how to demultiplex events and dispatch the appropriate hook methods defined by Event Handlers. In contrast, the application-specific functionality in a hook method knows how to perform a particular type of service.
Improve modularity, reusability, and configurability of event-driven applications: The pattern decouples application functionality into separate classes. For instance, there are two separate classes in the logging server: one for establishing connections and another for receiving and processing logging records. This decoupling enables the reuse of the connection establishment class for different types of connection-oriented services (such as file transfer, remote login, and video-on-demand). Therefore, modifying or extending the functionality of the logging server only affects the implementation of the logging handler class.
Improves application portability: The Initiation Dispatcher’s interface can be reused independently of the OS system calls that perform event demultiplexing. These system calls detect and report the occurrence of one or more events that may occur simultaneously on multiple sources of events. Common sources of events may in- clude I/O handles, timers, and synchronization objects. On UNIX platforms, the event demultiplexing system calls are called select and poll [1]. In the Win32 API [16], the WaitForMultipleObjects system call performs event demultiplexing.
Provides coarse-grained concurrency control: The Reactor pattern serializes the invocation of event handlers at the level of event demultiplexing and dispatching within a process or thread. Serialization at the Initiation Dispatcher level often eliminates the need for more complicated synchronization or locking within an application process.
這些貌似是很多模式的共性:解耦、提升複用性、子產品化、可移植性、事件驅動、細力度的并發控制等,因而并不能很好的說明什麼,特别是它鼓吹的對性能的提升,這裡并沒有展現出來。當然在這篇文章的開頭有描述過另一種直覺的實作:Thread-Per-Connection,即傳統的實作,提到了這個傳統實作的以下問題:
Thread Per Connection缺點
Efficiency: Threading may lead to poor performance due to context switching, synchronization, and data movement [2];
Programming simplicity: Threading may require complex concurrency control schemes;
Portability: Threading is not available on all OS platforms. 對于性能,它其實就是第一點關于Efficiency的描述,即線程的切換、同步、資料的移動會引起性能問題。也就是說從性能的角度上,它最大的提升就是減少了性能的使用,即不需要每個Client對應一個線程。我的了解,其他業務邏輯處理很多時候也會用到相同的線程,IO讀寫操作相對CPU的操作還是要慢很多,即使Reactor機制中每次讀寫已經能保證非阻塞讀寫,這裡可以減少一些線程的使用,但是這減少的線程使用對性能有那麼大的影響嗎?答案貌似是肯定的,這篇論文( SEDA: Staged Event-Driven Architecture - An Architecture for Well-Conditioned, Scalable Internet Service )對随着線程的增長帶來性能降低做了一個統計:
在這個統計中,每個線程從磁盤中讀8KB資料,每個線程讀同一個檔案,因而資料本身是緩存在作業系統内部的,即減少IO的影響;所有線程是事先配置設定的,不會有線程啟動的影響;所有任務在測試内部産生,因而不會有網絡的影響。該統計資料運作環境:Linux 2.2.14,2GB記憶體,4-way 500MHz Pentium III。從圖中可以看出,随着線程的增長,吞吐量線上程數為8個左右的時候開始線性下降,并且到64個以後而迅速下降,其相應事件也線上程達到256個後指數上升。即1+1<2,因為線程切換、同步、資料移動會有性能損失,線程數增加到一定數量時,這種性能影響效果會更加明顯。
對于這點,還可以參考 C10K Problem ,用以描述同時有10K個Client發起連接配接的問題,到2010年的時候已經出現10M Problem了。
當然也有人說: Threads are expensive are no longer valid .在不久的将來可能又會發生不同的變化,或者這個變化正在、已經發生着?沒有做過比較仔細的測試,因而不敢随便斷言什麼,然而本人觀點,即使線程變的影響并沒有以前那麼大,使用Reactor模式,甚至時SEDA模式來減少線程的使用,再加上其他解耦、子產品化、提升複用性等優點,還是值得使用的。
Reactor模式的缺點
Reactor模式的缺點貌似也是顯而易見的:
1. 相比傳統的簡單模型,Reactor增加了一定的複雜性,因而有一定的門檻,并且不易于調試。
2. Reactor模式需要底層的Synchronous Event Demultiplexer支援,比如Java中的Selector支援,作業系統的select系統調用支援,如果要自己實作Synchronous Event Demultiplexer可能不會有那麼高效。
3. Reactor模式在IO讀寫資料時還是在同一個線程中實作的,即使使用多個Reactor機制的情況下,那些共享一個Reactor的Channel如果出現一個長時間的資料讀寫,會影響這個Reactor中其他Channel的相應時間,比如在大檔案傳輸時,IO操作就會影響其他Client的相應時間,因而對這種操作,使用傳統的Thread-Per-Connection或許是一個更好的選擇,或則此時使用Proactor模式。
參考
Reactor Pattern WikiPedia
Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events
Scalable IO In Java
C10K Problem WikiPedia