前言:我是一名golang后端開發(fā)工程師,不是Java,也不是拍黃片,對,就是那個(gè)號稱原生支持高并發(fā)的“夠浪!”。那為什么go能支持高并發(fā)?原生支持高并發(fā)又是何解?跟著我,一起探討一下所謂的高并發(fā)是怎么回事...
閱讀本文你將收獲:
- 知道框架高性能的根本原因
- 了解進(jìn)程,線程切換開銷在哪里
- 熟悉阻塞與非阻塞IO,同步與異步調(diào)用的區(qū)別
大綱:
- 討論一個(gè)高性能框架甚至語言的時(shí)候,我們在討論什么?
- 三大網(wǎng)絡(luò)模型
- 阻塞IO+多進(jìn)程
- 阻塞IO+多線程
- 非阻塞IO+IO多路復(fù)用
- 五種網(wǎng)絡(luò)IO簡介
- 網(wǎng)絡(luò)IO的本質(zhì)
- 如何區(qū)分阻塞IO和非阻塞IO
- 如何區(qū)分同步和異步
- 個(gè)人整理的網(wǎng)絡(luò)IO思維導(dǎo)圖
1.討論一個(gè)高性能框架甚至語言的時(shí)候,我們在討論什么
我相信大家肯定聽過什么阻塞/非阻塞IO,同步/異步調(diào)用,我也嘗試過死記概念,結(jié)果大家應(yīng)該都有體會,過一陣子就忘記了。知其然而不知其所以然~然并卵。
大家在選擇一門語言或者一個(gè)框架的時(shí)候肯定優(yōu)先看他的性能,也就是并發(fā)量,例如常用的測試手段,就是用該語言或者框架寫個(gè)http server服務(wù)器,對于http請求返回一個(gè)“hello,world!”,利用wrk進(jìn)行壓測,看看每分鐘請求量最高能到多少,在4核8G的Ubuntu服務(wù)器上跑該http服務(wù),利用wrk壓測,gin框架每分鐘能處理的請求量接近300W!這是相當(dāng)優(yōu)秀的!
前一陣子在go meet up深圳討論語言性能的時(shí)候,有位老哥說同等業(yè)務(wù)與機(jī)器,PHP每秒請求量大概在300多,處理三萬并發(fā)量的服務(wù)程序,go需要一臺服務(wù)器,而PHP需要一百臺。我當(dāng)時(shí)非常震驚,為什么語言之間的差別這么大,是什么原因造成這個(gè)巨大的差別呢?我問Boss Lee(meet up講師,一位技術(shù)大佬),他跟我說因?yàn)镻HP是一個(gè)請求開一個(gè)進(jìn)程處理,注意是進(jìn)程而不是線程!
那為什么用進(jìn)程處理請求會造成性能差別這么大,甚至到了一百臺服務(wù)器的差別呢?(一百臺服務(wù)器一年得上百萬吧~)
經(jīng)過我查閱資料,得出了是網(wǎng)絡(luò)IO模型造成了性能根本上的差別這一結(jié)論!
這里直接說結(jié)論:PHP是阻塞IO+多進(jìn)程模型,大名鼎鼎的Netty(JAVA)框架是主從reactor+worker threads 模式。
為什么?因?yàn)镃PU切換進(jìn)程或線程所帶來的性能損耗是巨大的,主從reactor模式解決了IO分發(fā)的高效率問題!
這里先記住結(jié)論,后文看解析
2.三大網(wǎng)絡(luò)模型
2.1阻塞IO+多進(jìn)程
服務(wù)器初始監(jiān)聽在lisnted_fd套接字上,此時(shí)一個(gè)客戶端發(fā)起連接請求,連接成功后產(chǎn)生連接套接字,此時(shí)父進(jìn)程fork出一個(gè)子進(jìn)程,子進(jìn)程拿到連接套接字,并以此與客戶端通信。在這種網(wǎng)絡(luò)模型下,父進(jìn)程關(guān)心的是監(jiān)聽套接字,子進(jìn)程關(guān)心的是連接套接字。


這種網(wǎng)絡(luò)模型編程簡單,但是效率不高。
2.2阻塞IO+多線程
進(jìn)程切換上下文代價(jià)是相當(dāng)高的,有一種類似進(jìn)程,但是切換開銷比進(jìn)程小的東西,那就是線程。
為什么說線程切換比進(jìn)程切換開銷要小呢?
因?yàn)榫€程由操作系統(tǒng)內(nèi)核管理,在同一個(gè)進(jìn)程中,所有的線程共享該進(jìn)程的整個(gè)虛擬地址空間,包括代碼、數(shù)據(jù)、堆、共享庫等。
我們的代碼被CPU執(zhí)行需要一些數(shù)據(jù)支撐的,這就是所謂的上下文,包括但不限于程序計(jì)數(shù)器需要告訴CPU代碼執(zhí)行到哪里了,寄存器中存放了一些計(jì)算中間值,內(nèi)從中存放了當(dāng)前一些變量等。從一個(gè)計(jì)算場景切換到另一個(gè)計(jì)算場景,這些值都需要重新載入,這就是上下文切換。
2.2非阻塞IO+IO多路復(fù)用
使用poll和epoll可以設(shè)計(jì)出基于套接字滿足高性能,高并發(fā)的事件驅(qū)動(dòng)程序。
事件驅(qū)動(dòng)模型,叫做reactor模型,或者Even loop模型。是不是很熟悉?這個(gè)模型的核心有兩點(diǎn):
- 存在一個(gè)無限循環(huán)的事件分發(fā)線程,叫reactor線程,或者Even loop線程。這個(gè)分發(fā)線程背后的技術(shù)就是poll與epoll這類的IO多路復(fù)用技術(shù)。
- 所有的IO操作都可抽象為事件,每個(gè)事件必須有回調(diào)函數(shù)來處理。acceptor上有連接建立,已連接套接字的發(fā)送緩沖區(qū)可以寫,通信管道pipe上有數(shù)據(jù)可以讀,這些事件通過事件分發(fā),都能被檢測并調(diào)用回調(diào)函數(shù)處理。
-
單reactor模型 + worker threads
該模型是將acceptor上連接建立事件,和已連接套接字的IO事件的分發(fā)由一個(gè)reactor線程去執(zhí)行,由工作線程去處理耗時(shí)操作,例如數(shù)據(jù)庫讀取,文件解析,計(jì)算等等。
單reactor模型 + worker threads.png
-
主從reactor模型 + worker threads
當(dāng)所有acceptor的連接建立事件和已連接套接字的IO事件交由一個(gè)reactor線程處理,在并發(fā)量較高的情況下,這個(gè)reactor線程會忙不過來,表現(xiàn)在客戶端連接建立成功率偏低。
那么主從模式的核心思想就在于,主reactor上只監(jiān)聽acceptor上成功建立的連接事件,并將其分發(fā)給從reactor線程,從reactor線程只需要負(fù)責(zé)已連接套接字上的IO事件。

總結(jié):我們通過主reactor線程來分發(fā)成功建立的套接字,通過從reactor線程來分發(fā)已連接套接字上的IO事件,通過工作線程來處理耗時(shí)操作!更進(jìn)一步---通過用戶態(tài)自己建立的協(xié)程機(jī)制來調(diào)度業(yè)務(wù)處理程序,用戶態(tài)自己管理協(xié)程間切換,避免了CPU切換線程,又能為程序帶來更高的處理效率!
3. 五種網(wǎng)絡(luò)IO簡介
- 阻塞IO
- 非阻塞IO
- IO多路復(fù)用
- 異步IO
- 信號驅(qū)動(dòng)IO
阻塞IO:
當(dāng)應(yīng)用程序調(diào)用阻塞IO完成某個(gè)操作時(shí),應(yīng)用程序會被掛起,感覺上應(yīng)用程序像是被“阻塞”了一樣。實(shí)際上,內(nèi)核所做的事情是將CPU時(shí)間切換給了其他有需要的進(jìn)程,網(wǎng)絡(luò)應(yīng)用程序在這種情況下就會得不到CPU時(shí)間做該做的事情。
非阻塞IO:
當(dāng)應(yīng)用程序調(diào)用非阻塞IO完成某個(gè)操作時(shí),內(nèi)核立即返回,不會把CPU時(shí)間讓出給其他進(jìn)程,應(yīng)用程序在返回后可以得到足夠的CPU時(shí)間做其他的事情。
IO多路復(fù)用:
我們可以把標(biāo)準(zhǔn)輸入、套接字都看作IO的一路,多路復(fù)用的意思,就是在任何一路IO有“事件”發(fā)生的情況下,通知應(yīng)用程序去處理相應(yīng)的IO事件,這樣我們的程序就“好像”在同一時(shí)刻處理多個(gè)IO事件。
異步IO:
當(dāng)一個(gè)異步過程調(diào)用發(fā)出后,調(diào)用者不能立刻得到結(jié)果。實(shí)際處理這個(gè)調(diào)用的部件在完成后,通過狀態(tài)、通知和回調(diào)來通知調(diào)用者。
信號驅(qū)動(dòng)IO:
應(yīng)用進(jìn)程使用 sigaction 系統(tǒng)調(diào)用,內(nèi)核立即返回,應(yīng)用進(jìn)程可以繼續(xù)執(zhí)行。當(dāng)數(shù)據(jù)報(bào)準(zhǔn)備好讀取時(shí),內(nèi)核就為該進(jìn)程產(chǎn)生一個(gè)SIGIO信號,我們隨后可以在信號處理函數(shù)中讀取數(shù)據(jù)報(bào),也可以立即通知主循環(huán),讓他讀取數(shù)據(jù)。
4.網(wǎng)絡(luò)IO的本質(zhì)
網(wǎng)絡(luò)IO的本質(zhì)就是socket流的讀取,通常一次IO讀操作會涉及到兩個(gè)對象和兩個(gè)階段。
兩個(gè)對象:
- 用戶進(jìn)程(線程)
- 內(nèi)核對象
兩個(gè)階段:
- 等待數(shù)據(jù)流準(zhǔn)備
- 從內(nèi)核像進(jìn)程復(fù)制數(shù)據(jù)
對于socket流而言:
- 第一步通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá),然后被復(fù)制到內(nèi)核的某個(gè)緩沖區(qū)。
- 第二步把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到進(jìn)程緩沖區(qū)。
5. 如何區(qū)分阻塞IO和非阻塞IO
阻塞IO發(fā)起的read請求,線程會被掛起,一直等到內(nèi)核數(shù)據(jù)準(zhǔn)備好,并把數(shù)據(jù)從內(nèi)核區(qū)域拷貝到應(yīng)用程序的緩沖區(qū)中,拷貝完成后,read請求調(diào)用才返回。

非阻塞IO的read請求在數(shù)據(jù)為準(zhǔn)備的情況下立即返回,應(yīng)用程序可以不斷輪詢內(nèi)核,直到數(shù)據(jù)準(zhǔn)備好,內(nèi)核將數(shù)據(jù)拷貝到應(yīng)用程序緩沖區(qū)并完成這次read調(diào)用。

6. 如何區(qū)分同步和異步
同步調(diào)用與異步調(diào)用是對于獲取數(shù)據(jù)的過程而言的,前面的幾種最后獲取數(shù)據(jù)的read操作調(diào)用,都是同步的,即在read調(diào)用時(shí),內(nèi)核將數(shù)據(jù)從內(nèi)核空間拷貝到應(yīng)用程序空間,這個(gè)過程是在read函數(shù)中同步進(jìn)行的。

當(dāng)我們發(fā)起異步讀(aio_read)之后,就立即返回,內(nèi)核自動(dòng)將數(shù)據(jù)從內(nèi)核空間拷貝到應(yīng)用程序空間,這個(gè)拷貝過程是異步的,內(nèi)核自動(dòng)完成的,和前面的同步操作不一樣,應(yīng)用程序并不需要主動(dòng)發(fā)起拷貝動(dòng)作。

7. 個(gè)人整理的網(wǎng)絡(luò)IO思維導(dǎo)圖
閱讀網(wǎng)絡(luò)IO相關(guān)資料并整理成思維導(dǎo)圖,花了我將近一個(gè)月的空余時(shí)間,每天下班加班干完活兒就是看極客時(shí)間盛延敏老師的《網(wǎng)絡(luò)編程實(shí)戰(zhàn)》,他寫的言簡意賅,評論區(qū)大家的討論也非常之精彩,如果大家想深入了解網(wǎng)絡(luò)編程這塊,還是推薦大家直接購買盛延敏老師的專欄。

有需要的可以關(guān)注我的微信公眾號《從菜鳥到大佬》,聯(lián)系我,我分享給你xmind的思維導(dǎo)圖原版,大家一起學(xué)習(xí),一起進(jìn)步。
