Nginx:論高併發,在座各位都是渣渣

NGINX 在網絡應用中表現超群,在於其獨特的設計。許多網絡或應用服務器大都是基於線程或者進程的簡單框架,NGINX突出的地方就在於其成熟的事件驅動框架,它能應對現代硬件上成千上萬的併發連接。

NGINX 內部信息圖從進程框架的頂層開始,向下逐步揭示NGINX如何處理單個進程中的多個連接,並進一步探討其工作機制。

場景設置 — NGINX進程模型

Nginx:論高併發,在座各位都是渣渣

為了更好地理解這種設計模式,我們需要明白NGINX是如何運行的。NGINX擁有一個主線程,用來處理配置文件的讀取、端口的綁定等特權操作,以及一組工作進程、輔助進程。

Nginx:論高併發,在座各位都是渣渣


在這個四核服務器中,主線程創建了四個工作進程和一組緩存輔助進程(cache helper processes),後者用來管理硬盤緩存。

為什麼框架如此重要?

任何Unix應用的基礎是線程或者進程-對於Linux操作系統,線程和進程幾乎相同;最大的區別在於線程間是內存共享的。一個線程或者進程是一套指令集(self-contained set of instructions ),操作系統調度這些指令在單個CPU內核上運行。許多複雜應用並行地運行在多個線程或者進程,原因有二:

  • 應用可以同時使用計算機的多個CPU核
  • 線程和進程易於並行操作,比如同時處理多個連接

進程和線程消耗資源,比如對內存以及其它操作系統資源的佔用、內核切換(wapped on and off the cores)(本操作叫做一次上下文切換(context switch))。如今的服務器需要同時處理成千個小的、活躍線程或者進程,一旦內存耗盡、或者過高的讀寫負載,這些都會導致大規模的上下文切換,性能會嚴重退化。

通常的設計思路是,網絡應用為每個連接分派一個線程或者進程。這類框架簡單易於實現,不過在同時應對成千上萬個連接時難以擴展。

NGINX是如何運作的呢?

NGINX利用一個預測進程模型調度可用的硬件資源:

  • 主進程處理配置文件讀取、端口綁定等特權操作,以及創建一小組子進程(接下來三種類型的進程)
  • 啟動時緩存加載器進程加載硬盤中緩存到內存中,接著退出。對它的調度是保守的,所以資源開銷較低
  • 緩存管理進程定時運行,清理來自硬盤緩存的實體到指定的大小
  • 工作進程負責所有的工作,處理網絡連接、硬盤讀寫操作、以及上游服務器通信

NGINX推薦的配置是,一個工作進程對應一個CPU內核,確保硬件資源的有效利用,在配置文件中設置worker_processes auto:

worker_processes auto;

一旦NGINX服務起來,僅有工作進程在忙,每個工作進程採用非阻塞地方式處理多個連接,降低上下文切換的次數。

每個工作進程都是單線程且獨立運行,負責獲取新連接並進行處理。進程之間通過共享內存進行通信,諸如緩存數據,會話持續化數據(ession persistence data),以及其他共享資源。NGINX1.7.11及以後的版本,有一個可選的線程池,工作進程將阻塞操作丟給它們。更多細節,參看《Nginx 引入線程池,提升 9 倍性能》(http://blog.jobbole.com/87988/)。對於NGINX Plus用戶,這些新特性會在今年的發佈版7中出現。

NGINX內部工作進程

Nginx:論高併發,在座各位都是渣渣

每個NGINX工作進程由配置文件對其進行初始化,主進程為其提供一組監聽socket。

工作進程起始於socket監聽事件(accept_mutex 和 kernel socket sharding),事件由新的連接進行初始化,接著這些連接被派發給某個狀態機—HTTP狀態機是其中最常用的一種,不過NGINX也實現了基於流的狀態機、基於通信協議的狀態機(SMTP, IMAP, and POP3)。

Nginx:論高併發,在座各位都是渣渣

狀態機是一組重要的指令集,它會告訴NGINX怎樣處理每個請求。許多網絡服務器擁有NGINX的狀態機一樣的功能—區別就在於它們的實現不同。

調度狀態機

狀態機就像下象棋,單個HTTP事務如同一盤棋。棋盤的一端是網絡服務器—就像大師級棋手非常快地做出決定,另一端為遠程客戶端—網絡瀏覽器通過相對較慢的網絡訪問某個站點或應用。

不過遊戲規則可能非常複雜,比如網絡服務可能需要和第三方、或者某個認證服務器通信,甚至服務器中的第三方模塊來擴展遊戲規則。

阻塞狀態機

回到前面的描述,進程或者線程作為一套指令集,操作系統調度其運行在某個CPU內核上。大多數網絡服務器和網絡應用按照一個進程處理一個連接,或者一個線程處理一個連接的模型來玩象棋遊戲;每個包含指令的進程或者線程參與遊戲的整個過程。在這期間,運行在服務器上進程大多數時間被阻塞掉了,即等待某個客戶端去完成下一步棋。

Nginx:論高併發,在座各位都是渣渣

  1. 網絡服務器進程監聽socket上的新連接,此遊戲新連接由客戶端發起。
  2. 一旦獲得新遊戲,進入遊戲環節,每一次移動都需等待客戶端響應,進程就被阻塞了。
  3. 一旦遊戲結束,網絡服務器進程就會查看客戶端是否想再來一局(對應某個存活的連接)。一旦連接關閉(客戶端離開或者超時),網絡服務器進程就會返回監聽新的遊戲。

記住每一個活躍的HTTP連接即每一局象棋遊戲,需要象棋大師一般的特定進程或者線程參與其中。這個架構簡單易於擴展第三方模型即新的規則。然而,這裡存在一個極不平衡的邏輯,對於相關輕量級的HTTP連接,由單個文件描述符和少量的內存表示,此連接會映射到某個線程或進程上,而線程或者進程是一個重量級的操作系統對象。儘管編程時很方便,但浪費卻是巨大的。

NGINX是一個真正的大師

或許你聽說過同時展示遊戲,一個象棋大師同時對陣十二個棋手。

Nginx:論高併發,在座各位都是渣渣


NGINX工作進程也是這麼玩”象棋”的,每個工作進程-一個CPU內核上的工作者-即是一個可以同時應對成千上萬遊戲的大師。

Nginx:論高併發,在座各位都是渣渣

  1. 工作進程從已連接並開始監聽的套接字(socket)那裡獲取事件;
  2. 一旦socket接收到事件,工作進程會立即處理此事件:
  • socket上的某個監聽事件即客戶端開啟一個新的象棋遊戲,而工作進程創建一個新的socket連接。
  • socket連接上的某個事件即客戶端走了一步棋,工作線程做出了恰當地響應。

工作進程從來不會阻塞在網絡傳輸上等待它的對手(客戶端)回覆應答。每走完一步棋後,工作進程會迅速處理其它等待的象棋遊戲,或者歡迎新的遊戲玩家進入。

為何比阻塞、多進程框架快呢?

NGINX良好的擴展性在於其支持一個工作線程處理成千上萬個連接。每個新連接創建文件描述符,僅消耗工作進程很少一部分額外內存,額外的開銷很小。進程能夠一直綁定CPU(pinned to CPUs),這樣上下文切換相對沒有那麼頻繁,只有沒工作時才會發生。

譯者注:cpu綁定是指綁定一個或者多個進程到一個或者多個處理器上.

使用阻塞方式,即一個連接對應一個進程,每個連接需要大量的額外資源以及開銷,上下文切換非常頻繁。

只要恰當的系統調優,NGINX每個工作進程可以處理成千上萬個併發HTTP連接,毫無差錯地應對網絡高峰,即同時可以玩更多的象棋遊戲。

更新配置文件升級NGINX

進程框架擁有少量工作進程,有利配置文件甚至二進制文件更新。

Nginx:論高併發,在座各位都是渣渣

更新NGINX配置是一個簡單、輕量級的可靠操作。即只要運行nginx -s reload命令,就會檢查磁盤上的配置文件,並給主進程發送一個SIGHUB信號。

一旦主進程接受到一個SIGHUB,它會做兩件事:

  1. 重載配置文件、創建一組新的工作進程,新創建的工作進程立即接受連接、處理網絡通信( 採用新的配置環境)。
  2. 通知舊的工作進程優雅地推出,這些工作進程停止接受新連接。一旦當前處理的HTTP請求結束,工作進程會關閉連接。一旦所有連接關閉,工作進程就會退出。

重載進程會引起一個小的CPU和內存高峰,不過從活躍連接處加載的資源相比,開銷微乎其微。每一秒可以多次重載配置文件。產生諸多等待連接關閉的NGINX工作進程一般很少出問題,不過就算是有問題也可以迅速解決。

NGINX二進文件升級獲得極佳的高可用性-你可以在線升級文件,而且不會丟失任何連接、服務也不會停機或中斷。

譯者注: on the fly 程序在運行時,工作就可以完成。

Nginx:論高併發,在座各位都是渣渣

二進制文件升級進程方式類似優雅的配置文件重載;新的NGINX主進程和原有的主進程並行,分享監聽socket。兩個進程都處於活躍狀態,處理它們各自的網絡通信。你可以通知原有的主進程以及它的工作進程優雅地退出。

最後結語

NGINX內部信息圖展示了NGINX的高標準功能全景圖,簡單解釋的背後是十多年來不斷創新優化,得益於此NGINX被廣泛應用於各種硬件平臺,並且取得了最優異的性能表現。即便是在現代,網絡應用需要對安全和可靠性作出維護,NGINX也表現不凡。

歡迎加入我們,一起探討架構,交流源碼。加入方式:關注我 轉發後 後臺私信 架構


分享到:


相關文章: