爲了應對高併發的服務器端開發,微軟在2009年提出了一種更優雅地實現異步編程的方式Reactive Programming
即反應式編程。隨後其餘技術緊隨其後,好比ES6經過引入相似的異步編程方式等。react
在高性能的I/O設計中,有兩個比較著名的模式Reactor
和Proactor
模式,其中Reactor
模式用於同步I/O,Proactor
用於異步I/O操做。shell
Reactor模式稱之爲響應器模式,一般用於NIO
非阻塞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?
send socket
插入到receive socket
中以創建鏈接進行通訊。C/S架構中Socket之間是如何創建鏈接並通訊的呢?
bind
到指定的端口上後監聽listen
客戶端的插入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中存在一個全局管理者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
負責響應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讀線程模型是將Handler中的IO操做和非IO操做分開,操做IO的線程稱爲IO線程,非IO操做的線程稱爲工做線程。客戶端的請求會被直接丟到線程池中,所以不會發生堵塞。
多線程的Reactor的特色是一個Reactor線程和多個處理線程,將業務處理即process
交給線程池進行了分離,Reactor線程只關注事件分發和字節的發送和讀取。須要注意的是,實際的發送和讀取仍是由Reactor來處理。當在高併發環境下,有可能會出現鏈接來不及接收。
當用戶進一步增長時Reactor也會出現瓶頸,由於Reactor既要處理IO操做請求也要響應鏈接請求。爲了分擔Reactor的負擔,能夠引入主從Reactor模型。
對於多個CPU的機器,爲了充分利用系統資源會將Reactor拆分爲兩部分。
accept
鏈接交給Sub Reactor
處理,主Reactor用於響應鏈接請求。accept
鏈接,從Reactor用於處理IO操做請求。主從Reactor的特色是使用 一個Selector
池,一般有一個主Reactor
用於處理接收鏈接事件,多個從Reactor
處理實際的IO。總體來看,分工合做,分而治之,很是高效。
爲何須要單獨拆分一個Reactor來處理監聽呢?
由於像TCP這樣須要通過3次握手才能創建鏈接,這個創建的過程也是須要消耗時間和資源的,單獨拆分一個Reactor來處理,能夠提升性能。
Reactor模式的核心是解決多請求問題,若是有特別多的請求同時發生,不會由於線程池被短期佔滿而拒絕服務。通常實現多請求的模塊,會採用線程池的實現方案,這種方案對於併發量不是特別大的場景是足夠用的,好比單機TPS 1000如下都是夠用的。
線程池方案的最大缺點是:若是瞬間有大併發,則會一會兒耗滿線程,整個服務將會陷入阻塞中,後續請求沒法介入。基於Reactor
模式實現的方案,會有一個Dispatcher
先接收事件event
,而後快速分發給相應的耗時eventHandler
處理器去處理,這樣就不會阻塞請求的接收。
Reactor模式的優勢是什麼呢?
Reactor模式的缺點是什麼呢?
Synchronous Event Demultiplexer
支持,好比Java中的Selector支持,操做系統的select系統調用支持。Thread-Per-Connection
或許是一個更好的選擇,或者採用Proactor
模式。Reactor中的核心組件有哪些呢?
Selector
。Handler
,向Reactor註冊此Handler
。至關於NIO中創建鏈接的那個判斷分支。Handler
上會有更進一步的層次劃分,用來抽象諸如decode
、process
、encode
這些過程。至關於消息讀寫處理等操做類。在Reactor模式中有五個關鍵的參與者:描述符handle
、同步事件分離器demultiplexer
、事件處理器接口event handler
、具體的事件處理器、Reactor管理器
Reactor的結構
Reactor模式要求主線程(I/O處理單元)只負責監聽文件描述符上是否有事件發生,若是有的話當即將該事件通知給工做線程(邏輯單元)。除此以外,主線程不作任何其它實質性的工做。讀寫數據、接收新鏈接、處理客戶端請求均在工做線程中完成。
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
事件等。
同步事件分離器本質上是系統調用,好比Linux中的select
、poll
、epoll
等。好比select()
方法會一致阻塞直到文件描述符handle
上有事件發生時纔會返回。
無限循環等待新請求的到來,一旦發現有新的事件到來就會通知初始事件分發器去調取特定的時間處理器。
事件處理器,定義一些回調方法或稱爲鉤子函數,當handle
文件描述符上有事件發生時,回調方法便會執行。供初始事件分發器回調使用。
具體的事件處理器,實現了Event Handler,在回調方法中實現具體的業務邏輯。
初始事件分發器,提供了註冊、刪除、轉發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啓動流程
使用同步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和端口的四元組來確認客戶端和服務器。日誌服務器被多個客戶端同時使用,爲此日誌服務器須要保證多用戶鏈接請求和日誌記錄的併發性。
爲了保證併發性,可採用多線程的方式去實現該服務器,即每一個線程專門針對一個鏈接。然而使用多線程的方式實現服務器存在着如下問題:
多線程致使的上下文切換、同步、數據移動等可能帶來效率的降低。
多線程須要考慮複雜的併發設計,包括線程安全等諸多因素。
多線程在不一樣的操做系統下是不一樣的,所以會影響到可移植性。
因爲以上問題,多線程設計每每既不是最高效也不是最易於實現的方案,所以須要其餘方案來實現能夠並行請求的服務器。