2019獨角獸企業重金招聘Python工程師標準>>>
多線程網絡服務模型
/*** 謹獻給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顯然是個例外)把進程看待像線程一樣輕量級(或在某些情況下,反之亦然,線程都獲得了功能,這使得它們像進程一樣重量級)。如今,進程和線程之間的主要區別是在跨進程或跨線程通信和數據共享的功能。其中,進程和線程之間的區別是很重要的,我會進行適當的備注,否則,在這些部分可以安全地考慮“線程”和“過程”是可以互換的。
通用網絡應用任務與網絡服務模型
這篇文章具體處理網絡服務代碼,這部分需要實現以下三個任務:
- 任務#1:建立(和拆除)的網絡連接
- 任務#2:網絡通信(IO)
- 任務#3:有用的工作;例如負載或者應用程序為什么存在的原因
關于跨進程分區分區這三個任務,這里有幾個普遍的網絡服務模型,即:
- MP:多進程
- SPED:單進程,事件驅動
- SEDA:分階段的事件驅動架構
- AMPED:非對稱多進程事件驅動
- SYMPED:對稱多處理事件驅動
這些都是在學術界使用的網絡服務模型的名字,我記得“在野外”的同義詞發現至少其中的一些。(名字本身,當然,并不是那么重要的 -- 真正的價值是如何洞悉代碼是怎么回事。)
這些網絡服務模型,每一個都會在下面的部分中進一步說明。
多進程(MP)模型
MP的網絡服務模型是每個人都會首選用來學習的一個,特別是學習多線程的時候。在MP模型中,有一個“master”進程,接收連接(任務#1)。一旦建立了連接,主進程創建一個新的進程,并把連接的socket傳給它,所以一個進程一個連接。這個新的進程然后通常和此連接以簡單、連續、鎖步的方式工作:進程從連接中讀取一些東西(任務#2),然后做一些計算(任務#3),然后寫一些東西給它(再次 任務#2)。
模型MP是很容易實現的,而且實際工作極為出色只要進程總數維持很低很低。有多低?答案取決于任務#2和任務#3蘊含了什么。經驗法則,可以說進程數或線程數不應超過CPU內核的兩倍。一旦有在同一時間激活太多進程,操作系統則趨于花費了太多在于時間抖動(即,圍繞可用的CPU內核上平衡進程或線程)和這樣的應用通常最終花費幾乎所有的CPU的一次在“SYS”(或內核)代碼,實際上卻做了一點點真正有用的工作。
優點:實現很簡單,只要連接數很少可以工作得非常好。
缺點:如果進程數增長太大則趨于使得操作系統過載過重,并且可能會有延遲抖動網絡IO等待,直到有效載荷(計算)階段結束。
單進程事件驅動(SPED)模型
該SPED網絡服務器模型,因最近一些高調的網絡服務應用程序,如Nginx而出名。基本上,它在同一個進程做了這三項任務,在它們之間之間復用。為了提高效率,它需要像epoll和kqueue的一些相當先進的核心功能。在這種模型下,代碼是由傳入的連接和數據“事件”驅動,并且實現了一個看起來像這樣的“事件循環”:
- 問操作系統是否有任何新的網絡“事件”(如新的連接或輸入數據)
- 如果有新的可用連接,建立他們(任務#1)
- 如果有可用的數據,讀取它(任務#2)并對它采取行動(任務3#)
- 重復,直到服務器退出
所有這一切都在一個單一的進程中完成,并且可以非常有效地完成,因為它完全避免了進程之間的上下文切換,這通常會造成MP模型嚴重的性能問題。這里唯一的上下文切換來自系統調用,而這些又通過僅作用于有某些事件綁定的具體連接而使得切換最小化。該模型可以同時處理數萬的連接,只要有效載荷工作(任務#3)不是太復雜或是資源密集型的。
盡管這種方式有兩大缺點:
-
1、由于三個任務都在一個單一的循環迭代中順序進行,有效載荷工作(任務#3)和所有東西都是同步完成的,也就是說,如果它需要很長的時間來計算到由客戶端接收的數據的響應,當正在做這點時其他東西都會停止,而這會在延遲中引入潛在的巨大波動。
-
2、只使用一個CPU內核。這樣再次是有好處,絕對限制了來自操作系統要求的上下文切換數量,從而提高了整體性能,但有明顯的不足就是其他任何可用的CPU內核都無事可做。
這是對于需要更先進的模型的理由。
優點:可以是具有高性能,在操作系統易于實現(即,需要最少量的OS干預)。只需要一個CPU內核。
缺點:僅利用單個CPU(不管可用的數量)。如果有效載荷工作不統一,會導致非均勻的響應延遲。
分階段的事件驅動架構(SEDA)模型
該SEDA網絡服務模型有點復雜。它把復雜的,事件驅動的應用程序分解到一組由隊列連接的階段。盡管如果不仔細實現,它的性能會跟MP情況中同一問題而受到影響。它的工作原理是這樣的:
-
有效載荷工作(任務#3)會盡可能地分成多個階段,或模塊。每個模塊實現了駐留在其自己單獨的進程中單個特定功能(可認為是“微服務”或“微內核”),并且這些模塊經由消息隊列相互通信。此架構可以表示為節點圖,其中節點是進程,邊是消息隊列。
-
一個單一進程執行任務#1(通常遵循SPED模型),它將新連接交付于特定的條目點節點。這些節點可以是傳遞數據給其他節點進行計算,或者也可以是實現有效載荷處理(任務3#)的純網絡節點(任務#2)。通常沒有“master”進程(例如,一個收集并聚集響應,并將其通過連接發送返回),因為每一個節點都可以通過自身進行響應。
理論上,這種模式可以是任意復雜的,因為節點圖可能具有循環,連接到其他類似的應用程序,或是連接到實際上是在遠程系統上執行的節點。但在實踐中,即使有定義良好的消息和高效的隊列,它會變得笨拙難以思考,并且把系統的行為作為一個整體來推理。相比于SPED的模式,來往傳遞的消息可能會破壞該模型的性能,如果每個節點的工作都是很簡短的話。該模型的效率顯然比SPED模型的要低,所以它通常采用在有效載荷的工作復雜且耗時的情況。
優點:軟件架構師最終的夢想:一切都分割成整齊而又獨立的模塊。
缺點:復雜度隨模塊數量而爆炸,并且消息隊列仍然比直接內存共享慢得多。
非對稱多進程事件驅動(AMPED)模型
該AMPED網絡服務是SEDA馴服的,更易于模型的一個版本。沒有過多不同的模塊和進程,也沒有多過的消息隊列。下面是它如何工作的:
- 以SPED風格在一個單一的“master”進程中實現任務#1和任務#2。這是網絡IO的唯一進程。
- 在一個單獨的“worker”進程中實現任務#3(可能在多個實例中啟動),通過一個隊列連接到主進程(每個進程一個隊列)。
- 當“master”進程接收到數據,找到一個沒有被充分利用(或空閑)的工作進程,并把數據傳遞給它的消息隊列。當響應準備好時主進程由該進程發起消息通知,此時它通過連接傳遞響應。
這里最重要的是,有效負載工作是在一個固定的(通常配置的)數量的進程中進行,這獨立于連接的數量。這樣的好處是,有效負載可以是任意復雜,并且也不會影響網絡IO(這是很好的等待時間)。而且還可能帶來更高的安全性,因為只有一個進程在做網絡IO。
優點:網絡IO和有效載荷的工作分離非常清晰。
缺點:為在進程之間來回傳遞數據利用消息隊列,而這根據不同協議的性質,可能成為瓶頸。
對稱多處理事件驅動(SYMPED)模型
該SYMPED網絡服務模型在許多方面是網絡服務模型的“圣杯”,因為它就像有獨立SPED“worker”進程的多個實例。它是通過由單一進程循環接收連接,然后將它們傳遞到工作進程得以實現,每一個都有一個像SPED的事件循環。這有一些非常有利的后果:
- CPU都為生成的進程的準確數量而加載,這在每個時間點要么做網絡IO或有效載荷處理。沒有辦法進一步提升CPU利用率。
- 如果連接是獨立的(例如使用HTTP),在工作進程之間則沒有間通信。
事實上,這一點,也是最新版Nginx在做的;它們生產出少量工作進程,每個運行一個事件循環。為了使事情變得更好,大多數操作系統都提供了一個可由多個進程在一個獨立的TCP端口偵聽傳入連接的功能,省去了為某個特定進程決定與網絡連接工作的需要。如果你正在使用的應用程序可以通過這種方式來實現,我建議這樣做。
優點:通過像SPED那樣循環可控制的數量,嚴格提高CPU使用率天花板。
缺點:由于每個過程有一個像SPED那樣的循環,如果有效載荷工作是不均勻的,等待時間可以再次變化,就像與正常SPED模型那樣。
一些低級技巧
除了為您的應用選擇最佳的構架模型外,這里還有可用于進一步提高網絡代碼性能的一些低級招數。下面簡短列出了一些更有效的技巧:
-
1、避免動態內存分配。作為一個解釋,簡單地看流行的內存分配代碼 - 他們使用復雜的數據結構,互斥,并其中只是簡單地這么多的代碼(例如,jemalloc大概是450KiB左右的C代碼!)。上面大部分的模型可用完全靜態的(或預先分配)網絡和/或僅在需要的地方改變線程之間所有權緩沖器來實現。
-
2、使用操作系統可以提供最大值。大多數操作系統允許多個進程監聽一個單一socket,并在套接字直到接收到第一個字節(或甚至是第一個完整的請求!)時連接將不被接受時那里實現功能收到。如果可以請使用sendfile()。
-
3、了解您正在使用的網絡協議!例如,禁用Nagle算法通常是有意義的,并且如果(再)連接率高禁止持續是有意義的。學習TCP擁塞控制算法,看看它是否有意義去嘗試較一個新的。
在未來的博客文章,我可以更多地談論這些,以及其他技術和實用的技巧。但現在,這里希望能為編寫高性能網絡代碼提供關于的架構選擇一個有用的信息基礎,和它們的相對優勢和劣勢。
------------------------
本作品采用知識共享署名-非商業性使用-相同方式共享 3.0 未本地化版本許可協議進行許可。
- 本文翻譯作者為:dogstar,發表于艾翻譯(itran.cc);歡迎轉載,但請注明出處,謝謝!