/** * 謹獻給Yoyo * * 原文出處:https://www.toptal.com/software/guide-to-multi-processing-network-server-models * @author dogstar.huang <chanzonghuang@gmail.com> 2016-04-02 */
做爲多年來一直在編寫高性能網絡代碼的人(個人博士論文主題是適配多核系統分佈式應用的高速緩存服務),如今我看到了不少徹底不知道或忽略討論網絡服務模型基本原理的教程。所以,本文旨在但願能爲你們提供有用的概覽以及網絡服務模型的比較,以揭開編寫高性能網絡代碼的神祕面紗。
程序員
本文主要針對「系統程序員」,即與他們的應用程序的底層細節工做、實現網絡服務代碼的後端開發。這一般是在C++或C來完成,雖然時下大部分現代語言和框架經過各類級別的效率提供了體面的底層功能。算法
既然經過增長內核更容易擴展CPU,我會把這做爲常識,而本質倒是調整軟件以便最大化使用這些內核。所以,問題就變成如何在能在多個CPU上並行執行的線程(或進程)中分區軟件。後端
我也將理所固然地認爲讀者意識到,「併發」基本上意味着「多任務處理」,即一些代碼實例(不管是相同或不一樣的代碼,這並不重要),在同一時間是活躍的。併發能夠在單個CPU上實現,而且一般前現代時期是這樣的。具體地,併發能夠經過在單個CPU上的多個進程或線程之間快速切換來實現。這是老式的、單CPU系統如何管理在同一時間運行衆多應用程序的方式,在某種程度上,用戶會以爲應用程序是在同時執行,儘管實際上並無。另外一方面,平行度,從字面上看具體意味着代碼經過多個CPU或CPU內核在同一時間執行。緩存
出於這個討論的目的,假如咱們談論線程或全過程,它基本上是不相關的。現代操做系統(而Windows顯然是個例外)把進程看待像線程同樣輕量級(或在某些狀況下,反之亦然,線程都得到了功能,這使得它們像進程同樣重量級)。現在,進程和線程之間的主要區別是在跨進程或跨線程通訊和數據共享的功能。其中,進程和線程之間的區別是很重要的,我會進行適當的備註,不然,在這些部分能夠安全地考慮「線程」和「過程」是能夠互換的。安全
這篇文章具體處理網絡服務代碼,這部分須要實現如下三個任務:服務器
關於跨進程分區分區這三個任務,這裏有幾個廣泛的網絡服務模型,即:網絡
這些都是在學術界使用的網絡服務模型的名字,我記得「在野外」的同義詞發現至少其中的一些。(名字自己,固然,並非那麼重要的 -- 真正的價值是如何洞悉代碼是怎麼回事。)數據結構
這些網絡服務模型,每個都會在下面的部分中進一步說明。多線程
MP的網絡服務模型是每一個人都會首選用來學習的一個,特別是學習多線程的時候。在MP模型中,有一個「master」進程,接收鏈接(任務#1)。一旦創建了鏈接,主進程建立一個新的進程,並把鏈接的socket傳給它,因此一個進程一個鏈接。這個新的進程而後一般和此鏈接以簡單、連續、鎖步的方式工做:進程從鏈接中讀取一些東西(任務#2),而後作一些計算(任務#3),而後寫一些東西給它(再次 任務#2)。架構
模型MP是很容易實現的,並且實際工做極爲出色只要進程總數維持很低很低。有多低?答案取決於任務#2和任務#3蘊含了什麼。經驗法則,能夠說進程數或線程數不該超過CPU內核的兩倍。一旦有在同一時間激活太多進程,操做系統則趨於花費了太多在於時間抖動(即,圍繞可用的CPU內核上平衡進程或線程)和這樣的應用一般最終花費幾乎全部的CPU的一次在「SYS」(或內核)代碼,實際上卻作了一點點真正有用的工做。
優勢:實現很簡單,只要鏈接數不多能夠工做得很是好。
缺點:若是進程數增加太大則趨於使得操做系統過載太重,而且可能會有延遲抖動網絡IO等待,直到有效載荷(計算)階段結束。
該SPED網絡服務器模型,因最近一些高調的網絡服務應用程序,如Nginx而出名。基本上,它在同一個進程作了這三項任務,在它們之間之間複用。爲了提升效率,它須要像epoll和kqueue的一些至關先進的核心功能。在這種模型下,代碼是由傳入的鏈接和數據「事件」驅動,而且實現了一個看起來像這樣的「事件循環」:
全部這一切都在一個單一的進程中完成,而且能夠很是有效地完成,由於它徹底避免了進程之間的上下文切換,這一般會形成MP模型嚴重的性能問題。這裏惟一的上下文切換來自系統調用,而這些又經過僅做用於有某些事件綁定的具體鏈接而使得切換最小化。該模型能夠同時處理數萬的鏈接,只要有效載荷工做(任務#3)不是太複雜或是資源密集型的。
儘管這種方式有兩大缺點:
一、因爲三個任務都在一個單一的循環迭代中順序進行,有效載荷工做(任務#3)和全部東西都是同步完成的,也就是說,若是它須要很長的時間來計算到由客戶端接收的數據的響應,當正在作這點時其餘東西都會中止,而這會在延遲中引入潛在的巨大波動。
二、只使用一個CPU內核。這樣再次是有好處,絕對限制了來自操做系統要求的上下文切換數量,從而提升了總體性能,但有明顯的不足就是其餘任何可用的CPU內核都無事可作。
這是對於須要更先進的模型的理由。
優勢:能夠是具備高性能,在操做系統易於實現(即,須要最少許的OS干預)。只須要一個CPU內核。
缺點:僅利用單個CPU(無論可用的數量)。若是有效載荷工做不統一,會致使非均勻的響應延遲。
該SEDA網絡服務模型有點複雜。它把複雜的,事件驅動的應用程序分解到一組由隊列鏈接的階段。儘管若是不仔細實現,它的性能會跟MP狀況中同一問題而受到影響。它的工做原理是這樣的:
有效載荷工做(任務#3)會盡量地分紅多個階段,或模塊。每一個模塊實現了駐留在其本身單獨的進程中單個特定功能(可認爲是「微服務」或「微內核」),而且這些模塊經由消息隊列相互通訊。此架構能夠表示爲節點圖,其中節點是進程,邊是消息隊列。
一個單一進程執行任務#1(一般遵循SPED模型),它將新鏈接交付於特定的條目點節點。這些節點能夠是傳遞數據給其餘節點進行計算,或者也能夠是實現有效載荷處理(任務3#)的純網絡節點(任務#2)。一般沒有「master」進程(例如,一個收集並彙集響應,並將其經過鏈接發送返回),由於每個節點均可以經過自身進行響應。
理論上,這種模式能夠是任意複雜的,由於節點圖可能具備循環,鏈接到其餘相似的應用程序,或是鏈接到其實是在遠程系統上執行的節點。但在實踐中,即便有定義良好的消息和高效的隊列,它會變得笨拙難以思考,而且把系統的行爲做爲一個總體來推理。相比於SPED的模式,來往傳遞的消息可能會破壞該模型的性能,若是每一個節點的工做都是很簡短的話。該模型的效率顯然比SPED模型的要低,因此它一般採用在有效載荷的工做複雜且耗時的狀況。
優勢:軟件架構師最終的夢想:一切都分割成整齊而又獨立的模塊。
缺點:複雜度隨模塊數量而爆炸,而且消息隊列仍然比直接內存共享慢得多。
該AMPED網絡服務是SEDA馴服的,更易於模型的一個版本。沒有過多不一樣的模塊和進程,也沒有多過的消息隊列。下面是它如何工做的:
這裏最重要的是,有效負載工做是在一個固定的(一般配置的)數量的進程中進行,這獨立於鏈接的數量。這樣的好處是,有效負載能夠是任意複雜,而且也不會影響網絡IO(這是很好的等待時間)。並且還可能帶來更高的安全性,由於只有一個進程在作網絡IO。
優勢:網絡IO和有效載荷的工做分離很是清晰。
缺點:爲在進程之間來回傳遞數據利用消息隊列,而這根據不一樣協議的性質,可能成爲瓶頸。
該SYMPED網絡服務模型在許多方面是網絡服務模型的「聖盃」,由於它就像有獨立SPED「worker」進程的多個實例。它是經過由單一進程循環接收鏈接,而後將它們傳遞到工做進程得以實現,每個都有一個像SPED的事件循環。這有一些很是有利的後果:
事實上,這一點,也是最新版Nginx在作的;它們生產出少許工做進程,每一個運行一個事件循環。爲了使事情變得更好,大多數操做系統都提供了一個可由多個進程在一個獨立的TCP端口偵聽傳入鏈接的功能,省去了爲某個特定進程決定與網絡鏈接工做的須要。若是你正在使用的應用程序能夠經過這種方式來實現,我建議這樣作。
優勢:經過像SPED那樣循環可控制的數量,嚴格提升CPU使用率天花板。
缺點:因爲每一個過程有一個像SPED那樣的循環,若是有效載荷工做是不均勻的,等待時間能夠再次變化,就像與正常SPED模型那樣。
除了爲您的應用選擇最佳的構架模型外,這裏還有可用於進一步提升網絡代碼性能的一些低級招數。下面簡短列出了一些更有效的技巧:
一、避免動態內存分配。做爲一個解釋,簡單地看流行的內存分配代碼 - 他們使用複雜的數據結構,互斥,並其中只是簡單地這麼多的代碼(例如,jemalloc大概是450KiB左右的C代碼!)。上面大部分的模型可用徹底靜態的(或預先分配)網絡和/或僅在須要的地方改變線程之間全部權緩衝器來實現。
二、使用操做系統能夠提供最大值。大多數操做系統容許多個進程監聽一個單一socket,並在套接字直到接收到第一個字節(或甚至是第一個完整的請求!)時鏈接將不被接受時那裏實現功能收到。若是能夠請使用sendfile()。
三、瞭解您正在使用的網絡協議!例如,禁用Nagle算法一般是有意義的,而且若是(再)鏈接率高禁止持續是有意義的。學習TCP擁塞控制算法,看看它是否有意義去嘗試較一個新的。
在將來的博客文章,我能夠更多地談論這些,以及其餘技術和實用的技巧。但如今,這裏但願能爲編寫高性能網絡代碼提供關於的架構選擇一個有用的信息基礎,和它們的相對優點和劣勢。
------------------------