為什麼事件驅動伺服器這麼火

Ant發表於2020-04-06

原文出處:http://geogeo.github.com/blog/2012/12/31/node-dot-js-vs-tornado-vs-php/


OPPC模型瓶頸

傳統伺服器模型如Apache為每一個請求生成一個子程式。當使用者連線到伺服器的一個子程式就產生,並處理連線。每個連線獲得一個單獨的執行緒和子程式。當使用者請求資料返回時,子程式開始等待資料庫操作返回。如果此時另一個使用者也請求返回資料,這時就產生了阻塞。

這種模式在非常小的工作負荷是表現良好,當請求的數量變得太大是伺服器會壓力過於巨大。 當Apache達到程式的最大數量,所有程式都變得緩慢。每個請求都有自己的執行緒,如果服務程式碼使用PHP編寫時,每個程式所需要的記憶體量是相當大的[1]。


fork()操作延時

事實上,基於OPPC的網路並不如想象中的高效。首先新建程式的效能很大程度上依賴於作業系統對fork()的實現,然而不同作業系統的處理並非都理想。如圖為各作業系統fork()的延遲時間對比。

作業系統fork操作只是簡單的拷貝分頁對映。動態連結為共享庫和全域性偏移表中的ELF(Executable and Linking Format)部分建立太多的分頁對映。雖然靜態的連結fork會是的效能大幅度提升,但是延時依然不樂觀。


程式排程

Linux每10毫秒(Alpha是1毫秒,該值為已編譯常量)中斷一次在執行態的程式,檢視是否要切換別的程式執行。程式排程的任務就是決定下一個應該執行的程式,而其難度就在於如何公平的分配CPU資源。一個好的排程演算法應該給每一個程式都分享公平的CPU資源,而且不應該出現飢餓程式。

Unix系統採用多級反饋佇列排程演算法。使用多個不同優先順序的就緒佇列,使用Heap保持佇列按優先順序順序排序。Linux 2.6版本提供了一個複雜度O(1)的排程演算法,將程式排程延時降至最小。但是程式排程的頻率是100Hz,意味著10毫秒會中止一個程式而判斷是否需要切換到另一個程式。如果切換過多,會讓CPU忙於切換,導致降低吞吐量。

建立多程式會帶來另外一個問題:記憶體消耗。

每一個建立的程式都會佔用記憶體,在Linux 2.6中的測試結果,400個左右的連線後fork()的效能要超過pthread_create()的效能。IBM對Linux做過優化後,一個程式可以處理10萬個連線。fork()在每一個連線時都fork()一次成本太高,多執行緒在於需要考慮執行緒安全(thread-safe)與死鎖(deadlock),以及記憶體洩露問題這些問題。


可靠性

該模型具有可靠性問題。一個配置不當的伺服器,很容易遭受拒絕服務攻擊(DoS)。當大量併發請求的伺服器資源時,負載均衡配置不當時伺服器會很快耗盡源而奔潰。

同步阻塞 I/O

在這個模型中,應用程式執行一個系統呼叫,這會導致應用程式阻塞。這意味著應用程式會一直阻塞,直到系統呼叫完成為止(資料傳輸完成或發生錯誤)。呼叫應用程式處於一種不再佔用CPU,而只是簡單等待響應的狀態,但是該程式依然佔用著資源。當大量併發I/O請求到達時,則會產生I/O阻塞,造成伺服器瓶頸。


事件驅動模型伺服器

通過上訴分析與實驗說明,事實上,作業系統並不是設計來處理伺服器工作負載。傳統的執行緒模型是基於執行應用程式是的一些密集型操作的需要。 作業系統的設計是讓使用者執行的多執行緒程式,使後臺檔案寫入和UI操作同時進行,而並不是設計於處理大量併發請求連線。

Fork和多執行緒是相當費資源的操作,建立執行緒需要分配一個全新的記憶體堆疊。此外,上下文切換也是一項開銷的,CPU排程模型是並不太適合一個傳統的Web伺服器。

因此,OPPC模型面臨著多程式多執行緒的延遲已經記憶體消耗的問題。要用OPPC模型解決C10K問題顯得十分複雜。

為解決C10K問題,一些新的伺服器呈現出來。下列是解決C10K問題的Web伺服器

  • nginx:一個基於事件驅動的處理請求架構反向代理伺服器。
  • Cherokee:Twitter使用的開源Web伺服器。
  • Tornado:一個Python語言實現的非阻塞式Web伺服器框架。Facebook的FriendFeed模組使用此框架完成。
  • Node.js:非同步非阻塞Web伺服器,執行於Google V8 JavaScript引擎。


顯然以上解決C10K問題的伺服器都有著共同特點:事件驅動,非同步非阻塞技術。

由於網路負載工作包括大量的等待。比如 Apache伺服器,產生大量的子程式,需要消耗大量記憶體。但大多數子程式佔用大量記憶體資源卻只是在等待一個阻塞任務結束。由於這一特點,新模型拋棄了對每個請求生成子程式的想法。所有的請求和事物操作只使用一個單獨的執行緒管理,此執行緒被稱之為事件迴圈。事件迴圈將非同步的管理所有使用者連線與檔案儲存或資料庫伺服器。當請求到達時,使用poll或者select喚醒作業系統對其請求做相應處理。解決了很多問題。這樣以來處理的併發請求不再是緊緊圍繞在阻塞資源。當然,這樣也有一定的開銷,如保持一個始終開啟的TCP連線的列表,但記憶體並不會由於大量併發請求而急速上升,因為這個列表只佔記憶體堆上很小的一部分。Node.js和Nginx的都用這種方法來構建應用程式的規模超級大的連線數。一切操作都由一個事件迴圈管理,並很好地處理多個連線)。

目前最為流行的事件驅動的非同步非阻塞式I/O的Web伺服器Node.js,稱其會在記憶體佔用上更為高效,而且由於不是傳統OPPC模式,也不用擔心死鎖。Node.js沒有函式直接執行I/O操作,因此也不會產生阻塞。

在1000併發請求的壓力測試中可以看到,基於事件驅動的Node.js與Tornado都比傳統OPPC模型的Apache伺服器要快。當然Node.js的效能也離不開其執行於Google V8引擎上的原因。兩個事件驅動模型伺服器平均每秒處理的請求數為Apache伺服器的一倍,而記憶體降低了一半。圖2顯示事件驅動模型伺服器會佔用更高的CPU,這說明這種模型雖然是單執行緒執行,但是能更高效的利用CPU處理更多的併發請求。

存在的不足

事件迴圈並不能解決一切問題[2]。特別是在Node.js的有一些缺陷。Node.js的最明顯的遺漏是多執行緒的實現。事件驅動技術似乎應該都是多執行緒進行的,如大多數事件驅動GUI框架。理論上來說,事件之間應該是相互獨立的關係,因此並行化應該並不難實現。

雖然理論上是這樣,但一些技術上的原因使得Node.js難以實現多執行緒。Node.js執行與Google的V8 Javascript引擎上12。V8引擎是一個高效能的JavaScript引擎,但它並沒有設計為多執行緒。因為它原本為Google Chrome瀏覽器Javascript引擎,瀏覽器中Javascropt在一個單執行緒上執行。因此新增多執行緒,將是非常艱難的,底層架構並非為伺服器而設計。


未來

隨著nginx這樣的反向代理伺服器的發展,可以讓獨立執行的例項之間的負載均衡,Node.js的作者提出對解決多執行緒缺陷的最好的辦法是使用fork子程式,利用負載均衡來達到伺服器併發任務處理。 這種解決方案似乎像是要掩蓋其實現上的缺陷。但事件驅動模型倡導一個邏輯伺服器應該應該能在單核CPU下表現得最優,以及佔用更少的記憶體。與此相反,Apache的最初目的是以一切可利用的資源為代價充分高效管理併發和執行緒。事件驅動模型伺服器避開了這種繁瑣的設計而用最簡潔高效的方式實現了可擴充套件性良好的伺服器。

單執行緒的也正符合雲端計算的平臺的計算單位。很明顯,一個單一的雲例項,非常適合執行一個單一的Node.js的伺服器,並使用負載均衡橫向擴充套件。


事件驅動模型的出現,是為了解決傳統伺服器與網路工作負載的需求的不匹配,實現高度可伸縮伺服器,並降低記憶體開銷。事情驅動模型更改了連線到伺服器的方式。所有的連線都由事件迴圈管理,每個連線觸發一個在事件迴圈程式中執行的事件,而不是為每個連線生成一個新的 OS 執行緒,併為其分配一些配套記憶體。因此不用擔心出現死鎖,而且不會直接呼叫阻塞資源,而採用非同步的方式來實現非阻塞式I/O。通過事件驅動模型是的在相同配置的伺服器能接受更多的併發請求,實現可伸縮的伺服器。


相關文章