閱讀101 返回首頁    go 阿裏雲 go 技術社區[雲棲]


[譯]深入 NGINX: 為性能和擴展所做之設計

NGINX在web性能上的表現尤為出眾,這完全得益於其設計方式,許多web和應用服務器都是基於線程或進程這種簡單的架構,NGINX用了一種精妙的事件驅動架構,在現代的硬件上,它可以處理成千上萬的並發連接。

Inside NGINX中的信息圖對高級別的進程架構和NGINX如何在單個進程中處理多個連接進行了深入探討。本文更進一步地闡述了NGINX的所有工作原理。

背景——NGINX進程模型

要更好的理解這個設計,需要熟悉NGINX的運行過程。NGINX有一個主進程(該進程執行一些特權操作,例如讀取配置以及綁定端口)以及若幹worker進程和helper進程。

在這個4核服務器上,NGINX主進程創建了4個worker進程以及一對用來管理磁盤內容緩存的緩存helper進程。

架構為什麼重要?

任何Unix應用的基礎都是線程或進程。(從Linux操作係統的角度看,線程和進程幾乎是一樣的;最大的區別是內存共享的度。)一個線程或進程是一組自包含的指令,這些指令可由操作係統調度到某個CPU核心上運行。大部分複雜應用並行運行多個線程或進程一般有兩個原因:

  • 可以同時使用多個CPU核心。
  • 線程和進程讓並行操作變得簡單(例如,同時處理多個連接)。

進程和線程會消耗資源。每個進程或線程都會使用內存以及其他操作係統資源,他們都需要切換CPU(稱作上下文切換)。大部分現代的服務器都能同時處理幾百個小的、活動的線程或進程,但是,一旦內存耗盡或是遇到高I/O負載導致大量上下文切換時性能就會急劇下降。

常規的網絡應用設計都是為每個連接分配一個線程或進程。這種架構簡單且容易實現,但是,當應用需要同時處理成千上萬的連接時,擴展性就不好了。

NGINX是怎麼運行的?

NGINX用了一個可預測的進程模型,支持眾多硬件:

  • 主進程執行一些特權操作,比如讀取配置以及綁定端口,然後創建少數子進程(下麵的三種類型)。
  • 緩存加載進程在啟動時運行,用於將磁盤上的緩存加載到內存中,隨後退出。對這個進程的調度很保守,所以其資源需求比較低。
  • 緩存管理進程會周期性地運行,從磁盤緩存中刪除條目以保證緩存沒有超過配置的大小。
  • worker進程做了所有的工作!它們處理網絡連接,從磁盤讀取內容或往磁盤中寫入內容,以及與上遊服務器通信。

大部分場景中推薦的NGINX配置是 —— 每個CPU核心運行一個worker進程 —— 以充分利用硬件資源。在配置中加入worker_processes auto指令即可:

worker_processes auto;

當NGINX服務器活動時,隻有worker進程是處於繁忙狀態的。每個worker進程以非阻塞的方式處理多個連接,這減少了上下文切換的次數。

每個worker進程都是單線程的並且是獨立運行的,它們捕獲新的連接然後進行處理。進程之間的共享緩存數據、會話持久數據以及其它共享資源的通信通過共享內存實現。

深入理解NGINX Worker進程

每個worker進程都是用NGINX配置進行初始化的,並且由主進程提供了一組監聽套接字。

NGINX worker進程從等待監聽套接字上的事件開始(accept_mutex和內核套接字切分(kernel socket sharding))。事件由新進來的連接進行初始化。這些連接被分配給一個狀態機 —— HTTP狀態機是最常用的,但NGINX也為流(原始TCP)流量以及一些郵件協議(SMTP,IMAP和POP3)實現了狀態機。

狀態機本質上是一組指令,由它們告訴NGINX如何處理請求。大部分執行與NGINX相同方法的web服務器也用的類似的狀態機 —— 區別在於實現。

狀態機的調度

將狀態機想象成象棋規則。每個HTTP事務就是一盤象棋遊戲。棋盤的一側是web服務器 —— 一個可以快速做決定的象棋大師。另一側是遠程客戶端 —— 正在相對較慢的網絡中訪問站點或應用的web瀏覽器。

然而,遊戲的規則可能會非常複雜。比如,web服務器也許要與其它方(代理到上遊應用)進行交流或是要與認證服務器對話。web服務器中的第三方模塊甚至還可能擴展遊戲規則。

阻塞模式的狀態機

前麵我們提到,一個線程或進程是一組自包含的指令,這些指令可由操作係統調度到某個CPU核心上運行。大部分web服務器以及web應用使用的是每個連接分配一個進程或每個連接分配一個線程的模式來處理的。每個進程或線程都包含了從開始到結束需要執行的指令。在服務器運行進程期間,大部分時間都是“阻塞的” —— 等待客戶端完成其下一個動作。

  1. web服務器進程在監聽套接字上監聽新的連接(由客戶端初始化的新遊戲)。
  2. 當新遊戲準備好後,就開始遊戲,每個動作之後都會阻塞,等待客戶端的響應。
  3. 一旦遊戲結束,web服務器進程可能還要等等看是否這個客戶端需要發起一輪新的遊戲(這相當於keepalive連接)。如果連接被關閉(客戶端離開或超時),web服務器進程返回去監聽新的遊戲請求。

關鍵的一點在於每個活動的HTTP連接(每盤象棋遊戲)都需要一個專門的進程或線程(一個象棋大師)。這種架構在擴展第三方模塊(“新的規則”)時非常簡單方便。然而,存在一個巨大的失衡問題:相當輕量級的HTTP連接,本由一個文件描述符和少量的內存來表示,卻映射到了一個單獨的線程或進程這種非常重量級的操作係統對象。編程是便利了,但卻是個很大的浪費。

NGINX是真大師

也許你已經聽過simultaneous exhibition遊戲,一個象棋大師同時與幾十個對手對戰。

這就是NGINX worker進程下“象棋”的方式。每個worker進程(記住 —— 通常是每個CPU核心一個worker進程)都是一個大師,可以同時處理幾百盤(實際上是成千上萬)遊戲。

  1. worker進程等待監聽和連接套接字上的事件。
  2. 套接字上發生事件後,worker進程開始進行處理:

    • 監聽套接字上的事件意味著有個客戶端發起了一盤新的象棋遊戲。worker進程創建出一個新的連接套接字。
    • 連接套接字上的事件意味著客戶端開始有新的動作了。worker進程就迅速響應。

worker進程永遠不會因為網絡擁堵而阻塞來等待“對手”(客戶端)的響應。當處理完一個動作,worker進程立即去處理其他遊戲中等待處理的動作,或是迎接新玩家的到來。

為什麼這比阻塞的、多進程架構更快?

NGINX的可伸縮性非常好,每個worker進程可以支撐成千上萬個連接。每個新的連接會創建一個文件描述符以及消耗worker進程中少量的額外內存。每個連接的額外開銷極少。NGINX進程可以綁定到CPU。上下文切換是比較罕見的,隻有沒有任務要處理時才會發生。

在阻塞的、每個連接一個進程的方式下,每個連接都需要大量的額外資源與開銷,且上下文切換(從一個進程切換到另一個)非常頻繁。

更多細節解釋,看看這篇有關NGINX架構的文章

通過適當的係統調優,NGINX worker進程可以處理成千上萬的並發HTTP連接,能夠承受流量峰值(新遊戲蜂擁而至)還不會錯過一個請求。

配置更新與NGINX升級

NGINX這種使用少數worker進程的進程架構,可以非常高效的進行配置更新甚至是更新NGINX介質本身。

更新NGINX配置是個很簡單、輕量級且可靠的操作。通常隻是意味著去運行一下nginx –s reload命令,這個命令會去檢查磁盤上的配置,給主進程發送一個SIGHUP信號。

當主進程收到SIGHUP信號,會做兩件事:

  1. 重新加載配置,然後fork出一組新的worker進程。這些新的worker進程會立馬開始接受連接並處理(用的是新的配置)。
  2. 發信號讓舊的worker進程優雅退出。舊的worker進程停止接受新的連接。一旦每個當前的HTTP請求完成,worker進程將利索地結束連接(也就是,沒有lingering keeplive)。一旦所有連接都關閉了,worker進程就可以退出了。

這個配置加載進程會造成CPU和內存使用上的一個小峰值,但相比活動的連接帶來的資源負載,這是極其微小的。可以每秒重新加載配置多次(有許多NGINX用戶的確是這麼幹的)。多代NGINX worker進程都在等待連接關閉極少會造成問題,但即便有問題也會很快解決。

NGINX介質升級過程達到了高可用性的標準 —— 可以直接對線上運行的NGINX升級,而不會丟失任何連接,也不會有停機時間與服務中斷。

介質升級過程與重新加載配置的過程類似。一個新的NGINX主進程會與舊的主進程並行運行,它們共享監聽套接字。兩個進程都是活動的,並且各自對應的worker進程都還在處理請求。隨後可以發信號讓舊的主進程及其worker進程優雅退出。

整個過程在Controlling NGINX中有更詳細的描述。

總結

這篇深入NGINX信息圖從高層次概述了NGINX的功能,但是在這個簡單解釋的背後,是十多年的創新與優化,才使NGINX在保證安全和可靠性的同時,在眾多硬件上都能發揮出最佳性能。

如果想更多地了解NGINX的優化,下麵這些資源不錯:

最後更新:2017-05-22 16:01:52

  上一篇:go  戲(細)說Executor框架線程池任務執行全過程(下)
  下一篇:go  Mutex和內存可見性