1. 程式人生 > >高效能網路程式設計6--reactor反應堆與定時器管理

高效能網路程式設計6--reactor反應堆與定時器管理

反應堆開發模型被絕大多數高效能伺服器所選擇,上一篇所介紹的IO多路複用是它的實現基礎。定時觸發功能通常是伺服器必備元件,反應堆模型往往還不得不將定時器的管理囊括在內。本篇將介紹反應堆模型的特點和用法。首先我們要談談,網路程式設計界為什麼需要反應堆?有了IO複用,有了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複用處理網路事件驅動,還會實現定時器來處理時間事件的驅動(請求的超時處理或者定時任務的處理),就像下面的示意圖:
這幅圖有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狀態?一個沒有意義的答案是:不需要程序執行任務的時間段內是可以休息的。這就要求我們仔細想想,程序做了哪幾類任務,例如:1、所有網路包的處理,例如TCP連線的建立、讀寫、關閉,基本上所有的正常請求都由網路包來驅動的。對這類任務而言,沒有新的網路分組到達本機時,就是可以使程序休息的時段。2、定時器的管理,它與網路、IO複用無關,雖然它們在業務上可能有相關性。定時器裡的事件需要及時的觸發執行,不能因為其他原因,例如阻塞在epoll_wait上時耽誤了定時事件的處理。當一段時間內,可以預判沒有定時事件達到觸發條件時(這也是提供介面查詢最近一個定時事件距當下的時間的意義所在),對定時任務的管理而言,程序就可以休息了。3、其他型別的任務,例如磁碟IO執行完成,或者收到其他程序的signal訊號,等等,這些任務明顯不需要執行的時間段內,程序可以休息。於是,使用反應堆模型的程序程式碼中,通常除了epoll_wait這樣的IO複用外,其他呼叫都會基於無阻塞的方式使用。所以,epoll_wait的timeout超時時間,就是除網路外,其他任務所能允許的程序睡眠時間。而只考慮常見的定時器任務時,就像上圖中那樣,只需要定時器集合能夠提供最近超時事件到現在的時間即可。從這裡也可以推匯出,定時器集合通常會採用有序容器這樣的資料結構,好處是:1、容易取到最近超時事件的時間。2、可以從最近超時事件開始,向後依次遍歷已經超時的事件,直到第一個沒有超時的事件為止即可停止遍歷,不用全部遍歷到。因此,粗暴的採用無序的資料結構,例如普通的連結串列,通常是不足取的。但事無絕對,redis就是用了個毫無順序的連結串列,原因何在?因為redis的客戶端連線沒有超時概念,所以對於併發的成千上萬個連上,都不會因為超時被斷開。redis的定時器唯一的用途在於定時的將記憶體資料刷到磁碟上,這樣的定時事件通常只有個位數,其效能無關緊要。如果定時事件非常多,綜合插入、遍歷、刪除的使用頻率,使用樹的機會最多,例如小根堆(libevent)、二叉平衡樹(nginx紅黑樹)。當然,場景特殊時,儘可以用有序陣列、跳躍表等等實現。綜上所述,反應堆模型開發效率上比起直接使用IO複用要高,它通常是單執行緒的,設計目標是希望單執行緒使用一顆CPU的全部資源,但也有附帶優點,即每個事件處理中很多時候可以不考慮共享資源的互斥訪問。可是缺點也是明顯的,現在的硬體發展,已經不再遵循摩爾定律,CPU的頻率受制於材料的限制不再有大的提升,而改為是從核數的增加上提升能力,當程式需要使用多核資源時,反應堆模型就會悲劇,為何呢?如果程式業務很簡單,例如只是簡單的訪問一些提供了併發訪問的服務,就可以直接開啟多個反應堆,每個反應堆對應一顆CPU核心,這些反應堆上跑的請求互不相關,這是完全可以利用多核的。例如Nginx這樣的http靜態伺服器。如果程式比較複雜,例如一塊記憶體資料的處理希望由多核共同完成,這樣反應堆模型就很難做到了,需要昂貴的代價,引入許多複雜的機制。所以,大家就可以理解像redis、nodejs這樣的服務,為什麼只能是單執行緒,為什麼memcached簡單些的服務確可以是多執行緒。如果大家喜歡這個系列的文章,麻煩幫我的部落格在2013部落格之星上投個票哈,謝謝!