架構(gòu)第五章 單服務(wù)器高性能模式

本文來自于《極客時(shí)間》- 從0開始學(xué)架構(gòu)

本文內(nèi)容:
單服務(wù)器高性能模式:PPC與TPC
應(yīng)對(duì)高并發(fā)場(chǎng)景的單服務(wù)器高性能架構(gòu)模式:Reactor和Proactor。

高性能架構(gòu)設(shè)計(jì)主要集中在兩方面:
1.提升單服務(wù)器的性能,將單服務(wù)器的性能發(fā)揮到極致。
2.若單服務(wù)器無法支撐性能,設(shè)計(jì)服務(wù)器集群方案。
3.架構(gòu)設(shè)計(jì)決定了系統(tǒng)性能的上限(基礎(chǔ)),實(shí)現(xiàn)細(xì)節(jié)(編碼)決定了系統(tǒng)性能的下限。

單服務(wù)器高性能的關(guān)鍵之一就是服務(wù)器采取的并發(fā)模型,并發(fā)模型有如下兩個(gè)關(guān)鍵設(shè)計(jì)點(diǎn):
1.服務(wù)器如何管理連接。
2.服務(wù)器如何處理請(qǐng)求。

以上兩個(gè)設(shè)計(jì)點(diǎn)最終都和操作系統(tǒng)的I/O模型及進(jìn)程模型相關(guān)。
1.I/O模型:阻塞、非阻塞、同步、異步。
2.進(jìn)程模型:單進(jìn)程、多進(jìn)程、多線程。

單服務(wù)器高性能模式:PPC與TPC

PPC

指每次有新的連接就新建一個(gè)進(jìn)程去專門處理這個(gè)連接的請(qǐng)求,統(tǒng)的UNIX網(wǎng)絡(luò)服務(wù)器采用的模型。
基本的流程圖是:

image.png

注意,圖中有一個(gè)小細(xì)節(jié),父進(jìn)程“fork”子進(jìn)程后,直接調(diào)用了close,看起來好像是關(guān)閉了連接,其實(shí)只是將連接的文件描述符引用計(jì)數(shù)減一,真正的關(guān)閉連接是等子進(jìn)程也調(diào) 用close后,連接對(duì)應(yīng)的文件描述符引用計(jì)數(shù)變?yōu)?后,操作系統(tǒng)才會(huì)真正關(guān)閉連接,更多細(xì)節(jié)請(qǐng)參考《UNIX網(wǎng)絡(luò)編程:卷一》。

PPC模式實(shí)現(xiàn)簡(jiǎn)單,比較適合服務(wù)器的連接數(shù)沒那么多的情況.
例如數(shù)據(jù)庫服務(wù)器。對(duì)于普通的業(yè)務(wù)服務(wù)器,在互聯(lián)網(wǎng)興起之前,由于服務(wù)器的訪問量和并發(fā)量并沒有那么大,這種 模式其實(shí)運(yùn)作得也挺好.
世界上第一個(gè)web服務(wù)器CERN httpd就采用了這種模式(具體可以參考https://en.wikipedia.org/wiki/CERN_httpd)。

  • 互聯(lián)網(wǎng)興起后,服務(wù)器的并發(fā)和訪問量從幾十劇增到成千上萬,這種模式弊端就凸顯出來了。
    主要體現(xiàn)在這幾個(gè)方面:
    1).fork代價(jià)高:站在操作系統(tǒng)的角度,創(chuàng)建一個(gè)進(jìn)程的代價(jià)是很高的,需要分配很多內(nèi)核資源,需要將內(nèi)存映像從父進(jìn)程復(fù)制到子進(jìn)程。即使現(xiàn)在的操作系統(tǒng)在復(fù)制內(nèi)存映像時(shí)用到 了Copy on Write(寫時(shí)復(fù)制)技術(shù),總體來說創(chuàng)建進(jìn)程的代價(jià)還是很大的。
    2).父子進(jìn)程通信復(fù)雜:父進(jìn)程“fork”子進(jìn)程時(shí),文件描述符可以通過內(nèi)存映像復(fù)制從父進(jìn)程傳到子進(jìn)程,但“fork”完成后,父子進(jìn)程通信就比較麻煩了,需要采 用IPC(Interprocess Communication)之類的進(jìn)程通信方案。
    例如,子進(jìn)程需要在close之前告訴父進(jìn)程自己處理了多少個(gè)請(qǐng)求以支撐父進(jìn)程進(jìn)行全局的統(tǒng)計(jì),那么子進(jìn)程和 父進(jìn)程必須采用IPC方案來傳遞信息。
    3).支持的并發(fā)連接數(shù)量有限:如果每個(gè)連接存活時(shí)間比較長(zhǎng),而且新的連接又源源不斷的進(jìn)來,則進(jìn)程數(shù)量會(huì)越來越多,操作系統(tǒng)進(jìn)程調(diào)度和切換的頻率也越來越高,系統(tǒng)的壓力也 會(huì)越來越大。因此,一般情況下,PPC方案能處理的并發(fā)連接數(shù)量最大也就幾百。

  • prefork 提前創(chuàng)建進(jìn)程(pre-fork)
    PPC模式中,當(dāng)連接進(jìn)來時(shí)才fork新進(jìn)程來處理連接請(qǐng)求,由于fork進(jìn)程代價(jià)高,用戶訪問時(shí)可能感覺比較慢,prefork模式的出現(xiàn)就是為了解決這個(gè)問題。
    系統(tǒng)在啟動(dòng)的時(shí)候就預(yù)先創(chuàng)建好進(jìn)程,然后才開始接受用戶的請(qǐng)求,當(dāng)有新的連接進(jìn)來的時(shí)候,就可以省去fork進(jìn)程的操作, 讓用戶訪問更快、體驗(yàn)更好。
    prefork的基本示意圖是:

image.png
  • prefork的實(shí)現(xiàn)關(guān)鍵就是多個(gè)子進(jìn)程都accept同一個(gè)socket,當(dāng)有新的連接進(jìn)入時(shí),操作系統(tǒng)保證只有一個(gè)進(jìn)程能最后accept成功。
    但這里也存在一個(gè)小小的問題:“驚群”現(xiàn)象,就是指雖然只有一個(gè)子進(jìn)程能accept成功,但所有阻塞在accept上的子進(jìn)程都會(huì)被喚醒,這樣就導(dǎo)致了不必要的進(jìn)程調(diào)度和上下文切換了。
    幸運(yùn)的是,操作系統(tǒng)可以解決這個(gè)問題,例 如Linux 2.6版本后內(nèi)核已經(jīng)解決了accept驚群?jiǎn)栴}。

prefork模式和PPC一樣,還是存在父子進(jìn)程通信復(fù)雜、支持的并發(fā)連接數(shù)量有限的問題,因此目前實(shí)際應(yīng)用也不多。Apache服務(wù)器提供了MPM prefork模式,推薦在需要可靠性或 者與舊軟件兼容的站點(diǎn)時(shí)采用這種模式,默認(rèn)情況下最大支持256個(gè)并發(fā)連接。

TPC

TPC是Thread Per Connection的縮寫,其含義是指每次有新的連接就新建一個(gè)線程去專門處理這個(gè)連接的請(qǐng)求。與進(jìn)程相比,線程更輕量級(jí),創(chuàng)建線程的消耗比進(jìn)程要少得多;
同時(shí)多線程是共享進(jìn)程內(nèi)存空間的,線程通信相比進(jìn)程通信更簡(jiǎn)單。
因此,TPC實(shí)際上是解決或者弱化了PPC fork代價(jià)高的問題和父子進(jìn)程通信復(fù)雜的問題。

image.png

和PPC相比,主進(jìn)程不用“close”連接了。
原因是在于子線程是共享主進(jìn)程的進(jìn)程空間的,連接的文件描述符并沒有被復(fù)制,因此只需要一次close即可。
TPC雖然解決了fork代價(jià)高和進(jìn)程通信復(fù)雜的問題,但是也引入了新的問題。
具體表現(xiàn)在:
1)創(chuàng)建線程雖然比創(chuàng)建進(jìn)程代價(jià)低,但并不是沒有代價(jià),高并發(fā)時(shí)(例如每秒上萬連接)還是有性能問題。
2)無須進(jìn)程間通信,但是線程間的互斥和共享又引入了復(fù)雜度,可能一不小心就導(dǎo)致了死鎖問題。
3)多線程會(huì)出現(xiàn)互相影響的情況,某個(gè)線程出現(xiàn)異常時(shí),可能導(dǎo)致整個(gè)進(jìn)程退出(例如內(nèi)存越界)。

除了引入了新的問題,TPC還是存在CPU線程調(diào)度和切換代價(jià)的問題。因此,TPC方案本質(zhì)上和PPC方案基本類似,在并發(fā)幾百連接的場(chǎng)景下,反而更多地是采用PPC的方案,因?yàn)镻PC方案不會(huì)有死鎖的風(fēng)險(xiǎn),也不會(huì)多進(jìn)程互相影響,穩(wěn)定性更高。

  • prethread
    TPC模式中,當(dāng)連接進(jìn)來時(shí)才創(chuàng)建新的線程來處理連接請(qǐng)求,雖然創(chuàng)建線程比創(chuàng)建進(jìn)程要更加輕量級(jí),但還是有一定的代價(jià),而prethread模式就是為了解決這個(gè)問題。
    prethread模式會(huì)預(yù)先創(chuàng)建線程(和prefork類似),然后才開始接受用戶的請(qǐng)求,當(dāng)有新的連接進(jìn)來的時(shí)候,就可以省去創(chuàng)建線程的操作,讓用戶感覺更快、體驗(yàn)更好。 由于多線程之間數(shù)據(jù)共享和通信比較方便,因此實(shí)際上prethread的實(shí)現(xiàn)方式相比prefork要靈活一些,
    常見的實(shí)現(xiàn)方式有下面幾種:
    1)主進(jìn)程accept,然后將連接交給某個(gè)線程處理。
    2)子線程都嘗試去accept,最終只有一個(gè)線程accept成功,基本示意圖如下:
image.png

Apache服務(wù)器的MPM worker模式本質(zhì)上就是一種prethread方案,但稍微做了改進(jìn)。
Apache服務(wù)器會(huì)首先創(chuàng)建多個(gè)進(jìn)程,每個(gè)進(jìn)程里面再創(chuàng)建多個(gè)線程,這樣做主要是為了考慮 穩(wěn)定性,即:即使某個(gè)子進(jìn)程里面的某個(gè)線程異常導(dǎo)致整個(gè)子進(jìn)程退出,還會(huì)有其他子進(jìn)程繼續(xù)提供服務(wù),不會(huì)導(dǎo)致整個(gè)服務(wù)器全部掛掉。
prethread理論上可以比prefork支持更多的并發(fā)連接,Apache服務(wù)器MPM worker模式默認(rèn)支持16 × 25 = 400 個(gè)并發(fā)處理線程。

  • PPC和TPC能夠支持的最大連接數(shù)差不多,都是幾百個(gè)。適用的場(chǎng)景也是差不多的。
    接著再從連接數(shù)和請(qǐng)求數(shù)來劃分,這兩種方式明顯不支持高連接數(shù)的場(chǎng)景。
    1. 常量連接海量請(qǐng)求。比如數(shù)據(jù)庫,redis,kafka等等
    1. 海量連接常量請(qǐng)求。比如企業(yè)內(nèi)部網(wǎng)址(運(yùn)營(yíng)系統(tǒng)),管理系統(tǒng)

應(yīng)對(duì)高并發(fā)場(chǎng)景的單服務(wù)器高性能架構(gòu)模式:Reactor和Proactor。

單服務(wù)器高性能的PPC和TPC模式,優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單,缺點(diǎn)是都無法支撐高并發(fā)的場(chǎng)景,尤其是互聯(lián)網(wǎng)發(fā)展到現(xiàn)在,各種海量用戶業(yè)務(wù)的出現(xiàn),PPC和TPC完全無能為力。

Reactor

  • PPC模式(以PPC和進(jìn)程為例,換成TPC和線程,原理一樣),最主要的問題就是每個(gè)連接都要?jiǎng)?chuàng)建進(jìn)程連接結(jié)束后進(jìn)程就銷毀了,這樣做其實(shí)是很大的浪費(fèi)。
    為解決這個(gè)問題,就是資源復(fù)用,即不再單獨(dú)為每個(gè)連接創(chuàng)建進(jìn)程,而是創(chuàng)建一個(gè)進(jìn)程池,將連接分配給進(jìn)程,一個(gè)進(jìn)程可以處理多個(gè)連接的業(yè)務(wù)。

  • 引入資源池的處理方式后,會(huì)引出一個(gè)新的問題:進(jìn)程如何才能高效地處理多個(gè)連接的業(yè)務(wù)?
    當(dāng)一個(gè)連接一個(gè)進(jìn)程時(shí),進(jìn)程可以采用“read -> 業(yè)務(wù)處理 -> write”的處理流程,如果當(dāng)前連接沒有數(shù)據(jù)可以讀,則進(jìn)程就阻塞在read操作上。
    這種阻塞的方式在一個(gè)連接一個(gè)進(jìn)程的場(chǎng)景下沒有問題,但如果一個(gè)進(jìn)程處理多個(gè)連接,進(jìn)程阻塞在某個(gè)連接的read操作 上,此時(shí)即使其他連接有數(shù)據(jù)可讀,進(jìn)程也無法去處理,很顯然這樣是無法做到高性能的。
    解決這個(gè)問題的最簡(jiǎn)單的方式是將read操作改為非阻塞,然后進(jìn)程不斷地輪詢多個(gè)連接。
    這種方式能夠解決阻塞的問題,但解決的方式并不優(yōu)雅。
    首先,輪詢是要消耗CPU的;
    其次,如果一個(gè)進(jìn)程處理幾千上萬的連接,則輪詢的效率是很低的。
    為了能夠更好地解決上述問題,很容易可以想到,只有當(dāng)連接上有數(shù)據(jù)的時(shí)候進(jìn)程才去處理,這就是I/O多路復(fù)用技術(shù)的來源。

  • I/O多路復(fù)用技術(shù)歸納起來有兩個(gè)關(guān)鍵實(shí)現(xiàn)點(diǎn):
    1).當(dāng)多條連接共用一個(gè)阻塞對(duì)象后,進(jìn)程只需要在一個(gè)阻塞對(duì)象上等待,而無須再輪詢所有連接,常見的實(shí)現(xiàn)方式有select、epoll、kqueue等。
    2). 當(dāng)某條連接有新的數(shù)據(jù)可以處理時(shí),操作系統(tǒng)會(huì)通知進(jìn)程,進(jìn)程從阻塞狀態(tài)返回,開始進(jìn)行業(yè)務(wù)處理。

  • I/O多路復(fù)用結(jié)合線程池(Reactor),完美地解決了PPC和TPC的問題.
    Reactor,中文是“反應(yīng)堆”,“事件反應(yīng)”的意思,通俗理解為“來了一個(gè)事件我(Reactor)就有相應(yīng)的反應(yīng)”.具體的反應(yīng)是我們寫的代碼.
    Reactor會(huì)根據(jù)事件類型來調(diào)用相應(yīng)的代碼進(jìn)行處理。
    Reactor模式也叫Dispatcher模式(就是實(shí)現(xiàn)Reactor模式的),更貼近模式本身的含義,即I/O多路復(fù)用統(tǒng)一監(jiān)聽事件,收到事件后分配(Dispatch)給某個(gè)進(jìn)程。

  • Reactor模式的核心組成部分包括Reactor和處理資源池(進(jìn)程池或線程池),其中Reactor負(fù)責(zé)監(jiān)聽和分配事件,處理資源池負(fù)責(zé)處理事件。初看Reactor的實(shí)現(xiàn)是比較簡(jiǎn)單的,但實(shí)際上結(jié)合不同的業(yè)務(wù)場(chǎng)景,Reactor模式的具體實(shí)現(xiàn)方案靈活多變,主要體現(xiàn)在:
    1)Reactor的數(shù)量可以變化:可以是一個(gè)Reactor,也可以是多個(gè)Reactor。
    2)資源池的數(shù)量可以變化:以進(jìn)程為例,可以是單個(gè)進(jìn)程,也可以是多個(gè)進(jìn)程(線程類似)。

最終Reactor模式有這三種典型的實(shí)現(xiàn)方案:
1)單Reactor單進(jìn)程/線程。
2)單Reactor多線程
3)多Reactor多進(jìn)程/線程。
以上方案具體選擇進(jìn)程還是線程,更多地是和編程語言及平臺(tái)相關(guān).
例如,Java語言一般使用線程(例如,Netty),C語言使用進(jìn)程和線程都可以。
例如,Nginx使用進(jìn)程,Memcache使用線程。

  1. 單Reactor單進(jìn)程/線程。
image.png

因此,單Reactor單進(jìn)程的方案在實(shí)踐中應(yīng)用場(chǎng)景不多,只適用于業(yè)務(wù)處理非常快速的場(chǎng)景.
目前比較著名的開源軟件中使用單Reactor單進(jìn)程的是Redis。
需注意,C語言編寫系統(tǒng)的一般使用單Reactor單進(jìn)程,因?yàn)闆]有必要在進(jìn)程中再創(chuàng)建線程;
而Java語言編寫的一般使用單Reactor單線程,因?yàn)镴ava虛擬機(jī)是一個(gè)進(jìn)程,虛擬機(jī)中有很多線程,業(yè)務(wù)線程只是其中的一個(gè)線程而已。

  1. 單Reactor多線程
    為了克服單Reactor單進(jìn)程/線程方案的缺點(diǎn),引入多進(jìn)程/多線程.
    產(chǎn)生了第2個(gè)方案:單Reactor多線程。
image.png

圖中只列出了“單Reactor多線程”方案,沒有列出“單Reactor多進(jìn)程”方案,這是什么原因呢?
主要原因在于如果采用多進(jìn)程,子進(jìn)程完成業(yè)務(wù)處理后,將結(jié)果返回給父進(jìn)程,并通知父進(jìn)程發(fā)送給哪個(gè)client,這是很麻煩的事情。
因?yàn)楦高M(jìn)程只是通過Reactor監(jiān)聽各個(gè)連接上的事件然后進(jìn)行分配,子進(jìn)程與父進(jìn)程通信時(shí)并不是一個(gè)連接。
如果要將父 進(jìn)程和子進(jìn)程之間的通信模擬為一個(gè)連接,并加入Reactor進(jìn)行監(jiān)聽,則是比較復(fù)雜的。
而采用多線程時(shí),因?yàn)槎嗑€程是共享數(shù)據(jù)的,因此線程間通信是非常方便的。雖然要額外考慮線程間共享數(shù)據(jù)時(shí)的同步問題,但這個(gè)復(fù)雜度比進(jìn)程間通信的復(fù)雜度要低很多。

  1. 多Reactor多進(jìn)程/線程。
    為了解決單Reactor多線程的問題,最直觀的方法就是將單Reactor改為多Reactor,這就產(chǎn)生了第3個(gè)方案:多Reactor多進(jìn)程/線程。
image.png

案例:
開源系統(tǒng)Nginx采用的是多Reactor多進(jìn)程,采用多Reactor多線程的實(shí)現(xiàn)有Memcache和Netty。

Proactor

Peactor是非阻塞同步網(wǎng)絡(luò)模型,因?yàn)檎嬲膔ead和send操作都需要用戶進(jìn)程同步操作。
這里的“同步”指用戶進(jìn)程在執(zhí)行read和send這類I/O操作的時(shí)候是同步的,如果把I/O操作 改為異步就能夠進(jìn)一步提升性能,這就是異步網(wǎng)絡(luò)模型Proactor。
Reactor可以理解為“來了事件我通知你,你來處理”。
而Proactor可以理解為“來了事件我來處理,處理完了我通知你”。
這里的“我”就是操作系統(tǒng)內(nèi)核,“事件”就是有新連接、有數(shù)據(jù)可讀、有數(shù)據(jù)可寫的這些I/O事 件,“你”就是我們的程序代碼。

image.png

理論上Proactor比Reactor效率要高一些,異步I/O能夠充分利用DMA特性,讓I/O操作與計(jì)算重疊,但要實(shí)現(xiàn)真正的異步I/O,操作系統(tǒng)需要做大量的工作。
目前Windows下通 過IOCP實(shí)現(xiàn)了真正的異步I/O,而在Linux系統(tǒng)下的AIO并不完善,因此在Linux下實(shí)現(xiàn)高并發(fā)網(wǎng)絡(luò)編程時(shí)都是以Reactor模式為主。
所以即使Boost.Asio號(hào)稱實(shí)現(xiàn)了Proactor模 型,其實(shí)它在Windows下采用IOCP,而在Linux下是用Reactor模式(采用epoll)模擬出來的異步模型。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容