1. 程式人生 > >Reactor模式詳解(轉)

Reactor模式詳解(轉)

      在學習netty原始碼以前,需要熟悉JDK 中的NIO,多執行緒;瞭解一些設計模式:例如工廠方法、抽象工廠、建造者、外觀等;還有NIO的Reactor和AIO的Proactor模式。下邊我轉了兩篇關於Reactor和Proactor相關內容的文章,感覺還不錯,幫助理解:

在學習Reactor模式之前,我們需要對“I/O的四種模型”以及“什麼是I/O多路複用”進行簡單的介紹,因為Reactor是一個使用了同步非阻塞的I/O多路複用機制的模式。

I/O的四種模型

I/0 操作 主要分成兩部分 ① 資料準備,將資料載入到核心快取 ② 將核心快取中的資料載入到使用者快取

  • Synchronous blocking I/O

    Typical flow of the synchronous blocking I/O model

  • Synchronous non-blocking I/0

    Typical flow of the synchronous non-blocking I/O model

  • Asynchronous blocking I/0

    Typical flow of the asynchronous blocking I/O model (select)

  • Asynchronous non-blocking I/0

    Typical flow of the asynchronous non-blocking I/O model

堵塞、非堵塞的區別是在於第一階段,即資料準備階段。無論是堵塞還是非堵塞,都是用應用主動找核心要資料,而read資料的過程是‘堵塞’的,直到資料讀取完。 同步、非同步的區別在於第二階段,若由請求者主動的去獲取資料,則為同步操作,需要說明的是:read/write操作也是‘堵塞’的,直到資料讀取完。 若資料的read都由kernel核心完成了(在核心read資料的過程中,應用程序依舊可以執行其他的任務),這就是非同步操作。

換句話說,BIO裡使用者最關心“我要讀”,NIO裡使用者最關心"我可以讀了",在AIO模型裡使用者更需要關注的是“讀完了”。 NIO一個重要的特點是:socket主要的讀、寫、註冊和接收函式,在等待就緒階段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但效能非常高)。 NIO是一種同步非阻塞的I/O模型,也是I/O多路複用的基礎。

I/O多路複用

I/O多路複用是指使用一個執行緒來檢查多個檔案描述符(Socket)的就緒狀態,比如呼叫select和poll函式,傳入多個檔案描述符,如果有一個檔案描述符就緒,則返回,否則阻塞直到超時。得到就緒狀態後進行真正的操作可以在同一個執行緒裡執行,也可以啟動執行緒執行(比如使用執行緒池)。

一般情況下,I/O 複用機制需要事件分發器。 事件分發器的作用,將那些讀寫事件源分發給各讀寫事件的處理者。 涉及到事件分發器的兩種模式稱為:Reactor和Proactor。 Reactor模式是基於同步I/O的,而Proactor模式是和非同步I/O相關的。本文主要介紹的就是 Reactor模式相關的知識。

經典的I/O服務設計 ———— BIO模式

?這就是經典的每連線對應一個執行緒的同步阻塞I/O模式。

  • 流程: ① 伺服器端的Server是一個執行緒,執行緒中執行一個死迴圈來阻塞的監聽客戶端的連線請求和通訊。 ② 當客戶端向伺服器端傳送一個連線請求後,伺服器端的Server會接受客戶端的請求,ServerSocket.accept()從阻塞中返回,得到一個與客戶端連線相對於的Socket。 ③ 構建一個handler,將Socket傳入該handler。建立一個執行緒並啟動該執行緒,線上程中執行handler,這樣與客戶端的所有的通訊以及資料處理都在該執行緒中執行。當該客戶端和伺服器端完成通訊關閉連線後,執行緒就會被銷燬。 ④ 然後Server繼續執行accept()操作等待新的連線請求。

  • 優點: ① 使用簡單,容易程式設計 ② 在多核系統下,能夠充分利用了多核CPU的資源。即,當I/O阻塞系統,但CPU空閒的時候,可以利用多執行緒使用CPU資源。

  • 缺點: 該模式的本質問題在於嚴重依賴執行緒,但執行緒Java虛擬機器非常寶貴的資源。隨著客戶端併發訪問量的急劇增加,執行緒數量的不斷膨脹將伺服器端的效能將急劇下降。 ① 執行緒生命週期的開銷非常高。執行緒的建立與銷燬並不是沒有代價的。在Linux這樣的作業系統中,執行緒本質上就是一個程序,建立和銷燬都是重量級的系統函式。 ② 資源消耗。記憶體:大量空閒的執行緒會佔用許多記憶體,給垃圾回收器帶來壓力。;CPU:如果你已經擁有足夠多的執行緒使所有CPU保持忙碌狀態,那麼再建立更過的執行緒反而會降低效能。 ③ 穩定性。在可建立執行緒的數量上存在一個限制。這個限制值將隨著平臺的不同而不同,並且受多個因素制約:a)JVM的啟動引數、b)Threa的建構函式中請求的棧大小、c)底層作業系統對執行緒的限制 等。如果破壞了這些限制,那麼很可能丟擲OutOfMemoryError異常。 ④ 執行緒的切換成本是很高的。作業系統發生執行緒切換的時候,需要保留執行緒的上下文,然後執行系統呼叫。如果執行緒數過高,不僅會帶來許多無用的上下文切換,還可能導致執行執行緒切換的時間甚至會大於執行緒執行的時間,這時候帶來的表現往往是系統負載偏高、CPU sy(系統CPU)使用率特別高,導致系統幾乎陷入不可用的狀態。 ⑤ 容易造成鋸齒狀的系統負載。一旦執行緒數量高但外部網路環境不是很穩定,就很容易造成大量請求的結果同時返回,啟用大量阻塞執行緒從而使系統負載壓力過大。 ⑥ 若是長連線的情況下並且客戶端與伺服器端互動並不頻繁的,那麼客戶端和伺服器端的連線會一直保留著,對應的執行緒也就一直存在在,但因為不頻繁的通訊,導致大量執行緒在大量時間內都處於空置狀態。

  • 適用場景:如果你有少量的連線使用非常高的頻寬,一次傳送大量的資料,也許典型的IO伺服器實現可能非常契合。

Reactor模式

Reactor模式(反應器模式)是一種處理一個或多個客戶端併發交付服務請求的事件設計模式。當請求抵達後,服務處理程式使用I/O多路複用策略,然後同步地派發這些請求至相關的請求處理程式。

Reactor結構

Reactor模式的角色構成(Reactor模式一共有5中角色構成):

  • Handle(控制代碼或描述符,在Windows下稱為控制代碼,在Linux下稱為描述符):本質上表示一種資源(比如說檔案描述符,或是針對網路程式設計中的socket描述符),是由作業系統提供的;該資源用於表示一個個的事件,事件既可以來自於外部,也可以來自於內部;外部事件比如說客戶端的連線請求,客戶端傳送過來的資料等;內部事件比如說作業系統產生的定時事件等。它本質上就是一個檔案描述符,Handle是事件產生的發源地。
  • Synchronous Event Demultiplexer(同步事件分離器):它本身是一個系統呼叫,用於等待事件的發生(事件可能是一個,也可能是多個)。呼叫方在呼叫它的時候會被阻塞,一直阻塞到同步事件分離器上有事件產生為止。對於Linux來說,同步事件分離器指的就是常用的I/O多路複用機制,比如說select、poll、epoll等。在Java NIO領域中,同步事件分離器對應的元件就是Selector;對應的阻塞方法就是select方法。
  • Event Handler(事件處理器):本身由多個回撥方法構成,這些回撥方法構成了與應用相關的對於某個事件的反饋機制。在Java NIO領域中並沒有提供事件處理器機制讓我們呼叫或去進行回撥,是由我們自己編寫程式碼完成的。Netty相比於Java NIO來說,在事件處理器這個角色上進行了一個升級,它為我們開發者提供了大量的回撥方法,供我們在特定事件產生時實現相應的回撥方法進行業務邏輯的處理,即,ChannelHandler。ChannelHandler中的方法對應的都是一個個事件的回撥。
  • Concrete Event Handler(具體事件處理器):是事件處理器的實現。它本身實現了事件處理器所提供的各種回撥方法,從而實現了特定於業務的邏輯。它本質上就是我們所編寫的一個個的處理器實現。
  • Initiation Dispatcher(初始分發器):實際上就是Reactor角色。它本身定義了一些規範,這些規範用於控制事件的排程方式,同時又提供了應用進行事件處理器的註冊、刪除等設施。它本身是整個事件處理器的核心所在,Initiation Dispatcher會通過Synchronous Event Demultiplexer來等待事件的發生。一旦事件發生,Initiation Dispatcher首先會分離出每一個事件,然後呼叫事件處理器,最後呼叫相關的回撥方法來處理這些事件。Netty中ChannelHandler裡的一個個回撥方法都是由bossGroup或workGroup中的某個EventLoop來呼叫的。  

Reactor模式流程

① 初始化Initiation Dispatcher,然後將若干個Concrete Event Handler註冊到Initiation Dispatcher中。當應用向Initiation Dispatcher註冊Concrete Event Handler時,會在註冊的同時指定感興趣的事件,即,應用會標識出該事件處理器希望Initiation Dispatcher在某些事件發生時向其發出通知,事件通過Handle來標識,而Concrete Event Handler又持有該Handle。這樣,事件 ————> Handle ————> Concrete Event Handler 就關聯起來了。 ② Initiation Dispatcher 會要求每個事件處理器向其傳遞內部的Handle。該Handle向作業系統標識了事件處理器。 ③ 當所有的Concrete Event Handler都註冊完畢後,應用會呼叫handle_events方法來啟動Initiation Dispatcher的事件迴圈。這是,Initiation Dispatcher會將每個註冊的Concrete Event Handler的Handle合併起來,並使用Synchronous Event Demultiplexer(同步事件分離器)同步阻塞的等待事件的發生。比如說,TCP協議層會使用select同步事件分離器操作來等待客戶端傳送的資料到達連線的socket handler上。 比如,在Java中通過Selector的select()方法來實現這個同步阻塞等待事件發生的操作。在Linux作業系統下,select()的實現中 a)會將已經註冊到Initiation Dispatcher的事件呼叫epollCtl(epfd, opcode, fd, events)註冊到linux系統中,這裡fd表示Handle,events表示我們所感興趣的Handle的事件;b)通過呼叫epollWait方法同步阻塞的等待已經註冊的事件的發生。不同事件源上的事件可能同時發生,一旦有事件被觸發了,epollWait方法就會返回;c)最後通過發生的事件找到相關聯的SelectorKeyImpl物件,並設定其發生的事件為就緒狀態,然後將SelectorKeyImpl放入selectedSet中。這樣一來我們就可以通過Selector.selectedKeys()方法得到事件就緒的SelectorKeyImpl集合了。 ④ 當與某個事件源對應的Handle變為ready狀態時(比如說,TCP socket變為等待讀狀態時),Synchronous Event Demultiplexer就會通知Initiation Dispatcher。 ⑤ Initiation Dispatcher會觸發事件處理器的回撥方法,從而響應這個處於ready狀態的Handle。當事件發生時,Initiation Dispatcher會將被事件源啟用的Handle作為『key』來尋找並分發恰當的事件處理器回撥方法。 ⑥ Initiation Dispatcher會回撥事件處理器的handle_event(type)回撥方法來執行特定於應用的功能(開發者自己所編寫的功能),從而相應這個事件。所發生的事件型別可以作為該方法引數並被該方法內部使用來執行額外的特定於服務的分離與分發。

Reactor模式的實現方式

單執行緒Reactor模式

流程: ① 伺服器端的Reactor是一個執行緒物件,該執行緒會啟動事件迴圈,並使用Selector來實現IO的多路複用。註冊一個Acceptor事件處理器到Reactor中,Acceptor事件處理器所關注的事件是ACCEPT事件,這樣Reactor會監聽客戶端向伺服器端發起的連線請求事件(ACCEPT事件)。 ② 客戶端向伺服器端發起一個連線請求,Reactor監聽到了該ACCEPT事件的發生並將該ACCEPT事件派發給相應的Acceptor處理器來進行處理。Acceptor處理器通過accept()方法得到與這個客戶端對應的連線(SocketChannel),然後將該連線所關注的READ事件以及對應的READ事件處理器註冊到Reactor中,這樣一來Reactor就會監聽該連線的READ事件了。或者當你需要向客戶端傳送資料時,就向Reactor註冊該連線的WRITE事件和其處理器。 ③ 當Reactor監聽到有讀或者寫事件發生時,將相關的事件派發給對應的處理器進行處理。比如,讀處理器會通過SocketChannel的read()方法讀取資料,此時read()操作可以直接讀取到資料,而不會堵塞與等待可讀的資料到來。 ④ 每當處理完所有就緒的感興趣的I/O事件後,Reactor執行緒會再次執行select()阻塞等待新的事件就緒並將其分派給對應處理器進行處理。

注意,Reactor的單執行緒模式的單執行緒主要是針對於I/O操作而言,也就是所以的I/O的accept()、read()、write()以及connect()操作都在一個執行緒上完成的。

但在目前的單執行緒Reactor模式中,不僅I/O操作在該Reactor執行緒上,連非I/O的業務操作也在該執行緒上進行處理了,這可能會大大延遲I/O請求的響應。所以我們應該將非I/O的業務邏輯操作從Reactor執行緒上解除安裝,以此來加速Reactor執行緒對I/O請求的響應。

改進:使用工作者執行緒池

與單執行緒Reactor模式不同的是,添加了一個工作者執行緒池,並將非I/O操作從Reactor執行緒中移出轉交給工作者執行緒池來執行。這樣能夠提高Reactor執行緒的I/O響應,不至於因為一些耗時的業務邏輯而延遲對後面I/O請求的處理。

使用執行緒池的優勢: ① 通過重用現有的執行緒而不是建立新執行緒,可以在處理多個請求時分攤線上程建立和銷燬過程產生的巨大開銷。 ② 另一個額外的好處是,當請求到達時,工作執行緒通常已經存在,因此不會由於等待建立執行緒而延遲任務的執行,從而提高了響應性。 ③ 通過適當調整執行緒池的大小,可以建立足夠多的執行緒以便使處理器保持忙碌狀態。同時還可以防止過多執行緒相互競爭資源而使應用程式耗盡記憶體或失敗。

注意,在上圖的改進的版本中,所以的I/O操作依舊由一個Reactor來完成,包括I/O的accept()、read()、write()以及connect()操作。 對於一些小容量應用場景,可以使用單執行緒模型。但是對於高負載、大併發或大資料量的應用場景卻不合適,主要原因如下: ① 一個NIO執行緒同時處理成百上千的鏈路,效能上無法支撐,即便NIO執行緒的CPU負荷達到100%,也無法滿足海量訊息的讀取和傳送; ② 當NIO執行緒負載過重之後,處理速度將變慢,這會導致大量客戶端連線超時,超時之後往往會進行重發,這更加重了NIO執行緒的負載,最終會導致大量訊息積壓和處理超時,成為系統的效能瓶頸;

多Reactor執行緒模式

Reactor執行緒池中的每一Reactor執行緒都會有自己的Selector、執行緒和分發的事件迴圈邏輯。 mainReactor可以只有一個,但subReactor一般會有多個。mainReactor執行緒主要負責接收客戶端的連線請求,然後將接收到的SocketChannel傳遞給subReactor,由subReactor來完成和客戶端的通訊。

流程: ① 註冊一個Acceptor事件處理器到mainReactor中,Acceptor事件處理器所關注的事件是ACCEPT事件,這樣mainReactor會監聽客戶端向伺服器端發起的連線請求事件(ACCEPT事件)。啟動mainReactor的事件迴圈。 ② 客戶端向伺服器端發起一個連線請求,mainReactor監聽到了該ACCEPT事件並將該ACCEPT事件派發給Acceptor處理器來進行處理。Acceptor處理器通過accept()方法得到與這個客戶端對應的連線(SocketChannel),然後將這個SocketChannel傳遞給subReactor執行緒池。 ③ subReactor執行緒池分配一個subReactor執行緒給這個SocketChannel,即,將SocketChannel關注的READ事件以及對應的READ事件處理器註冊到subReactor執行緒中。當然你也註冊WRITE事件以及WRITE事件處理器到subReactor執行緒中以完成I/O寫操作。Reactor執行緒池中的每一Reactor執行緒都會有自己的Selector、執行緒和分發的迴圈邏輯。 ④ 當有I/O事件就緒時,相關的subReactor就將事件派發給響應的處理器處理。注意,這裡subReactor執行緒只負責完成I/O的read()操作,在讀取到資料後將業務邏輯的處理放入到執行緒池中完成,若完成業務邏輯後需要返回資料給客戶端,則相關的I/O的write操作還是會被提交回subReactor執行緒來完成。

注意,所以的I/O操作(包括,I/O的accept()、read()、write()以及connect()操作)依舊還是在Reactor執行緒(mainReactor執行緒 或 subReactor執行緒)中完成的。Thread Pool(執行緒池)僅用來處理非I/O操作的邏輯。

多Reactor執行緒模式將“接受客戶端的連線請求”和“與該客戶端的通訊”分在了兩個Reactor執行緒來完成。mainReactor完成接收客戶端連線請求的操作,它不負責與客戶端的通訊,而是將建立好的連線轉交給subReactor執行緒來完成與客戶端的通訊,這樣一來就不會因為read()資料量太大而導致後面的客戶端連線請求得不到即時處理的情況。並且多Reactor執行緒模式在海量的客戶端併發請求的情況下,還可以通過實現subReactor執行緒池來將海量的連線分發給多個subReactor執行緒,在多核的作業系統中這能大大提升應用的負載和吞吐量。

Netty 與 Reactor模式

Netty的執行緒模式就是一個實現了Reactor模式的經典模式。

  • 結構對應: NioEventLoop ———— Initiation Dispatcher Synchronous EventDemultiplexer ———— Selector Evnet Handler ———— ChannelHandler ConcreteEventHandler ———— 具體的ChannelHandler的實現

  • 模式對應: Netty服務端使用了“多Reactor執行緒模式” mainReactor ———— bossGroup(NioEventLoopGroup) 中的某個NioEventLoop subReactor ———— workerGroup(NioEventLoopGroup) 中的某個NioEventLoop acceptor ———— ServerBootstrapAcceptor ThreadPool ———— 使用者自定義執行緒池

  • 流程: ① 當伺服器程式啟動時,會配置ChannelPipeline,ChannelPipeline中是一個ChannelHandler鏈,所有的事件發生時都會觸發Channelhandler中的某個方法,這個事件會在ChannelPipeline中的ChannelHandler鏈裡傳播。然後,從bossGroup事件迴圈池中獲取一個NioEventLoop來現實服務端程式繫結本地埠的操作,將對應的ServerSocketChannel註冊到該NioEventLoop中的Selector上,並註冊ACCEPT事件為ServerSocketChannel所感興趣的事件。 ② NioEventLoop事件迴圈啟動,此時開始監聽客戶端的連線請求。 ③ 當有客戶端向伺服器端發起連線請求時,NioEventLoop的事件迴圈監聽到該ACCEPT事件,Netty底層會接收這個連線,通過accept()方法得到與這個客戶端的連線(SocketChannel),然後觸發ChannelRead事件(即,ChannelHandler中的channelRead方法會得到回撥),該事件會在ChannelPipeline中的ChannelHandler鏈中執行、傳播。 ④ ServerBootstrapAcceptor的readChannel方法會該SocketChannel(客戶端的連線)註冊到workerGroup(NioEventLoopGroup) 中的某個NioEventLoop的Selector上,並註冊READ事件為SocketChannel所感興趣的事件。啟動SocketChannel所在NioEventLoop的事件迴圈,接下來就可以開始客戶端和伺服器端的通訊了。

後記

本文主要對Reactor模式進行詳細的解析,Netty中正是應用Reactor模式來實現非同步事件驅動網路應用框架的,所以對於Reactor模式的掌握在Netty的學習是至關重要的。 若文章有任何錯誤,望大家不吝指教:)

參考