深入NGINX:我們如何設計它的效能和擴充套件性

發表於2019-05-11

NGINX之所以能在效能上如此優越,是由於其背後的設計。許多web伺服器和應用伺服器使用簡單的執行緒的(threaded)、或基於流程的(process-based)架構, NGINX則以一種複雜的事件驅動(event-driven)的架構脫穎而出,這種架構能支援現代硬體上成千上萬的併發連線。

Inside NGINX infographic涉及了從高層次程式架構的挖掘,到NGINX的單程式處理多連線的圖解。本篇文章講解了這些工作細節。

設定場景——NGINX程式模型
Setting the Scene – the NGINX Process Model

[img=NGINX,效能調優]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150614/-1920617532.png[/img]

為了更好地理解設計,你需要了解NGINX是如何工作的。NGINX有一個主程式(master process)(執行特權操作,如讀取配置、繫結埠)和一系列工作程式(worker process)和輔助程式(helper process)。

[img=NGINX,效能調優]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150614/-1818696695.png[/img]

這個四核伺服器內,NGINX主程式建立了4個工作程式和2個快取輔助程式(cache helper processes)來管理磁碟內容快取(on-disk content cache)。
為什麼架構很重要?
Why Is Architecture Important?
任何Unix應用程式的根本基礎都是執行緒或程式。(從Linux作業系統的角度看,執行緒和程式基本上是相同的,主要區別是他們共享記憶體的程度。)程式或執行緒,是一組作業系統可排程的、執行在CPU核心上的獨立指令集。大多數複雜的應用程式都並行執行多個執行緒或程式,原因有兩個:
  • ● 可以同時使用更多的計算機核心。
  • ● 執行緒和程式使並行操作很容易實現(例如,同時處理多個連線)。
程式和執行緒都消耗資源。它們都使用記憶體和其他OS資源,導致核心頻繁切換(被稱作上下文切換(context switch)的操作)。大多數現代伺服器可以同時處理數百個小的、活躍的(active)執行緒或程式,但一旦記憶體耗盡,或高I/O負載導致大量的上下文切換時,伺服器的效能就會嚴重下降。

對於網路應用,通常會為每個連線(connection)分配一個執行緒或程式。這種架構易於實現,但是當應用程式需要處理成千上萬的併發連線時,這種架構的擴充套件性就會出現問題。
NGINX是如何工作的?
How Does NGINX Work?
NGINX使用一個了可預見式的(predictable)程式模型,排程可用的硬體資源:
1.主程式執行特權操作,如讀取配置和繫結埠,還負責建立子程式(下面的三種型別)。 
2.快取載入程式(cache loader process)在啟動時執行,把基於磁碟的快取(disk-based cache)載入到記憶體中,然後退出。對它的排程很謹慎,所以其資源需求很低。
3.快取管理程式(cache manager process)週期性執行,並削減磁碟快取(prunes entries from the disk caches),以使其保持在配置範圍內。
4.工作程式(worker processes)才是執行所有實際任務的程式:處理網路連線、讀取和寫入內容到磁碟,與上游伺服器通訊等。
多數情況下,NGINX建議每1個CPU核心都執行1個工作程式,使硬體資源得到最有效的利用。你可以在配置中設定如下指令:
worker_processes auto
當NGINX伺服器在執行時,只有工作程式在忙碌。每個工作程式都以非阻塞的方式處理多個連線,以消減上下文切換的開銷。
每個工作程式都是單執行緒且獨立執行的,抓取並處理新的連線。程式間通過共享記憶體的方式,來共享快取資料、會話永續性資料(session persistence data)和其他共享資源。
NGINX內部的工作程式
Inside the NGINX Worker Process
每一個NGINX的工作程式都是NGINX配置(NGINX configuration)初始化的,並被主程式設定了一組監聽套接字(listen sockets)。
[img=NGINX,效能調優]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150614/-469944474.png[/img]

NGINX工作程式會監聽套接字上的事件(accept_mutexkernel socket sharding),來決定什麼時候開始工作。事件是由新的連線初始化的。這些連線被會分配給狀態機(state machine)——HTTP狀態機是最常用的,但NGINX還為流(原生TCP)和大量的郵件協議(SMTP,IMAP和POP3)實現了狀態機。
[img=NGINX,效能調優]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150614/646475436.png[/img]

狀態機本質上是一組告知NGINX如何處理請求的指令。大多數和NGINX具有相同功能的web伺服器也使用類似的狀態機——只是實現不同。
排程狀態機
Scheduling the State Machine
把狀態機想象成國際象棋的規則。每個HTTP事務(HTTP transaction)都是一局象棋比賽。棋盤的一邊是web伺服器——坐著一位可以迅速做出決定的大師級棋手。另一邊是遠端客戶端——在相對較慢的網路中,訪問站點或應用程式的web瀏覽器。
然而,比賽的規則可能會很複雜。例如,web伺服器可能需要與各方溝通(代理一個上游的應用程式),或者和認證伺服器交流。web伺服器的第三方模組也可以擴充比賽規則。
阻塞狀態機
A Blocking State Machine
回憶一下我們之前對程式和執行緒的描述:是一組作業系統可排程的、執行在CPU核心上的獨立指令集。大多數web伺服器和web應用都使用一個連線/一個程式或一個連線/一個執行緒的模型來進行這局國際象棋比賽。每個程式或執行緒都包含一個將比賽玩到最後的指令。在這個過程中,程式是由伺服器來執行的,它的大部分時間都花在“阻塞(blocked)”上,等待客戶端完成其下一個動作。

[img=NGINX,效能調優]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150614/456631041.png[/img]

  • 1.web伺服器程式(web server process)在監聽套接字上,監聽新的連線(客戶端發起的新比賽)。
  • 2.一局新的比賽發起後,程式就開始工作,每一步棋下完後都進入阻塞狀態,等待客戶端走下一步棋。
  • 3.一旦比賽結束,web伺服器程式會看看客戶是否想開始新的比賽(這相當於一個存活的連線)。如果連線被關閉(客戶端離開或者超時),web伺服器程式會回到監聽狀態,等待全新的比賽。

記住重要的一點:每一個活躍的HTTP連線(每局象棋比賽)都需要一個專用的程式或執行緒(一位大師級棋手)。這種架構非常易於擴充套件第三方模組(“新規則”)。然而,這裡存在著一個巨大的不平衡:一個以檔案描述符(file descriptor)和少量記憶體為代表的輕量級HTTP連線,會對映到一個單獨的程式或執行緒——它們是非常重量級的作業系統物件。這在程式設計上是方便的,但它造成了巨大的浪費。
NGINX是真正的大師
NGINX is a True Grandmaster
也許你聽說過車輪表演賽,在比賽中一個象棋大師要在同一時間對付幾十個對手。
[img=NGINX,效能調優]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150614/-1356490852.png[/img]

Kiril Georgiev在保加利亞首都索菲亞同時對陣360名棋手,最終取得284勝,70平,6負的戰績。

這就是 NGINX工作程式玩“國際象棋”的方式。每一個工作程式都是一位大師(記住:通常情況下,每個工作程式佔用一個CPU核心),能夠同時對戰上百棋手(實際上是成千上萬)。

[img=NGINX,效能調優]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150614/1014043748.png[/img]

1.工作程式在監聽套接字和連線套接字上等待事件。
2.事件發生在套接字上,工作程式會處理這些事件。

  • ● 監聽套接字上的事件意味著:客戶端開始了一局新的遊戲。工作程式建立了一個新的連線套接字。
  • ● 連線套接字上的事件意味著:客戶端移動了棋子。工作程式會迅速響應。


工作程式從不會在網路上停止,它時時刻刻都在等待其“對手”(客戶端)做出回應。當它已經移動了這局比賽的棋子,它會立即去處理下一局比賽,或者迎接新的對手。
為什麼它會比阻塞式多程式的架構更快?
Why Is This Faster than a Blocking, Multi-Process Architecture?
NGINX的規模可以很好地支援每個工作程式上數以萬計的連線。每個新連線都會建立另一個檔案描述符,並消耗工作程式中少量的額外記憶體。每一個連線的額外消耗都很少。NGINX程式可以保持固定的CPU佔用率。當沒有工作時,上下文切換也較少。
在阻塞式的、一個連線/一個程式的模式中,每個連線需要大量的額外資源和開銷,並且上下文切換(從一個程式到另一個程式)非常頻繁。
如果想了解更多,請檢視由NGINX公司發展和聯合創始人副總裁Andrew Alexeev編寫的有關 NGINX體系結構的文章
通過適當的系統調優,NGINX能大規模地處理每個工作程式數十萬併發的HTTP連線,並且能在流量高峰期間不丟失任何資訊(新比賽開始)。
配置更新和NGINX升級
Updating Configuration and Upgrading NGINX
僅包含少量工作程式的NGINX程式架構,使得配置、甚至是二進位制檔案本身的更新都非常高效。

[img=NGINX,效能調優]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150614/-1510778969.png[/img]

更新NGINX的配置,是一個非常簡單的、輕量級的、可靠的操作。執行nginx –s reload命令即可,該命令會檢查磁碟上的配置,並給主程式傳送一個SIGHUP訊號。
當主程式接收到SIGHUP訊號後,會做兩件事:

  • 1.重新載入配置,fork一套新的工作程式。這些新的工作程式會立即開始接受連線和處理流量(traffic)(使用新的配置)。
  • 2.發出訊號,通知舊的工作程式安靜地退出。這些舊程式不會再接受新的連線了。只要它們處理的HTTP請求結束了,它們就會乾淨地關閉連線。一旦所有的連線都被關閉,工作程式也就退出了。


這個過程會導致CPU佔用率和記憶體使用的一個小高峰,但相比於從活動連線中載入資源,這個小高峰可忽略不計。你可以在一秒內重新載入配置多次。極少情況下,一代又一代工作程式等待連線關閉時會出現問題,但即便出現問題,它們也會被立即解決掉。
NGINX的二進位制升級過程更加神奇——你可以飛速地升級NGINX本身,伺服器不會有任何的丟連線、當機、或服務中斷等情況。

[img=NGINX,效能調優]https://jf-bucket-public.oss-cn-qingdao.aliyuncs.com/jfperiodical/attached/image/20150614/-1885031265.png[/img]

二進位制升級過程與配置更新相似。新的NGINX主程式與原來的主程式並行,它們共享監聽套接字。兩個程式都是活躍的(active),它們各自的工作程式處理各自的流量(traffic)。然後,你可以通知舊的主程式與其工作程式完美退出。
Controlling NGINX中,整個過程有更詳細的描述。
結論
Conclusion

NGINX的內部圖表高度概述了NGINX是如何運作的,但在這簡單的解釋背後是超過十年的創新與優化。這些創新與優化,使NGINX在多種硬體上表現出良好的效能,同時還具備現代web應用所需要的安全性和可靠性。
如果你想閱讀更多關於NGINX優化的資料,可以看看這些很棒的資源:
NGINX安裝和效能調優(webinar; slidesat Speaker Deck)
NGINX的效能調優
開源應用架構——NGINX
NGINX1.9.1套接字切分(Socket Sharding)(使用SO_REUSEPORT套接字選項)
回覆

相關文章