反應堆開發模型被絕大多數高性能服務器所選擇,上一篇所介紹的IO多路複用是它的實現基礎。定時觸發功能一般是服務器必備組件,反應堆模型每每還不得不將定時器的管理囊括在內。本篇將介紹反應堆模型的特色和用法。node
首先咱們要談談,網絡編程界爲何須要反應堆?有了IO複用,有了epoll,咱們已經可使服務器併發幾十萬鏈接的同時,維持高TPS了,難道這還不夠嗎?nginx
個人答案是,技術層面足夠了,但在軟件工程層面倒是不夠的。程序員
程序使用IO複用的難點在哪裏呢?1個請求雖然由屢次IO處理完成,但相比傳統的單線程完整處理請求生命期的方法,IO複用在人的大腦思惟中並不天然,由於,程序員編程中,處理請求A的時候,假定A請求必須通過多個IO操做A1-An(兩次IO間可能間隔很長時間),每通過一次IO操做,再調用IO複用時,IO複用的調用返回裏,很是可能再也不有A,而是返回了請求B。即請求A會常常被請求B打斷,處理請求B時,又被C打斷。這種思惟下,編程容易出錯。redis
形象的說,傳統編程方法就好像是到了銀行營業廳裏,每一個窗口前排了長隊,業務員們在窗口後一個個的解決客戶們的請求。一個業務員能夠盡情思考着客戶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複用處理網絡事件驅動,還會實現定時器來處理時間事件的驅動(請求的超時處理或者定時任務的處理),就像下面的示意圖:
這幅圖有5點意思:
(1)處理應用時基於OO思想,不一樣的類型的請求處理間是分離的。例如,A類型請求是用戶註冊請求,B類型請求是查詢用戶頭像,那麼當咱們把用戶頭像新增多種分辨率圖片時,更改B類型請求的代碼處理邏輯時,徹底不涉及A類型請求代碼的修改。
(2)應用處理請求的邏輯,與事件分發框架徹底分離。什麼意思呢?即寫應用處理時,不用去管什麼時候調用IO複用,不用去管什麼調用epoll_wait,去處理它返回的多個socket鏈接。應用代碼中,只關心如何讀取、發送socket上的數據,如何處理業務邏輯。事件分發框架有一個抽象的事件接口,全部的應用必須實現抽象的事件接口,經過這種抽象才把應用與框架進行分離。
(3)反應堆上提供註冊、移除事件方法,供應用代碼使用,而分發事件方法,一般是循環的調用而已,是否提供給應用代碼調用,仍是由框架簡單粗暴的直接循環使用,這是框架的自由。
(4)IO多路複用也是一個抽象,它能夠是具體的select,也能夠是epoll,它們只必須提供採集到某一瞬間全部待監控鏈接中活躍的鏈接。
(5)定時器也是由反應堆對象使用,它必須至少提供4個方法,包括添加、刪除定時器事件,這該由應用代碼調用。最近超時時間是須要的,這會被反應堆對象使用,用於確認select或者epoll_wait執行時的阻塞超時時間,防止IO的等待影響了定時事件的處理。遍歷也是由反應堆框架使用,用於處理定時事件。
下面用極簡流程來形象說明下反應堆是如何處理一個請求的,下圖中桔色部分皆爲反應堆的分發事件流程:
能夠看到,分發IO、定時器事件都由反應堆框架來完成,應用代碼只會關注於如何處理可讀、可寫事件。
固然,上圖是極度簡化的流程,實際上要處理的異常狀況都沒有列入。
這裏能夠看到,爲何定時器集合須要提供最近超時事件距離如今的時間?由於,調用epoll_wait或者select時,並不可以始終傳入-1做爲timeout參數。由於,咱們的服務器主營業務每每是網絡請求處理,若是網絡請求不多時,那麼CPU的全部時間都會被頻繁卻又沒必要要的epoll_wait調用所佔用。在服務器閒時使進程的CPU利用率下降是頗有意義的,它可使服務器上其餘進程獲得更多的執行機會,也能夠延長服務器的壽命,還能夠省電。這樣,就須要傳入準確的timeout最大阻塞時間給epoll_wait了。
什麼樣的timeout時間纔是準確的呢?這等價於,咱們須要準確的分析,什麼樣的時段進程能夠真正休息,進入sleep狀態?
一個沒有意義的答案是:不須要進程執行任務的時間段內是能夠休息的。
這就要求咱們仔細想一想,進程作了哪幾類任務,例如:
一、全部網絡包的處理,例如TCP鏈接的創建、讀寫、關閉,基本上全部的正常請求都由網絡包來驅動的。對這類任務而言,沒有新的網絡分組到達本機時,就是可使進程休息的時段。
二、定時器的管理,它與網絡、IO複用無關,雖然它們在業務上可能有相關性。定時器裏的事件須要及時的觸發執行,不能由於其餘緣由,例如阻塞在epoll_wait上時耽誤了定時事件的處理。當一段時間內,能夠預判沒有定時事件達到觸發條件時(這也是提供接口查詢最近一個定時事件距當下的時間的意義所在),對定時任務的管理而言,進程就能夠休息了。
三、其餘類型的任務,例如磁盤IO執行完成,或者收到其餘進程的signal信號,等等,這些任務明顯不須要執行的時間段內,進程能夠休息。
因而,使用反應堆模型的進程代碼中,一般除了epoll_wait這樣的IO複用外,其餘調用都會基於無阻塞的方式使用。因此,epoll_wait的timeout超時時間,就是除網絡外,其餘任務所能容許的進程睡眠時間。而只考慮常見的定時器任務時,就像上圖中那樣,只須要定時器集合可以提供最近超時事件到如今的時間便可。
從這裏也能夠推導出,定時器集合一般會採用有序容器這樣的數據結構,好處是:
一、容易取到最近超時事件的時間。
二、能夠從最近超時事件開始,向後依次遍歷已經超時的事件,直到第一個沒有超時的事件爲止便可中止遍歷,不用所有遍歷到。
所以,粗暴的採用無序的數據結構,例如普通的鏈表,一般是不足取的。但事無絕對,redis就是用了個毫無順序的鏈表,緣由何在?由於redis的客戶端鏈接沒有超時概念,因此對於併發的成千上萬個連上,都不會由於超時被斷開。redis的定時器惟一的用途在於定時的將內存數據刷到磁盤上,這樣的定時事件一般只有個位數,其性能可有可無。
若是定時事件很是多,綜合插入、遍歷、刪除的使用頻率,使用樹的機會最多,例如小根堆(libevent)、二叉平衡樹(nginx紅黑樹)。固然,場景特殊時,盡能夠用有序數組、跳躍表等等實現。
綜上所述,反應堆模型開發效率上比起直接使用IO複用要高,它一般是單線程的,設計目標是但願單線程使用一顆CPU的所有資源,但也有附帶優勢,即每一個事件處理中不少時候能夠不考慮共享資源的互斥訪問。但是缺點也是明顯的,如今的硬件發展,已經再也不遵循摩爾定律,CPU的頻率受制於材料的限制再也不有大的提高,而改成是從核數的增長上提高能力,當程序須要使用多核資源時,反應堆模型就會悲劇,爲什麼呢?
若是程序業務很簡單,例如只是簡單的訪問一些提供了併發訪問的服務,就能夠直接開啓多個反應堆,每一個反應堆對應一顆CPU核心,這些反應堆上跑的請求互不相關,這是徹底能夠利用多核的。例如Nginx這樣的http靜態服務器。
若是程序比較複雜,例如一塊內存數據的處理但願由多核共同完成,這樣反應堆模型就很難作到了,須要昂貴的代價,引入許多複雜的機制。因此,你們就能夠理解像redis、nodejs這樣的服務,爲何只能是單線程,爲何memcached簡單些的服務確能夠是多線程。