Reactor模式,是大多數IO相關組件如Netty、Redis在使用的IO模式,為什么需要這種模式,它是如何設計來解決高性能並發的呢?
最最原始的網絡編程思路就是服務器用一個while循環,不斷監聽端口是否有新的套接字連接,如果有,那么就調用一個處理函數處理,類似:
while(true){
socket = accept();
handle(socket)
}
這種方法的最大問題是無法並發,效率太低,如果當前的請求沒有處理完,那么后面的請求只能被阻塞,服務器的吞吐量太低。
之后,想到了使用多線程,也就是很經典的connection per thread,每一個連接用一個線程處理,類似:
while(true){
socket = accept();
new thread(socket);
}
tomcat服務器的早期版本確實是這樣實現的。多線程的方式確實一定程度上極大地提高了服務器的吞吐量,因為之前的請求在read阻塞以后,不會影響到后續的請求,因為他們在不同的線程中。這也是為什么通常會講“一個線程只能對應一個socket”的原因。最開始對這句話很不理解,線程中創建多個socket不行嗎?語法上確實可以,但是實際上沒有用,每一個socket都是阻塞的,所以在一個線程里只能處理一個socket,就算accept了多個也沒用,前一個socket被阻塞了,后面的是無法被執行到的。
缺點在於資源要求太高,系統中創建線程是需要比較高的系統資源的,如果連接數太高,系統無法承受,而且,線程的反復創建-銷毀也需要代價。
線程池本身可以緩解線程創建-銷毀的代價,這樣優化確實會好很多,不過還是存在一些問題的,就是線程的粒度太大。每一個線程把一次交互的事情全部做了,包括讀取和返回,甚至連接,表面上似乎連接不在線程里,但是如果線程不夠,有了新的連接,也無法得到處理,所以,目前的方案線程里可以看成要做三件事,連接,讀取和寫入。
線程同步的粒度太大了,限制了吞吐量。應該把一次連接的操作分為更細的粒度或者過程,這些更細的粒度是更小的線程。整個線程池的數目會翻倍,但是線程更簡單,任務更加單一。這其實就是Reactor出現的原因,在Reactor中,這些被拆分的小線程或者子過程對應的是handler,每一種handler會出處理一種event。這里會有一個全局的管理者selector,我們需要把channel注冊感興趣的事件,那么這個selector就會不斷在channel上檢測是否有該類型的事件發生,如果沒有,那么主線程就會被阻塞,否則就會調用相應的事件處理函數即handler來處理。典型的事件有連接,讀取和寫入,當然我們就需要為這些事件分別提供處理器,每一個處理器可以采用線程的方式實現。一個連接來了,顯示被讀取線程或者handler處理了,然后再執行寫入,那么之前的讀取就可以被后面的請求復用,吞吐量就提高了。
幾乎所有的網絡連接都會經過讀請求內容——》解碼——》計算處理——》編碼回復——》回復的過程,Reactor模式的的演化過程如下:
這種模型由於IO在阻塞時會一直等待,因此在用戶負載增加時,性能下降的非常快。
server導致阻塞的原因:
1、serversocket的accept方法,阻塞等待client連接,直到client連接成功。
2、線程從socket inputstream讀入數據,會進入阻塞狀態,直到全部數據讀完。
3、線程向socket outputstream寫入數據,會阻塞直到全部數據寫完。
改進:采用基於事件驅動的設計,當有事件觸發時,才會調用處理器進行數據處理。
Reactor:負責響應IO事件,當檢測到一個新的事件,將其發送給相應的Handler去處理。
Handler:負責處理非阻塞的行為,標識系統管理的資源;同時將handler與事件綁定。
Reactor為單個線程,需要處理accept連接,同時發送請求到處理器中。
由於只有單個線程,所以處理器中的業務需要能夠快速處理完。
改進:使用多線程處理業務邏輯。
將處理器的執行放入線程池,多線程進行業務處理。但Reactor仍為單個線程。
繼續改進:對於多個CPU的機器,為充分利用系統資源,將Reactor拆分為兩部分。
Using Multiple Reactors
mainReactor負責監聽連接,accept連接給subReactor處理,為什么要單獨分一個Reactor來處理監聽呢?因為像TCP這樣需要經過3次握手才能建立連接,這個建立連接的過程也是要耗時間和資源的,單獨分一個Reactor來處理,可以提高性能。
Reactor模式是什么,有哪些優缺點?
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模式結構
在解決了什么是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接口,實現特定事件處理邏輯。優點
1)響應快,不必為單個同步時間所阻塞,雖然Reactor本身依然是同步的;
2)編程相對簡單,可以最大程度的避免復雜的多線程及同步問題,並且避免了多線程/進程的切換開銷;
3)可擴展性,可以方便的通過增加Reactor實例個數來充分利用CPU資源;
4)可復用性,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模式。