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

——————————————————摘抄自《極客時(shí)間 李運(yùn)華 從0開(kāi)始學(xué)架構(gòu)》
單服務(wù)器高性能的關(guān)鍵之一就是服務(wù)器采取的并發(fā)模型,并發(fā)模型有如下兩個(gè)關(guān)鍵設(shè)計(jì)點(diǎn):

  • 服務(wù)器如何管理連接。

  • 服務(wù)器如何處理請(qǐng)求。

以上兩個(gè)設(shè)計(jì)點(diǎn)最終都和操作系統(tǒng)的 I/O 模型及進(jìn)程模型相關(guān)。

  • I/O 模型:阻塞、非阻塞、同步、異步。

  • 進(jìn)程模型:?jiǎn)芜M(jìn)程、多進(jìn)程、多線程。

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

PPC

PPC 是 Process Per Connection 的縮寫(xiě),其含義是指每次有新的連接就新建一個(gè)進(jìn)程去專門(mén)處理這個(gè)連接的請(qǐng)求,這是傳統(tǒng)的 UNIX 網(wǎng)絡(luò)服務(wù)器所采用的模型?;镜牧鞒虉D是:

PPC
  • 父進(jìn)程接受連接(圖中 accept)。

  • 父進(jìn)程“fork”子進(jìn)程(圖中 fork)。

  • 子進(jìn)程處理連接的讀寫(xiě)請(qǐng)求(圖中子進(jìn)程 read、業(yè)務(wù)處理、write)。

  • 子進(jìn)程關(guān)閉連接(圖中子進(jìn)程中的 close)。

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

PPC 模式實(shí)現(xiàn)簡(jiǎn)單,比較適合服務(wù)器的連接數(shù)沒(méi)那么多的情況,例如數(shù)據(jù)庫(kù)服務(wù)器。對(duì)于普通的業(yè)務(wù)服務(wù)器,在互聯(lián)網(wǎng)興起之前,由于服務(wù)器的訪問(wèn)量和并發(fā)量并沒(méi)有那么大,這種模式其實(shí)運(yùn)作得也挺好,世界上第一個(gè) web 服務(wù)器 CERN httpd 就采用了這種模式(具體你可以參考https://en.wikipedia.org/wiki/CERN_httpd)?;ヂ?lián)網(wǎng)興起后,服務(wù)器的并發(fā)和訪問(wèn)量從幾十劇增到成千上萬(wàn),這種模式的弊端就凸顯出來(lái)了,主要體現(xiàn)在這幾個(gè)方面:

  • 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(寫(xiě)時(shí)復(fù)制)技術(shù),總體來(lái)說(shuō)創(chuàng)建進(jìn)程的代價(jià)還是很大的。

  • 父子進(jìn)程通信復(fù)雜:父進(jìn)程“fork”子進(jìn)程時(shí),文件描述符可以通過(guò)內(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 方案來(lái)傳遞信息。

  • 支持的并發(fā)連接數(shù)量有限:如果每個(gè)連接存活時(shí)間比較長(zhǎng),而且新的連接又源源不斷的進(jìn)來(lái),則進(jìn)程數(shù)量會(huì)越來(lái)越多,操作系統(tǒng)進(jìn)程調(diào)度和切換的頻率也越來(lái)越高,系統(tǒng)的壓力也會(huì)越來(lái)越大。因此,一般情況下,PPC 方案能處理的并發(fā)連接數(shù)量最大也就幾百。

prefork

PPC 模式中,當(dāng)連接進(jìn)來(lái)時(shí)才 fork 新進(jìn)程來(lái)處理連接請(qǐng)求,由于 fork 進(jìn)程代價(jià)高,用戶訪問(wèn)時(shí)可能感覺(jué)比較慢,prefork 模式的出現(xiàn)就是為了解決這個(gè)問(wèn)題。

顧名思義,prefork 就是提前創(chuàng)建進(jìn)程(pre-fork)。系統(tǒng)在啟動(dòng)的時(shí)候就預(yù)先創(chuàng)建好進(jìn)程,然后才開(kāi)始接受用戶的請(qǐng)求,當(dāng)有新的連接進(jìn)來(lái)的時(shí)候,就可以省去 fork 進(jìn)程的操作,讓用戶訪問(wèn)更快、體驗(yàn)更好。prefork 的基本示意圖是:

prefork

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

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

TPC

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

TPC 的基本流程是:

TPC
  • 父進(jìn)程接受連接(圖中 accept)。

  • 父進(jìn)程創(chuàng)建子線程(圖中 pthread)。

  • 子線程處理連接的讀寫(xiě)請(qǐng)求(圖中子線程 read、業(yè)務(wù)處理、write)。

  • 子線程關(guān)閉連接(圖中子線程中的 close)。

注意,和 PPC 相比,主進(jìn)程不用“close”連接了。原因是在于子線程是共享主進(jìn)程的進(jìn)程空間的,連接的文件描述符并沒(méi)有被復(fù)制,因此只需要一次 close 即可。

TPC 雖然解決了 fork 代價(jià)高和進(jìn)程通信復(fù)雜的問(wèn)題,但是也引入了新的問(wèn)題,具體表現(xiàn)在:

  • 創(chuàng)建線程雖然比創(chuàng)建進(jìn)程代價(jià)低,但并不是沒(méi)有代價(jià),高并發(fā)時(shí)(例如每秒上萬(wàn)連接)還是有性能問(wèn)題。

  • 無(wú)須進(jìn)程間通信,但是線程間的互斥和共享又引入了復(fù)雜度,可能一不小心就導(dǎo)致了死鎖問(wèn)題。

  • 多線程會(huì)出現(xiàn)互相影響的情況,某個(gè)線程出現(xiàn)異常時(shí),可能導(dǎo)致整個(gè)進(jìn)程退出(例如內(nèi)存越界)。

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

prethread

TPC 模式中,當(dāng)連接進(jìn)來(lái)時(shí)才創(chuàng)建新的線程來(lái)處理連接請(qǐng)求,雖然創(chuàng)建線程比創(chuàng)建進(jìn)程要更加輕量級(jí),但還是有一定的代價(jià),而 prethread 模式就是為了解決這個(gè)問(wèn)題。

和 prefork 類似,prethread 模式會(huì)預(yù)先創(chuàng)建線程,然后才開(kāi)始接受用戶的請(qǐng)求,當(dāng)有新的連接進(jìn)來(lái)的時(shí)候,就可以省去創(chuàng)建線程的操作,讓用戶感覺(jué)更快、體驗(yàn)更好。

由于多線程之間數(shù)據(jù)共享和通信比較方便,因此實(shí)際上 prethread 的實(shí)現(xiàn)方式相比 prefork 要靈活一些,常見(jiàn)的實(shí)現(xiàn)方式有下面幾種:

  • 主進(jìn)程 accept,然后將連接交給某個(gè)線程處理。

  • 子線程都嘗試去 accept,最終只有一個(gè)線程 accept 成功,方案的基本示意圖如下:

prethread

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ā)處理線程。

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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