19 | 單服務器高性能模式:Reactor與Proactor

此爲筆記

課程鏈接

https://time.geekbang.org/column/intro/100006601?utm_source=time_web&utm_medium=menu&utm_term=timewebmenu


高性能是每個程序員的追求,無論我們是做一個系統還是寫一行代碼,都希望能夠達到高性能的效果,而高性能又是最複雜的一環,磁盤、操作系統、CPU、內存、緩存、網絡、編程語言、架構等,每個都有可能影響系統達到高性能,一行不恰當的 debug 日誌,就可能將服務器的性能從 TPS 30000 降低到 8000;一個 tcp_nodelay 參數,就可能將響應時間從 2 毫秒延長到 40 毫秒。因此,要做到高性能計算是一件很複雜很有挑戰的事情,軟件系統開發過程中的不同階段都關係着高性能最終是否能夠實現。

站在架構師的角度,當然需要特別關注高性能架構的設計。高性能架構設計主要集中在兩方面:

  • 儘量提升單服務器的性能,將單服務器的性能發揮到極致。

  • 如果單服務器無法支撐性能,設計服務器集羣方案。

除了以上兩點,最終系統能否實現高性能,還和具體的實現及編碼相關。但架構設計是高性能的基礎,如果架構設計沒有做到高性能,則後面的具體實現和編碼能提升的空間是有限的。形象地說,架構設計決定了系統性能的上限,實現細節決定了系統性能的下限。

單服務器高性能的關鍵之一就是服務器採取的併發模型,併發模型有如下兩個關鍵設計點:

  • 服務器如何管理連接。

  • 服務器如何處理請求。

以上兩個設計點最終都和操作系統的 I/O 模型及進程模型相關。

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

  • 進程模型:單進程、多進程、多線程。

在下面詳細介紹併發模型時會用到上面這些基礎的知識點,所以我建議你先檢測一下對這些基礎知識的掌握情況,更多內容你可以參考《UNIX 網絡編程》三卷本。今天,我們先來看看單服務器高性能模式:PPC 與 TPC。

PPC

PPC 是 Process Per Connection 的縮寫,其含義是指每次有新的連接就新建一個進程去專門處理這個連接的請求,這是傳統的 UNIX 網絡服務器所採用的模型。基本的流程圖是:

  • 父進程接受連接(圖中 accept)。

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

  • 子進程處理連接的讀寫請求(圖中子進程 read、業務處理、write)。

  • 子進程關閉連接(圖中子進程中的 close)。

注意,圖中有一個小細節,父進程“fork”子進程後,直接調用了 close,看起來好像是關閉了連接,其實只是將連接的文件描述符引用計數減一,真正的關閉連接是等子進程也調用 close 後,連接對應的文件描述符引用計數變爲 0 後,操作系統纔會真正關閉連接,更多細節請參考《UNIX 網絡編程:卷一》。

PPC 模式實現簡單,比較適合服務器的連接數沒那麼多的情況,例如數據庫服務器。對於普通的業務服務器,在互聯網興起之前,由於服務器的訪問量和併發量並沒有那麼大,這種模式其實運作得也挺好,世界上第一個 web 服務器 CERN httpd 就採用了這種模式(具體你可以參考https://en.wikipedia.org/wiki/CERN_httpd)。互聯網興起後,服務器的併發和訪問量從幾十劇增到成千上萬,這種模式的弊端就凸顯出來了,主要體現在這幾個方面:

  • fork 代價高:站在操作系統的角度,創建一個進程的代價是很高的,需要分配很多內核資源,需要將內存映像從父進程複製到子進程。即使現在的操作系統在複製內存映像時用到了 Copy on Write(寫時複製)技術,總體來說創建進程的代價還是很大的。

  • 父子進程通信複雜:父進程“fork”子進程時,文件描述符可以通過內存映像複製從父進程傳到子進程,但“fork”完成後,父子進程通信就比較麻煩了,需要採用 IPC(Interprocess Communication)之類的進程通信方案。例如,子進程需要在 close 之前告訴父進程自己處理了多少個請求以支撐父進程進行全局的統計,那麼子進程和父進程必須採用 IPC 方案來傳遞信息。

  • 支持的併發連接數量有限:如果每個連接存活時間比較長,而且新的連接又源源不斷的進來,則進程數量會越來越多,操作系統進程調度和切換的頻率也越來越高,系統的壓力也會越來越大。因此,一般情況下,PPC 方案能處理的併發連接數量最大也就幾百。

prefork

PPC 模式中,當連接進來時才 fork 新進程來處理連接請求,由於 fork 進程代價高,用戶訪問時可能感覺比較慢,prefork 模式的出現就是爲了解決這個問題。

顧名思義,prefork 就是提前創建進程(pre-fork)。系統在啓動的時候就預先創建好進程,然後纔開始接受用戶的請求,當有新的連接進來的時候,就可以省去 fork 進程的操作,讓用戶訪問更快、體驗更好。prefork 的基本示意圖是:

prefork 的實現關鍵就是多個子進程都 accept 同一個 socket,當有新的連接進入時,操作系統保證只有一個進程能最後 accept 成功。但這裏也存在一個小小的問題:“驚羣”現象,就是指雖然只有一個子進程能 accept 成功,但所有阻塞在 accept 上的子進程都會被喚醒,這樣就導致了不必要的進程調度和上下文切換了。幸運的是,操作系統可以解決這個問題,例如 Linux 2.6 版本後內核已經解決了 accept 驚羣問題。

prefork 模式和 PPC 一樣,還是存在父子進程通信複雜、支持的併發連接數量有限的問題,因此目前實際應用也不多。Apache 服務器提供了 MPM prefork 模式,推薦在需要可靠性或者與舊軟件兼容的站點時採用這種模式,默認情況下最大支持 256 個併發連接。

TPC

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

TPC 的基本流程是:

  • 父進程接受連接(圖中 accept)。

  • 父進程創建子線程(圖中 pthread)。

  • 子線程處理連接的讀寫請求(圖中子線程 read、業務處理、write)。

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

注意,和 PPC 相比,主進程不用“close”連接了。原因是在於子線程是共享主進程的進程空間的,連接的文件描述符並沒有被複制,因此只需要一次 close 即可。

TPC 雖然解決了 fork 代價高和進程通信複雜的問題,但是也引入了新的問題,具體表現在:

  • 創建線程雖然比創建進程代價低,但並不是沒有代價,高併發時(例如每秒上萬連接)還是有性能問題。

  • 無須進程間通信,但是線程間的互斥和共享又引入了複雜度,可能一不小心就導致了死鎖問題。

  • 多線程會出現互相影響的情況,某個線程出現異常時,可能導致整個進程退出(例如內存越界)。

除了引入了新的問題,TPC 還是存在 CPU 線程調度和切換代價的問題。因此,TPC 方案本質上和 PPC 方案基本類似,在併發幾百連接的場景下,反而更多地是採用 PPC 的方案,因爲 PPC 方案不會有死鎖的風險,也不會多進程互相影響,穩定性更高。

prethread

TPC 模式中,當連接進來時才創建新的線程來處理連接請求,雖然創建線程比創建進程要更加輕量級,但還是有一定的代價,而 prethread 模式就是爲了解決這個問題。

和 prefork 類似,prethread 模式會預先創建線程,然後纔開始接受用戶的請求,當有新的連接進來的時候,就可以省去創建線程的操作,讓用戶感覺更快、體驗更好。

由於多線程之間數據共享和通信比較方便,因此實際上 prethread 的實現方式相比 prefork 要靈活一些,常見的實現方式有下面幾種:

  • 主進程 accept,然後將連接交給某個線程處理。

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



Apache 服務器的 MPM worker 模式本質上就是一種 prethread 方案,但稍微做了改進。Apache 服務器會首先創建多個進程,每個進程裏面再創建多個線程,這樣做主要是爲了考慮穩定性,即:即使某個子進程裏面的某個線程異常導致整個子進程退出,還會有其他子進程繼續提供服務,不會導致整個服務器全部掛掉。

prethread 理論上可以比 prefork 支持更多的併發連接,Apache 服務器 MPM worker 模式默認支持 16 × 25 = 400 個併發處理線程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章