在高性能的I/O設計中,有兩個比較著名的模式Reactor和Proactor模式,其中Reactor模式用於同步I/O,Proactor用於異步I/O操作。
Reactor模式稱之為響應器模式,通常用於NIO非阻塞IO的網絡通信框架中。
幾個概念:
- 什么是阻塞和非阻塞?
阻塞和非阻塞是針對於進程在訪問數據時,根據IO操作的就緒狀態而采取的不同方式,簡單來說是一種讀取或寫入操作函數的實現方式,阻塞方式下讀取或寫入函數將一直等待。非阻塞方式下,讀取和寫入函數會立即返回一個狀態值。
- 什么是同步和異步?
同步和異步是針對應用程序和內核的交互而言的,同步是指用戶進程觸發IO操作並等待或輪詢的查看IO操作是否就緒,異步是指用戶進程觸發IO操作以后便開始做自己的事情,當IO操作完成時會得到通知,換句話說異步的特點就是通知。
- 什么是IO模型?
一般而言,IO模型可以分為四種:同步阻塞、同步非阻塞、異步阻塞、異步非阻塞
-
同步阻塞IO是指用戶進程在發起一個IO操作后必須等待IO操作完成,只有當真正完成了IO操作后用戶進程才能運行。
-
同步非阻塞IO是指用戶進程發起一個IO操作后立即返回,程序也就可以做其他事情。但是用戶進程需要不時的詢問IO操作是否就緒,這就要求用戶進程不停的去詢問,從而引入不必要的CPU資源浪費。
-
異步阻塞IO是指應用發起一個IO操作后不必等待內核IO操作的完成,內核完成IO操作后會通知應用程序。這其實是同步和異步最關鍵的區別,同步必須等待或主動詢問IO操作是否完成,那么為什么說是阻塞呢?因為此時是通過
select系統調用來完成的,而select函數本身的實現方式是阻塞的,采用select函數的好處在於可以同時監聽多個文件句柄,從而提高系統的並發性。 -
異步非阻塞IO是指用戶進程只需要發起一個IO操作后立即返回,等IO操作真正完成后,應用系統會得到IO操作完成的通知,此時用戶進程只需要對數據進行處理即可,不需要進行實際的IO讀寫操作,因為真正的IO讀寫操作已經由內核完成。
NIO非阻塞IO處理流程
Acceptor注冊Selector並監聽accept事件- 當客戶端連接后會觸發
accept事件 - 服務器構建對應的
Channel並在其上注冊Selector,用於監聽讀寫事件。 - 當發生讀寫事件后進行相應的讀寫處理
NIO非阻塞IO的優點在於性能瓶頸高,缺點在於模型復雜、編碼復雜、需要處理半包問題。簡單來說非阻塞IO不需要一個連接建立一個線程,它可以在一個線程中處理所有的連接。但是由於是非阻塞的,所以應用無法知道什么時候消息讀完了,也就會存在半包的問題。
什么是半包問題呢?
TCP/IP在發送消息時可能會拆包,拆包會導致接收端無法得知什么時候接收到的數據是一個完整的數據。在BIO阻塞性IO模型中,當讀取步到數據后會阻塞,而在NIO非阻塞IO中則不會,所以需要自行進行處理。比如以換行符作為判斷依據,或者是定長消息發送,或者是自定義協議等。
什么是Reactor模式?
Reactor模式是處理並發I/O常見的一種模式,用於同步I/O,其中心思想是將所有要處理的I/O事件注冊到一個中心I/O多路復用器上,同時主線程阻塞在多路復用器上,一旦有I/O事件到來或是准備就緒,多路復用器將返回並將相應I/O事件分發到對應的處理器中。
Reactor是一種事件驅動機制,和普通函數調用不同的是應用程序不是主動的調用某個API來完成處理,恰恰相反的是Reactor逆置了事件處理流程,應用程序需提供相應的接口並注冊到Reactor上,如果有相應的事件發生,Reactor將主動調用應用程序注冊的接口(回調函數)。
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模式稱為反應器模式或應答者模式,是基於事件驅動的設計模式,擁有一個或多個並發輸入源,有一個服務處理器和多個請求處理器,服務處理器會同步的將輸入的請求事件以多路復用的方式分發給相應的請求處理器。
Reactor設計模式是一種為處理並發服務請求,並將請求提交到一個或多個服務處理程序的事件設計模式。當客戶端請求抵達后,服務處理程序使用多路分配策略,由一個非阻塞的線程來接收所有請求,然后將請求派發到相關的工作線程並進行處理的過程。
在事件驅動的應用中,將一個或多個客戶端的請求分離和調度給應用程序,同步有序地接收並處理多個服務請求。對於高並發系統經常會使用到Reactor模式,用來替代常用的多線程處理方式以節省系統資源並提高系統的吞吐量。
基礎
什么是C/S架構?
C表示Client客戶端S表示Server服務器,服務器管理着某種資源Resource,通過操作這種資源為客戶端提供服務。
C/S架構的工作流程
- 客戶端進程向服務器進程發送請求
- 服務器進程接收並處理請求
- 服務器進程向客戶端進程發送響應
- 客戶端進程處理響應
什么是套接字Socket?
- Socket原意為插口,所表達的意思是插口與插槽之間的關系。
- Socket是對TCP/IP編程的抽象,簡單來說,是
send socket插入到receive socket中以建立連接進行通信。
C/S架構中Socket之間是如何建立連接並通信的呢?
- 服務端Socket綁定
bind到指定的端口上后監聽listen客戶端的插入 - 客戶端Socket連接到
connect到服務端 - 當服務端
accept到客戶端連接后 - 客戶端與服務端之間收發信息開發通信
- 通信完成后客戶端與服務器關閉
close掉Socket
演化
當前分布式計算Web服務盛行天下,網絡服務的底層都離不開對Socket的操作,而它們都具有一個共同的結構。
不同於傳統IO的串行調度方式,NIO非阻塞IO操作會將整個服務請求划分為五個階段。
在網絡服務和分布式中對於網絡中請求的處理,處理流程大致可划分為五個階段。
read接收請求讀取數據decode數據解碼compute業務邏輯處理(計算處理)encode編碼回復send發送回復
在這五個階段中,以read和send階段IO操作最為頻繁。
在處理網絡請求時,通常具有兩種體系結構。
- 基於線程
thread-based architecture
基於線程的體系結構會使用多線程來處理客戶端的請求,每當接收一個請求便開啟一個獨立的線程來處理。這種方式雖然簡單直觀,但僅適用於並發訪問不大的場景。因為線程是需要占用一定的內存資源,而且操作系統在線程之間的切換也需要一定的開銷。當線程過多時顯然會降低網絡服務器的性能。另外,當線程在處理IO操作時,在等待輸出的這段時間內線程是處於空閑狀態,造成CPU資源浪費。
- 事件驅動
event-driver architecture
事件驅動體系結構是目前廣泛使用的一種方式,這種方式定義了一系列的事件處理程序來響應事件的發生,而且將服務端接收連接和事件處理分離,事件本身只是一種狀態的改變。在事件驅動的應用中,會將一個或多個客戶端的服務請求分離demultiplex和調度dispatch給應用程序。
Reactor設計模式是event-driven architecture的一種實現方式,用於處理多個客戶端並發的向服務器請求服務的場景。每種服務在服務器上可能由多個方法組成。Reactor會解耦並發請求的服務並分發給對應的時間處理器來處理。
從結構上看,Reactor類似於生產消費模式,也就是一個或多個生產者會將事件放入一個隊列中,一個或多個消費者主動從隊列中poll拉取事件進行處理。Reactor並沒有使用隊列來做緩沖,每當一個事件輸入到服務處理程序之后,服務處理程序會主動根據不同的事件類型將其分發給對應的請求處理程序進行處理。
Reactor模式和生產者和消費者之間最大的區別在於
-
生產者消費者模式是基於隊列
queue的實現,能夠解決生產端和消費端處理速度不同步的問題,隊列可以采用先有的MQ產品來實現。 -
Reactor模式是基於事件驅動模型,當接收到請求后會將請求封裝成事件,並將事件分發給相應處理事件的
handler,handler處理完成后將時間狀態修改為下一個狀態,再由Reactor將事件分發給能夠處理下一個狀態的handle進行處理。
Reactor模式與Observer觀察者模式在某些方面極為相似,當一個主體發生改變時,所有依屬體都將得到通知。不過觀察者模式與單個事件源關聯,而反應器模式則於多個事件源關聯。
Reactor模式的優點很明顯:解耦、提升復用性、模塊化、可移植性、事件驅動、細粒度的開發控制等。Reactor模式的缺點也很明顯:模型復雜,涉及到內部回調、多線程處理、不容易調試、需要操作系統底層支持,因此導致不同操作系統可能會產生不一樣的結果。總來而言,如果並發要求不是很高,可使用傳統的阻塞線程池足夠了。如果使用場景是產生瞬間大並發可使用Reactor模式來實現。
傳統服務模型
最原始的網絡編程思路是服務器使用一個while循環並不斷監聽端口是否有新的socket套接字連接,如果有就會去調用一個處理函數。
while(true) { socket = accept(); handle(socket); }
這種方式最大的問題是無法並發且效率太低,如果當前請求沒有處理完畢后續請求只能被阻塞,因此服務器的吞吐量太低。
導致服務器阻塞的原因是什么呢?
- 服務器
socket的accept方法將阻塞等待客戶端連接,直到客戶端連接成功。 - 線程從
socket inputstream套接字輸入流讀取數據並進入阻塞狀態,直到全部數據讀取完畢 - 線程向
socket outputstream套接字輸出流寫入數據並進入阻塞狀態,直到全部數據寫入完畢。
由於IO在阻塞時會處於等待狀態,因此在用戶負載增加時,性能下降的非常快。
多線程
改進的方式是使用多線程,也就是經典的connection per thread,每一個連接擁有一個線程處理。
while(true) { socket = accept(); new thread(socket); }
對於傳統的服務設計,每個抵達的請求系統會分配一個線程去處理,Tomcat服務器早期版本是這樣實現的。
當系統請求量瞬間暴增時(高並發情況下),會直接把系統拖垮,因為系統能夠創建線程的數量是有限的。
多線程並發模式采用一個連接一個線程的方式,優點是確實一定程度上提高了服務器的吞吐量,因為之前的請求在read讀阻塞后不會影響到后續的請求,由於它們在不同的線程中,而且一個線程只能對應一個套接字socket,每一個套接字socket都是阻塞的,所以一個線程中只能處理一個套接字。就算accept多個socket,如果前一個socket被阻塞其后的socket是無法被執行到的。
多線程的服務器模型
多線程並發模式的缺點在於資源要求太高,系統中創建線程是需要消耗系統資源的,如果連接數過高系統將無法承受。另外,線程反復被創建和銷毀也是需要代價的。
線程池
雖然利用線程池可以緩解線程創建和銷毀的代價,不過還是存在一些問題,線程的粒度太大。每一個線程會將一次交互操作全部處理完成,包括讀取和返回甚至是連接。表面上似乎連接不在線程里面,但是如果線程不夠,新連接將無法得到處理。所以線程的任務可以簡化為做三件事:連接、讀取、寫入。
顯然傳統一對一的線程處理無法滿足需求的變化,對此考慮使用線程池使得線程可以被復用,大大降低創建線程和銷毀線程的時間。然而,線程池並不能很好滿足高並發線程的需求。當海量請求抵達時線程池中的工作線程達到飽和狀態,此時可能就導致請求被拋棄,無法完成客戶端的請求。對此,考慮到將一次完整的請求切分為幾個小的任務,每個小任務都是非阻塞的,對於讀寫操作使用NIO非阻塞IO對其進行讀寫,不同的任務將被分配到與之關聯的處理程序上進行處理,每個處理器通過異步回調機制來實現。這樣可以大大提高系統吞吐量,減少響應時間。
由於線程同步的粒度太大限制了吞吐量,所以應該將一次連接操作拆分為更細的粒度或過程,這些更細的粒度則是更小的線程。這樣做之后,整個線程池中線程的數量將會翻倍增加,但線程更加簡單且任務更為單一。這也是Reactor出現的原因。
Reactor
在Reactor中這些被拆分的小線程或子過程對應的處理程序,每一種處理程序會去處理一種事件。Reactor中存在一個全局管理者Selector,開發者需要將Channel注冊到感興趣的事件上,Selector會不斷在Channel上檢測是否有該類型的事件發生,如果沒有主線程會被阻塞,否則會調用相應的事件處理函數來處理。
由於典型的事件包括連接、讀取、寫入,因此需要為這些事件分別提供對應的處理程序,每個處理程序可以采用線程的方式實現。一旦連接來了,而且顯示被讀取線程或處理程序處理了,則會再執行寫入。那么之前的讀取就可以被后面的請求復用,因此吞吐量就提高了。
傳統的thread per connection中線程在真正處理請求之間是需要從socket中讀取網絡請求,由於讀取完成之前線程本身是被阻塞的不能做任何事情,這就導致線程資源被占用,而線程資源本身很珍貴的,尤其是在處理高並發請求時。Rector模式指出在等待IO時,線程可以先退出,這樣就會因為有線程等待IO而占用資源。但是這樣原先的執行流程就沒法還原了。因此可以利用事件驅動的方式,要求線程在退出之前向event loop事件循環中注冊回調函數,這樣IO完成時event loop事件循環就可以調用回調函數完成剩下的操作。所以Reactor模式通過減少服務器的資源消耗提供並發能力。
細分
Reactor從線程池和Reactor的選擇上可細分為:Reactor單線程模型、Reactor多線程模型,Reactor主從模型
單線程Reactor模型
單線程的Reactor模式對於客戶端的所有請求使用一個專門的線程去處理,這個線程無限循環地監聽是否有客戶端的請求抵達,一旦收到客戶端的請求,就將其分發給響應處理程序進行處理。

事件驅動設計
采用基於事件驅動的設計,當有事件觸發時才會調用處理器進行數據處理。使用Reactor模式可以對線程的數量進行控制,可以使用一個線程去處理大量的事件。
-Reactor 負責響應IO事件,當檢測到一個新的事件會將其發送給相應的處理程序去處理。
Handler負責處理非阻塞的行為,標識系統管理的資源,同時將處理程序與事件綁定。
Reactor是單個線程,需要處理accept連接,同時發送請求到處理器中。由於只是單個線程,所以處理器中的業務需要能夠快速處理完畢。
單線程的Reactor與NIO流程類似,只是將消息相關處理獨立到Handler中。雖然NIO中一個線程可以支持所有的IO處理,但瓶頸也是顯而易見的。如果某個客戶端多次進行請求時在Handler中的處理速度較慢,那么后續的客戶端請求都會被積壓,導致響應變慢。所以需要引入Reactor多線程模型。
單線程的Reactor的特點是只有一個Reactor線程,也就是說只有一個Selector事件通知器,因此字節的讀取I/O和后續的業務處理process()均由Reactor線程來做,很顯然業務的處理影響后續事件的分發,所以引出多線程版本進行優化。
從性能角度來看,單線程的Reactor沒有過多的提升空間,因為IO和CPU的速度嚴重不匹配。
單線程的Reactor模式並沒有解決IO和CPU處理速度不匹配問題,所以多線程的Reactor模式引入了線程池的概念,將耗時的IO操作交由線程池處理,處理完畢后再同步到selectionkey中。
多線程Reactor模型
考慮到工作線程的復用,可以將工作線程設計線程池。將處理器的執行放入線程池,並使用多線程處理業務邏輯,Reactor仍然是單個線程。
Reactor讀線程模型是將Handler中的IO操作和非IO操作分開,操作IO的線程稱為IO線程,非IO操作的線程稱為工作線程。客戶端的請求會被直接丟到線程池中,因此不會發生堵塞。
多線程的Reactor的特點是一個Reactor線程和多個處理線程,將業務處理即process交給線程池進行了分離,Reactor線程只關注事件分發和字節的發送和讀取。需要注意的是,實際的發送和讀取還是由Reactor來處理。當在高並發環境下,有可能會出現連接來不及接收。
當用戶進一步增加時Reactor也會出現瓶頸,因為Reactor既要處理IO操作請求也要響應連接請求。為了分擔Reactor的負擔,可以引入主從Reactor模型。
主從Reactor模型
對於多個CPU的機器,為了充分利用系統資源會將Reactor拆分為兩部分。
- Main Reactor 負責監聽連接,將
accept連接交給Sub Reactor處理,主Reactor用於響應連接請求。 - Sub Reactor 處理
accept連接,從Reactor用於處理IO操作請求。
主從Reactor的特點是使用 一個Selector池,通常有一個主Reactor用於處理接收連接事件,多個從Reactor處理實際的IO。整體來看,分工合作,分而治之,非常高效。
為什么需要單獨拆分一個Reactor來處理監聽呢?
因為像TCP這樣需要經過3次握手才能建立連接,這個建立的過程也是需要消耗時間和資源的,單獨拆分一個Reactor來處理,可以提高性能。
優缺點
Reactor模式的核心是解決多請求問題,如果有特別多的請求同時發生,不會因為線程池被短時間占滿而拒絕服務。
Reactor模式的優點是什么呢?
- 響應快,不為單個同步時間所阻塞,雖然Reactor自身依然是同步的。
- 編程相對簡單,可以最大程度的避免復雜的多線程以及同步問題和多線程以及多進程的切換開銷。
- 可擴展性,可以方便的通過增加Reactor實例個數來充分利用CPU資源。
- 可復用性, Reactor框架本身與具體事件處理邏輯無關,具有很高的復用性。
Reactor模式的缺點是什么呢?
- 相比傳統的模型,Reactor增加了一定的復雜性,因而具有一定的門檻,並且不易於調試。
- Reactor模式需要底層的
Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系統的select系統調用支持。 - Reactor模式在IO讀寫數據時會在同一線程中實現,即使使用多個Reactor機制的情況下,那些共享一個Reactor的Channel如果出現一個長時間的數據讀寫,會影響這個Reactor中其他Channel的相應時間。例如在大文件傳輸時,IO操作會影響其他客戶端的時間,因而對於這種操作,使用傳統的
Thread-Per-Connection或許是一個更好的選擇,或者采用Proactor模式。
結構
Reactor中的核心組件有哪些呢?
- Reactor
IO事件的派發者,相當於有分發功能的Selector。 - Acceptor
接收客戶端連接並建立對應客戶端的Handler,向Reactor注冊此Handler。相當於NIO中建立連接的那個判斷分支。 - Handler
和一個客戶端通訊的實體,一般在基礎的Handler上會有更進一步的層次划分,用來抽象諸如decode、process、encode這些過程。相當於消息讀寫處理等操作類。
在Reactor模式中有五個關鍵的參與者:描述符handle、同步事件分離器demultiplexer、事件處理器接口event handler、具體的事件處理器、Reactor管理器
Reactor的結構
Reactor模式要求主線程(I/O處理單元)只負責監聽文件描述符上是否有事件發生,如果有的話立即將該事件通知給工作線程(邏輯單元)。除此之外,主線程不做任何其它實質性的工作。讀寫數據、接收新連接、處理客戶端請求均在工作線程中完成。
- Handle 文件描述符
Handle在Linux中一般稱為文件描述符,在Windows中稱為句柄,兩者含義一樣。Handle是事件的發源地。比如網絡socket、磁盤文件等。發生在Handle上的事件可以有connection、ready for read、ready for write等。
Handle是操作系統的句柄,是對資源在操作系統上的一種抽象,它可以是打開的文件、一個Socket連接、Timer定時器等。由於Rector模式一般使用在網絡編程中,因而這里一般指的是Socket Handle,也就是一個網絡連接(connection/channel)。這個channel注冊到同步事件分離器中,以監聽Handle中發生的事件,對ServerSocketChannel可以是CONNECT事件,對SocketChannel可以是read、write、close事件等。
- Synchronous Event Demultiplexer 同步(多路)事件分離器
同步事件分離器本質上是系統調用,比如Linux中的select、poll、epoll等。比如select()方法會一致阻塞直到文件描述符handle上有事件發生時才會返回。
無限循環等待新請求的到來,一旦發現有新的事件到來就會通知初始事件分發器去調取特定的時間處理器。
- Event Handler 事件處理器
事件處理器,定義一些回調方法或稱為鈎子函數,當handle文件描述符上有事件發生時,回調方法便會執行。供初始事件分發器回調使用。
- Concrete Event Handler 具體的事件處理器
具體的事件處理器,實現了Event Handler,在回調方法中實現具體的業務邏輯。
- Initiation Dispatcher 初始事件分發器
初始事件分發器,提供了注冊、刪除、轉發Event Handler的方法。當Synchronous Event Demultiplexer檢測到handler上有事件發生時,便會通知initiation dispatcher調用特定的event handler的回調方法。
初始事件分發器用於管理Event Handler,定義注冊、移除EventHandler等。它還作為Rector模式的入口調用Synchronous Event Demultiplexer同步多路事件分離器的select方法以阻塞等待事件返回,當阻塞等待返回時,根據事件發生的Handle將其分發給對應的Event Handle事件處理器進行處理,也就是回調EventHandler中的handle_event方法。
事件多路分解器
現代操作系統大多提供了一種本機機制,該機制通過一種有效的方式處理並發和非阻塞資源,這種機制稱為同步事件多路分解器或事件通知接口。
Reactor啟動流程
- 創建Reactor
- 注冊事件處理器
- 調用事件多路分發器進入無限事件循環
- 當操作系統通知某描述符狀態就緒時,事件分發器找出並調用此描述注冊的事件處理器。
使用同步IO模型(以epoll_wait為例)實現的Reactor模式的工作流程
- 主線程向
epoll內核事件表中注冊socket上的讀就緒事件 - 主線程調用
epoll_wait等待socket上有數據可讀 - 當
socket上有數據可讀時,epoll_wait通知主線程,主線程將socket可讀事件放入請求隊列。 - 休眠在請求隊列上的某個工作線程被喚醒,從
socket中讀取數據並處理客戶端請求,然后向epoll內核事件表中注冊該socket上的寫就緒事件。 - 主線程調用
epoll_wait等待socket可寫 - 當
socket可寫時epoll_wait通知主線程,主線程將socket可寫事件放入請求隊列。 - 休眠在請求隊列上的某個工作線程被喚醒,向
socket上寫入服務器處理客戶請求的結果。
案例
例如:使用Reactor實現的日志服務器
日志服務器中的Reactor模式實現分為兩部分
- 客戶端連接到日志服務器
- 客戶端向日志服務器寫入日志
例如:需要建立一個提供分布式日志服務的事件驅動服務器,客戶端向服務器發送請求記錄自己的狀態信息,信息包括錯誤通知、調試信息、表現診斷等。日志服務器對於收到的信息進行分類和分發,具體包括顯示屏顯示、打印機打印、數據庫存儲等。
為了保證數據可靠性,客戶端和服務器之間的通信協議通常選用TCP等面向連接的協議,通過IP和端口的四元組來確認客戶端和服務器。日志服務器被多個客戶端同時使用,為此日志服務器需要保證多用戶連接請求和日志記錄的並發性。
為了保證並發性,可采用多線程的方式去實現該服務器,即每個線程專門針對一個連接。然而使用多線程的方式實現服務器存在着以下問題:
- 效率
多線程導致的上下文切換、同步、數據移動等可能帶來效率的下降。
- 編程簡單性
多線程需要考慮復雜的並發設計,包括線程安全等諸多因素。
- 可移植性
多線程在不同的操作系統下是不同的,因此會影響到可移植性。
參考:
https://www.jianshu.com/p/458e4b276607
