程序員應(yīng)如何理解Reactor模式?
?大家好,我是小風(fēng)哥!今天我們聊聊reactor模式。
在設(shè)計(jì)高并發(fā)高性能服務(wù)器時(shí),一項(xiàng)關(guān)鍵的考慮就是I/O。
I/O是一個(gè)問題
有的同學(xué)可能會(huì)有疑問,為什么I/O會(huì)成為問題?
假設(shè)有一個(gè)web server,每分鐘有數(shù)百萬次的請(qǐng)求過來,服務(wù)器在處理請(qǐng)求時(shí)要訪問數(shù)據(jù)庫,同時(shí)該服務(wù)器也可能要請(qǐng)求其它的服務(wù),一張典型的后端server可能的架構(gòu)如圖所示:
一個(gè)用戶請(qǐng)求過來后Server可能需要訪問數(shù)據(jù)庫,然后再去請(qǐng)求另外幾個(gè)server后才能得到用戶請(qǐng)求的處理結(jié)果,然后將response返回給客戶端,從這張圖中數(shù)一數(shù)涉及到哪些IO?
其實(shí)主要有兩種:
- 數(shù)據(jù)庫操作的磁盤IO、文件IO
- 網(wǎng)絡(luò)IO
現(xiàn)在你已經(jīng)知道了涉及哪些IO,讓我們?cè)賮砜匆粡垐D:
我們可以看到,磁盤IO和網(wǎng)絡(luò)IO是非常慢的,也就是說我們通常用手機(jī)APP、PC瀏覽器打開一個(gè)頁面點(diǎn)擊一個(gè)按鈕到完全得到響應(yīng),這其中大部分的時(shí)間都消耗在了這兩種IO上,真正用在處理數(shù)據(jù)的CPU時(shí)間反而不是很多。
這告訴了我們一個(gè)道理,那就是高效處理IO對(duì)于高并發(fā)高性能服務(wù)器來說至關(guān)重要。
兩類經(jīng)典設(shè)計(jì)模式
有兩類處理網(wǎng)絡(luò)請(qǐng)求的經(jīng)典模型:
- 基于線程(進(jìn)程)的 Thread(Process)-per-connection,也就是每個(gè)請(qǐng)求一個(gè)線程(進(jìn)程)
- 基于事件驅(qū)動(dòng)的Reactor模式,也就是反應(yīng)器模式。
其中第一種模式我們?cè)谥暗奈恼轮幸呀?jīng)多次講解過了,這種模式會(huì)為每個(gè)請(qǐng)求都創(chuàng)建一個(gè)線程或者進(jìn)程:
但這種模式的一個(gè)問題就在于當(dāng)并發(fā)數(shù)較多時(shí)需要?jiǎng)?chuàng)建很多的線程,創(chuàng)建線程過多會(huì)有性能問題。
第二種基于事件的模式我們?cè)谥暗奈恼轮幸仓v解過,在這種模式下我們只需要一個(gè)線程就能同時(shí)處理多個(gè)用戶請(qǐng)求:
在基于事件的并發(fā)編程中有一種叫做Reactor的模式非常流行,Node.js以及Nginx就使用Reactor,在這篇文章中我們?cè)敿?xì)的講解一下高性能高并發(fā)服務(wù)器中的Reactor模式。
當(dāng)然,在了解Reactor之前我們先來看一下咖啡館是怎樣運(yùn)作的。
咖啡館是怎樣運(yùn)作的
假設(shè)你有一家咖啡館,作為老板的你在前臺(tái)接待喝咖啡的顧客,你的生意不錯(cuò),來這里喝咖啡的人絡(luò)繹不絕。
有時(shí),有的人點(diǎn)的東西很簡單,比如來一杯咖啡或者牛奶之類,但也有一些顧客會(huì)點(diǎn)一些復(fù)雜的比如來一份意大利面等,作為前臺(tái)的你,如果這是停止接待顧客而且制作意大利面的話那么后續(xù)到來的所有顧客都要等待。
幸好,作為老板的你還有幾位大廚來幫忙,因此你只需要簡單的把制作意大利面的命令交代下去就好了,“張三去煮面條,李四去制作醬料,制作好后通知我”。
就這樣,即便前臺(tái)只有你一個(gè)人也能快速接待顧客的點(diǎn)餐,其實(shí)這背后本質(zhì)上就是Reactor模式。
Reactor模式
實(shí)際上每個(gè)你可以把咖啡館這個(gè)例子中每個(gè)顧客理解為服務(wù)器接收的請(qǐng)求,前臺(tái)的服務(wù)員理解為一個(gè)單線程的while循環(huán),這個(gè)while循環(huán)有一個(gè)很形象的名字,event loop,這個(gè)event loop要做的事情非常簡單,那就是接收用戶請(qǐng)求,然后讓handler,或者回調(diào)函數(shù)去處理,這里的handler或者回調(diào)函數(shù)就好比大廚張三和李四去,handler或者回調(diào)函數(shù)可以和event loop運(yùn)行在同一個(gè)線程中,也可以和event loop各自運(yùn)行在各自的線程中。
既然該模式是基于事件驅(qū)動(dòng),那么都有哪些事件呢?
我們需要關(guān)心的典型事件這樣幾種:
- 網(wǎng)絡(luò)請(qǐng)求的到來,也就是socket編程中accept到客戶端連接
- 文件可讀
- 文件可寫
看到了吧,這幾種event都是和IO相關(guān)的,涉及網(wǎng)絡(luò)和文件。
有的同學(xué)可能會(huì)問,那么這個(gè)event loop是怎么知道有這些event到來呢?
這是涉及到了IO多路復(fù)用技術(shù),典型的像Linux中的select、poll、epoll。
通過IO多路復(fù)用技術(shù),我們可以一次監(jiān)控一堆的文件描述符,當(dāng)這些文件描述符對(duì)應(yīng)的IO事件發(fā)生時(shí)會(huì)收到操作系統(tǒng)的通知,這時(shí)我們獲取到該event并交給相應(yīng)的handler或者回調(diào)函數(shù)來處理。
總結(jié)下來,Reactor的核心組成部分就是event loop + IO多路復(fù)用 + 回調(diào)函數(shù)。
單線程 or 多線程
我們?cè)谏衔奶岬竭^,處理event的handler可以和event loop運(yùn)行在同一個(gè)線程中,也可以運(yùn)行在不同的線程中。
如果是運(yùn)行在同一個(gè)線程中那么我們無需面對(duì)復(fù)雜的多線程問題,但在當(dāng)前的多核時(shí)代,單線程無法充分利用多核資源,此外如果某個(gè)請(qǐng)求比較復(fù)雜需要占用的CPU資源較多,那么在單線程下其它所有的用戶請(qǐng)求都要等待,基于以上考慮我們可以使用線程池(多線程)技術(shù)。
event loop在接收到event后,將event和處理event的handler(回調(diào)函數(shù))打包發(fā)給線程池,線程池中的線程接收到打包后的任務(wù)后調(diào)用handler(回調(diào)函數(shù))來處理相應(yīng)的event。
這樣我們的組合就成了event loop + IO多路復(fù)用 + 回調(diào)函數(shù) + 線程池。
把協(xié)程也加進(jìn)來
回調(diào)函數(shù)的一大缺點(diǎn)在于如果處理用戶請(qǐng)求的邏輯比較復(fù)雜可能會(huì)導(dǎo)致回調(diào)地獄,關(guān)于回調(diào)地獄你可以參考這里,協(xié)程這種技術(shù)在一定程度上解決了這一問題,讓我們可以用同步的方式來進(jìn)行異步編程,關(guān)于協(xié)程你可以參考這里和這里。
最終我們的組合就成了event loop + IO多路復(fù)用 + 協(xié)程 + 線程池。
接下來讓我們以Node.js來講解一下Reactor模式。
Node.js與Reactor模式
我們來看一下Node.js的架構(gòu)圖:
這張架構(gòu)圖已經(jīng)無比清晰的展示了Reactor模式是如何運(yùn)行的。
1, 當(dāng)用戶請(qǐng)求到來后需要將其放到一個(gè)隊(duì)列當(dāng)中,因?yàn)閑vent loop是運(yùn)行在單線程中的。
2,接下來event loop不斷檢測event queue中是否有event到來,如果隊(duì)列中有請(qǐng)求,那么根據(jù)隊(duì)列的“先來先服務(wù)”原則,event loop取出相應(yīng)的event,并將其交給線程池。
3,該線程池不斷檢測是否有task到來,這里的task也就是將event和相應(yīng)的回調(diào)函數(shù)打包后形成的。
4,線程接收到task后,線程池中的線程開始工作,比如查詢數(shù)據(jù)庫、讀取文件等等。
5,當(dāng)線程處理完一個(gè)請(qǐng)求后調(diào)用task相應(yīng)的回調(diào)函數(shù),并將該處理結(jié)果response發(fā)送給event loop。
6,event loop在接收到處理結(jié)果后發(fā)送給客戶端。
怎樣,這是不是像極了上文中的咖啡館以及這里的核反應(yīng)堆。
這就是Reactor模式。
此外,Node.js中的協(xié)程叫做Fiber,都是用來以同步的方式來進(jìn)行異步編程的,這里就不詳細(xì)講解了。
Reactor vs Proactor
Reactor模式中使用的IO都是同步IO,什么是同步IO呢?
就是說調(diào)用方在IO完成之前會(huì)被阻塞等待,這種IO更具體的就叫做同步阻塞式IO。
但我們知道event loop是運(yùn)行在一個(gè)線程中的,如果在event loop中調(diào)用同步阻塞式IO的話,那么整個(gè)線程會(huì)被暫停運(yùn)行,由于event loop就像咖啡廳前臺(tái),非常關(guān)鍵,如果event loop所在線程被阻塞那么所有的用戶請(qǐng)求都必須等待。
因此,在event loop中的IO不能是阻塞式的。
有同步阻塞式IO就有同步非阻塞式IO。
什么是同步非阻塞式IO呢?意思是當(dāng)我們調(diào)用同步非阻塞式IO相關(guān)函數(shù)時(shí),函數(shù)會(huì)立刻返回,并告訴我們文件是否可讀或者可寫,如果可讀或者可寫的話我們?cè)僬嬲倪M(jìn)行文件讀寫,這就是同步非阻塞式IO。
Reactor模式都是采用的同步非阻塞式IO。
與同步IO相對(duì)應(yīng)的是異步IO。
在異步IO下我們需要將接收或者寫入數(shù)據(jù)的地址告訴操作系統(tǒng),操作系統(tǒng)會(huì)將數(shù)據(jù)從進(jìn)程地址空間寫入文件或者將文件內(nèi)容寫到進(jìn)程地址空間中,操作系統(tǒng)完成IO后會(huì)通知我們,這就是異步IO。
執(zhí)行異步IO同樣不會(huì)阻塞調(diào)用線程。
關(guān)于同步以及異步的概念你可以參考這里。
而采用異步IO的事件驅(qū)動(dòng)編程被稱為Proactor。
也就是說Reactor和Proactor的區(qū)別就在于一個(gè)采用同步IO一個(gè)采用異步IO。
接下來我們用一個(gè)讀文件的例子來講解這兩者的差異。
Reactor中的讀:
- 告訴event loop,我們對(duì)某個(gè)文件可讀事件感興趣
- event loop等待該事件
- 事件到來,event loop被喚醒,并調(diào)用相應(yīng)handler
- 該handler開始讀取文件,并處理數(shù)據(jù),完成后返回到event loop
而Proactor的讀是這樣的:
- 我們發(fā)起一個(gè)針對(duì)某個(gè)文件的異步讀取操作,告訴event loop,我們不關(guān)心這個(gè)文件是否可讀,我們只關(guān)心這個(gè)文件是否讀取完成。
- event loop開始等待該事件
- 與此同時(shí),操作系統(tǒng)開始執(zhí)行真正的文件讀取,讀取完成后通知event loop讀取完成
- event loop被喚醒,此時(shí)文件已經(jīng)讀取完畢,調(diào)用相應(yīng)的handler
- handler開始處理數(shù)據(jù),完成后返回到event loop。
現(xiàn)在你應(yīng)該明白R(shí)eactor和Proactor的差異了吧。
總結(jié)
在這篇文章中我們?cè)敿?xì)講解了高性能高并發(fā)目前流行的Reactor模式,其實(shí)其本質(zhì)和咖啡館沒什么區(qū)別,如果你善于觀察和思考的話那么你會(huì)發(fā)現(xiàn)其實(shí)很多技術(shù)問題都能在現(xiàn)實(shí)生活中找到相似的場景。
希望這篇能對(duì)大家理解Reactor模式有幫助。?