兩種高效的服務器設計模型:Reactor和Proactor模型

Reactor模型

Reactor模式是處理併發I/O比較常見的一種模式,用於同步I/O,中心思想是將全部要處理的I/O事件註冊到一箇中心I/O多路複用器上,同時主線程/進程阻塞在多路複用器上;一旦有I/O事件到來或是準備就緒(文件描述符或socket可讀、寫),多路複用器返回並將事先註冊的相應I/O事件分發到對應的處理器中。react

  Reactor是一種事件驅動機制,和普通函數調用的不一樣之處在於:應用程序不是主動的調用某個API完成處理,而是偏偏相反,Reactor逆置了事件處理流程,應用程序須要提供相應的接口並註冊到Reactor上,若是相應的事件發生,Reactor將主動調用應用程序註冊的接口,這些接口又稱爲「回調函數」。用「好萊塢原則」來形容Reactor再合適不過了:不要打電話給咱們,咱們會打電話通知你。linux

Reactor模式與Observer模式在某些方面極爲類似:當一個主體發生改變時,全部依屬體都獲得通知。不過,觀察者模式與單個事件源關聯,而反應器模式則與多個事件源關聯 。nginx

在Reactor模式中,有5個關鍵的參與者:程序員

  • 描述符(handle):由操做系統提供的資源,用於識別每個事件,如Socket描述符、文件描述符、信號的值等。在Linux中,它用一個整數來表示。事件能夠來自外部,如來自客戶端的鏈接請求、數據等。事件也能夠來自內部,如信號、定時器事件。
  • 同步事件多路分離器(event demultiplexer):事件的到來是隨機的、異步的,沒法預知程序什麼時候收到一個客戶鏈接請求或收到一個信號。因此程序要循環等待並處理事件,這就是事件循環。在事件循環中,等待事件通常使用I/O複用技術實現。在linux系統上通常是select、poll、epol_waitl等系統調用,用來等待一個或多個事件的發生。I/O框架庫通常將各類I/O複用系統調用封裝成統一的接口,稱爲事件多路分離器。調用者會被阻塞,直到分離器分離的描述符集上有事件發生。
  • 事件處理器(event handler):I/O框架庫提供的事件處理器一般是由一個或多個模板函數組成的接口。這些模板函數描述了和應用程序相關的對某個事件的操做,用戶須要繼承它來實現本身的事件處理器,即具體事件處理器。所以,事件處理器中的回調函數通常聲明爲虛函數,以支持用戶拓展。
  • 具體的事件處理器(concrete event handler):是事件處理器接口的實現。它實現了應用程序提供的某個服務。每一個具體的事件處理器總和一個描述符相關。它使用描述符來識別事件、識別應用程序提供的服務。
  • Reactor 管理器(reactor):定義了一些接口,用於應用程序控制事件調度,以及應用程序註冊、刪除事件處理器和相關的描述符。它是事件處理器的調度核心。 Reactor管理器使用同步事件分離器來等待事件的發生。一旦事件發生,Reactor管理器先是分離每一個事件,而後調度事件處理器,最後調用相關的模 板函數來處理這個事件。

兩種高效的服務器設計模型:Reactor和Proactor模型

能夠看出,是Reactor管理器並非應用程序負責等待事件、分離事件和調度事件。Reactor並無被具體的事件處理器調度,而是管理器調度具體的事件處理器,由事件處理器對發生的事件做出處理,這就是Hollywood原則。應用程序要作的僅僅是實現一個具體的事件處理器,而後把它註冊到Reactor管理器中。接下來的工做由管理器來完成:若是有相應的事件發生,Reactor會主動調用具體的事件處理器,由事件處理器對發生的事件做出處理。golang

應用場景web

場景: 長途客車在路途上,有人上車有人下車,可是乘客老是但願可以在客車上獲得休息。redis

傳統作法: 每隔一段時間(或每個站),司機或售票員對每個乘客詢問是否下車。編程

Reactor作法:汽車是乘客訪問的主體(Reactor),乘客上車後,到售票員(acceptor)處登記,以後乘客即可以休息睡覺去了,當到達乘客所要到達的目的地時(指定的事件發生,乘客到了下車地點),售票員將其喚醒便可。windows

爲何使用Reactor後端

網絡編程爲何要用反應堆?有了I/O複用,有了epoll已經可使服務器併發幾十萬鏈接的同時,維持高TPS了,難道這還不夠嗎?

答案是,技術層面足夠了,但在軟件工程層面倒是不夠的。

程序使用IO複用的難點在哪裏呢?

1個請求可能由屢次IO處理完成,但相比傳統的單線程完整處理請求生命期的方法,IO複用在人的大腦思惟中並不天然,由於,程序員編程中,處理請求A的時候,假定A請求必須通過多個IO操做A1-An(兩次IO間可能間隔很長時間),每通過一次IO操做,再調用IO複用時,IO複用的調用返回裏,很是可能再也不有A,而是返回了請求B。即請求A會常常被請求B打斷,處理請求B時,又被C打斷。這種思惟下,編程容易出錯。

形象例子:

傳統編程方法就好像是到了銀行營業廳裏,每一個窗口前排了長隊,業務員們在窗口後一個個的解決客戶們的請求。一個業務員能夠盡情思考着客戶A依次提出的問題,例如:

「我要買2萬XX理財產品。「

「看清楚了,5萬起售。」

「等等,查下我活期餘額。」

「餘額5萬。」

「那就買 5萬吧。」

業務員開始錄入信息。

」對了,XX理財產品年利率8%?」

「是預期8%,最低無利息保本。「

」早不說,拜拜,我去買餘額寶。「

業務員無表情的刪着已經錄入的信息進行事務回滾。

」下一個!「

用了IO複用則是大師業務員開始挑戰極限,在超大營業廳裏給客戶們人手一個牌子,黑壓壓的客戶們都在大廳中,有問題時舉牌申請提問,大師目光敏銳點名指定某人提問,該客戶迅速獲得大師的答覆後,要通過一段時間思考,查查本身的銀袋子,諮詢下LD,才能再次進行下一個提問,直到獲得完整的滿意答覆退出大廳。例如:大師剛指導A填寫轉賬單的某一項,B又來申請兌換泰銖,給了B兌換單後,C又來辦理定轉活,而後D與F在爭搶有限的圓珠筆時出現了不和諧現象,被大師叫停業務,暫時等待。

這就是基於事件驅動的IO複用編程比起傳統1線程1請求的方式來,有難度的設計點了,客戶們都是上帝,既不能出錯,還不能厚此薄彼。

當沒有反應堆時,咱們可能的設計方法是這樣的:大師把每一個客戶的提問都記錄下來,當客戶A提問時,首先查閱A以前問過什麼作過什麼,這叫聯繫上下文,而後再根據上下文和當前提問查閱有關的銀行規章制度,有針對性的回答A,並把回答也記錄下來。當圓滿回答了A的全部問題後,刪除A的全部記錄。

在程序中:

某一瞬間,服務器共有10萬個併發鏈接,此時,一次IO複用接口的調用返回了100個活躍的鏈接等待處理。先根據這100個鏈接找出其對應的對象,這並不難,epoll的返回鏈接數據結構裏就有這樣的指針能夠用。接着,循環的處理每個鏈接,找出這個對象此刻的上下文狀態,再使用read、write這樣的網絡IO獲取這次的操做內容,結合上下文狀態查詢此時應當選擇哪一個業務方法處理,調用相應方法完成操做後,若請求結束,則刪除對象及其上下文。

這樣,咱們就陷入了面向過程編程方法之中了,在面向應用、快速響應爲王的移動互聯網時代,這樣作遲早得把本身玩死。咱們的主程序須要關注各類不一樣類型的請求,在不一樣狀態下,對於不一樣的請求命令選擇不一樣的業務處理方法。這會致使隨着請求類型的增長,請求狀態的增長,請求命令的增長,主程序複雜度快速膨脹,致使維護愈來愈困難,苦逼的程序員不再敢輕易接新需求、重構。

反應堆是解決上述軟件工程問題的一種途徑,它也許並不優雅,開發效率上也不是最高的,但其執行效率與面向過程的使用IO複用卻幾乎是等價的,因此,不管是nginx、memcached、redis等等這些高性能組件的代名詞,都義無反顧的一頭扎進了反應堆的懷抱中。

反應堆模式能夠在軟件工程層面,將事件驅動框架分離出具體業務,將不一樣類型請求之間用OO的思想分離。一般,反應堆不只使用IO複用處理網絡事件驅動,還會實現定時器來處理時間事件的驅動(請求的超時處理或者定時任務的處理),就像下面的示意圖:

兩種高效的服務器設計模型:Reactor和Proactor模型

這幅圖有5點意思:

(1)處理應用時基於OO思想,不一樣的類型的請求處理間是分離的。例如,A類型請求是用戶註冊請求,B類型請求是查詢用戶頭像,那麼當咱們把用戶頭像新增多種分辨率圖片時,更改B類型請求的代碼處理邏輯時,徹底不涉及A類型請求代碼的修改。

(2)應用處理請求的邏輯,與事件分發框架徹底分離。什麼意思呢?即寫應用處理時,不用去管什麼時候調用IO複用,不用去管什麼調用epoll_wait,去處理它返回的多個socket鏈接。應用代碼中,只關心如何讀取、發送socket上的數據,如何處理業務邏輯。事件分發框架有一個抽象的事件接口,全部的應用必須實現抽象的事件接口,經過這種抽象才把應用與框架進行分離。

(3)反應堆上提供註冊、移除事件方法,供應用代碼使用,而分發事件方法,一般是循環的調用而已,是否提供給應用代碼調用,仍是由框架簡單粗暴的直接循環使用,這是框架的自由。

(4)IO多路複用也是一個抽象,它能夠是具體的select,也能夠是epoll,它們只必須提供採集到某一瞬間全部待監控鏈接中活躍的鏈接。

(5)定時器也是由反應堆對象使用,它必須至少提供4個方法,包括添加、刪除定時器事件,這該由應用代碼調用。最近超時時間是須要的,這會被反應堆對象使用,用於確認select或者epoll_wait執行時的阻塞超時時間,防止IO的等待影響了定時事件的處理。遍歷也是由反應堆框架使用,用於處理定時事件。

Reactor的幾種模式

參考資料:Scalable IO in Java

在web服務中,不少都涉及基本的操做:read request、decode request、process service、encod reply、send reply等。

一、單線程模式

這是最簡單的單Reactor單線程模型。Reactor線程是個多面手,負責多路分離套接字,Accept新鏈接,並分派請求處處理器鏈中。該模型適用於處理器鏈中業務處理組件能快速完成的場景。不過這種單線程模型不能充分利用多核資源,因此實際使用的很少。

兩種高效的服務器設計模型:Reactor和Proactor模型

二、多線程模式(單Reactor)

該模型在事件處理器(Handler)鏈部分採用了多線程(線程池),也是後端程序經常使用的模型。

兩種高效的服務器設計模型:Reactor和Proactor模型

三、多線程模式(多個Reactor)

比起第二種模型,它是將Reactor分紅兩部分,mainReactor負責監聽並accept新鏈接,而後將創建的socket經過多路複用器(Acceptor)分派給subReactor。subReactor負責多路分離已鏈接的socket,讀寫網絡數據;業務處理功能,其交給worker線程池完成。一般,subReactor個數上可與CPU個數等同。

兩種高效的服務器設計模型:Reactor和Proactor模型

Proacotr模型

Proactor是和異步I/O相關的。

在Reactor模式中,事件分離者等待某個事件或者可應用或個操做的狀態發生(好比文件描述符可讀寫,或者是socket可讀寫),事件分離器就把這個事件傳給事先註冊的處理器(事件處理函數或者回調函數),由後者來作實際的讀寫操做。

在Proactor模式中,事件處理者(或者代由事件分離者發起)直接發起一個異步讀寫操做(至關於請求),而實際的工做是由操做系統來完成的。發起時,須要提供的參數包括用於存放讀到數據的緩存區,讀的數據大小,或者用於存放外發數據的緩存區,以及這個請求完後的回調函數等信息。事件分離者得知了這個請求,它默默等待這個請求的完成,而後轉發完成事件給相應的事件處理者或者回調。

能夠看出二者的區別:Reactor是在事件發生時就通知事先註冊的事件(讀寫由處理函數完成);Proactor是在事件發生時進行異步I/O(讀寫由OS完成),待IO完成事件分離器才調度處理器來處理。

舉個例子,將有助於理解Reactor與Proactor兩者的差別,以讀操做爲例(類操做相似)。

在Reactor(同步)中實現讀:

- 註冊讀就緒事件和相應的事件處理器

- 事件分離器等待事件

- 事件到來,激活分離器,分離器調用事件對應的處理器。

- 事件處理器完成實際的讀操做,處理讀到的數據,註冊新的事件,而後返還控制權。

Proactor(異步)中的讀:

- 處理器發起異步讀操做(注意:操做系統必須支持異步IO)。在這種狀況下,處理器無視IO就緒事件,它關注的是完成事件。

- 事件分離器等待操做完成事件

- 在分離器等待過程當中,操做系統利用並行的內核線程執行實際的讀操做,並將結果數據存入用戶自定義緩衝區,最後通知事件分離器讀操做完成。

- 事件分離器呼喚處理器。

- 事件處理器處理用戶自定義緩衝區中的數據,而後啓動一個新的異步操做,並將控制權返回事件分離器。

現行作法

開源C++框架:ACE

開源C++開發框架 ACE 提供了大量平臺獨立的底層併發支持類(線程、互斥量等)。 同時在更高一層它也提供了獨立的幾組C++類,用於實現Reactor及Proactor模式。 儘管它們都是平臺獨立的單元,但他們都提供了不一樣的接口。ACE Proactor在MS-Windows上不管是性能還在健壯性都更勝一籌,這主要是因爲Windows提供了一系列高效的底層異步API。(這段可能過期了點吧) 不幸的是,並非全部操做系統都爲底層異步提供健壯的支持。舉例來講, 許多Unix系統就有麻煩。所以, ACE Reactor多是Unix系統上更合適的解決方案。 正由於系統底層的支持力度不一,爲了在各系統上有更好的性能,開發者不得不維護獨立的好幾份代碼: 爲Windows準備的ACE Proactor以及爲Unix系列提供的ACE Reactor。真正的異步模式須要操做系統級別的支持。因爲事件處理者及操做系統交互的差別,爲Reactor和Proactor設計一種通用統一的外部接口是很是困難的。這也是設計通行開發框架的難點所在。

ACE是一個大型的中間件產品,代碼20萬行左右,過於宏大,一堆的設計模式,架構了一層又一層,使用的時候,要根據狀況,看從那一層來進行使用。支持跨平臺。

設計模式 :ACE主要應用了Reactor,Proactor等;

層次架構 :ACE底層是C風格的OS適配層,上一層基於C++的wrap類,再上一層是一些框架 (Accpetor,Connector,Reactor,Proactor等),最上一層是框架上服務;

可移植性 :ACE支持多種平臺,可移植性不存在問題,聽說socket編程在linux下有很多bugs;

事件分派處理 :ACE主要是註冊handler類,當事件分派時,調用其handler的虛掛勾函數。實現 ACE_Handler/ACE_Svc_Handler/ACE_Event_handler等類的虛函數;

涉及範圍 :ACE包含了日誌,IPC,線程池,共享內存,配置服務,遞歸鎖,定時器等;

線程調度 :ACE的Reactor是單線程調度,Proactor支持多線程調度;

發佈方式 :ACE是開源免費的,不依賴於第三方庫,通常應用使用它時,以動態連接的方式發佈動態庫;開發難度 :基於ACE開發應用,對程序員要求比較高,要用好它,必須很是瞭解其框架。在其框架下開發,每每new出一個對象,不知在什麼地方釋放好。

C網絡庫:libevent

libevent是一個C語言寫的網絡庫,官方主要支持的是類linux操做系統,最新的版本添加了對windows的IOCP的支持。在跨平臺方面主要經過select模型來進行支持。

設計模式 :libevent爲Reactor模式;

層次架構:livevent在不一樣的操做系統下,作了多路複用模型的抽象,能夠選擇使用不一樣的模型,經過事件函數提供服務;

可移植性 :libevent主要支持linux平臺,freebsd平臺,其餘平臺下經過select模型進行支持,效率不是過高;

事件分派處理 :libevent基於註冊的事件回調函數來實現事件分發;

涉及範圍 :libevent只提供了簡單的網絡API的封裝,線程池,內存池,遞歸鎖等均須要本身實現;

線程調度 :libevent的線程調度須要本身來註冊不一樣的事件句柄;

發佈方式 :libevent爲開源免費的,通常編譯爲靜態庫進行使用;

開發難度 :基於libevent開發應用,相對容易,具體能夠參考memcached這個開源的應用,裏面使用了 libevent這個庫。

改進方案:模擬異步

在改進方案中,咱們將Reactor原來位於事件處理器內的read/write操做移至分離器(不妨將這個思路稱爲「模擬異步」),以此尋求將Reactor多路同步IO轉化爲模擬異步IO。

以讀操做爲例子,改進過程以下:

- 註冊讀就緒事件及其處理器,併爲分離器提供數據緩衝區地址,須要讀取數據量等信息。

- 分離器等待事件(如在select()上等待)

- 事件到來,激活分離器。分離器執行一個非阻塞讀操做(它有完成這個操做所需的所有信息),最後調用對應處理器。

- 事件處理器處理用戶自定義緩衝區的數據,註冊新的事件(固然一樣要給出數據緩衝區地址,須要讀取的數據量等信息),最後將控制權返還分離器。

如咱們所見,經過對多路IO模式功能結構的改造,可將Reactor轉化爲Proactor模式。改造先後,模型實際完成的工做量沒有增長,只不過參與者間對工做職責稍加調換。沒有工做量的改變,天然不會形成性能的削弱。對以下各步驟的比較,能夠證實工做量的恆定:

標準/典型的Reactor:

- 步驟1:等待事件到來(Reactor負責)

- 步驟2:將讀就緒事件分發給用戶定義的處理器(Reactor負責)

- 步驟3:讀數據(用戶處理器負責)

- 步驟4:處理數據(用戶處理器負責)

改進實現的模擬Proactor:

- 步驟1:等待事件到來(Proactor負責)

- 步驟2:獲得讀就緒事件,執行讀數據(如今由Proactor負責)

- 步驟3:將讀完成事件分發給用戶處理器(Proactor負責)

- 步驟4:處理數據(用戶處理器負責)

對於不提供異步IO API的操做系統來講,這種辦法能夠隱藏socket API的交互細節,從而對外暴露一個完整的異步接口。藉此,咱們就能夠進一步構建徹底可移植的,平臺無關的,有通用對外接口的解決方案。上述方案已經由某公司實現爲TProactor。它有兩個版本:C++和JAVA的。C++版本採用ACE跨平臺底層類開發,爲全部平臺提供了通用統一的主動式異步接口。

Boost.Asio類庫

Boost.Asio類庫,其就是以Proactor這種設計模式來實現

須要C/C++ Linux服務器開發學習資料加qun563998835(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享

相關文章
相關標籤/搜索