深入 Nginx 之架構篇

Jeffrey陳發表於2019-02-24

前言

最近在讀 Nginx 相關的書籍,做一下讀書筆記。

Nginx 作為業界知名的高效能伺服器,被廣泛的應用。它的高效能正是由於其優秀的架構設計,其架構主要包括這幾點:模組化設計、事件驅動架構、請求的多階段非同步處理、管理程式與多工作程式設計、記憶體池的設計,以下內容依次進行說明。

模組化設計

高度模組化的設計是 Nginx 的架構基礎。在 Nginx 中,除了少量的核心程式碼,其他一切皆為模組。

所有模組間是分層次、分類別的,Nginx 官方共有五大型別的模組:核心模組、配置模組、事件模組、HTTP 模組、mail 模組。它們之間的關係如下:

深入 Nginx 之架構篇

在這 5 種模組中,配置模組和核心模組是與 Nginx 框架密切相關的。而事件模組則是 HTTP 模組和 mail 模組的基礎。HTTP 模組和 mail 模組的“地位”類似,它們都是更關注於應用層面。

事件驅動架構

事件驅動架構,簡單的說就是由一些事件發生源來產生事件,由事件收集器來收集、分發事件,然後由事件處理器來處理這些事件(事件處理器需要先在事件收集器裡註冊自己想處理的事件)。

對於 Nginx 伺服器而言,一般由網路卡、磁碟產生事件,Nginx 中的事件模組將負責事件的收集、分發操作;而所有的模組都可能是事件消費者,它們首先需要向事件模組註冊感興趣的事件型別,這樣,在有事件產生時,事件模組會把事件分發到相應的模組中進行處理。

對於傳統 web 伺服器(如 Apache)而言,採用的所謂事件驅動往往侷限在 TCP 連線建立、關閉事件上,一個連線建立以後,在其關閉之前的所有操作都不再是事件驅動,這時會退化成按順序執行每個操作的批處理模式,這樣每個請求在連線建立後都將始終佔用著系統資源,直到關閉才會釋放資源。這種請求佔用著伺服器資源等待處理的模式會造成伺服器資源極大的浪費。如下圖所示,傳統 web 伺服器往往把一個程式或執行緒作為時間消費者,當一個請求產生的事件被該程式處理時,直到這個請求處理結束時,程式資源都將被這一請求所佔用。比較典型的例子如 Apache 同步阻塞的多程式模式就是這樣的。

傳統 web 伺服器處理事件的簡單模型(矩形代表程式):

深入 Nginx 之架構篇

Nginx 採用事件驅動架構處理業務的方式與傳統的 web 伺服器是不同的。它不使用程式或者執行緒來作為事件消費者,所謂的事件消費者只能是某個模組。只有事件收集、分發器才有資格佔用程式資源,它們會在分發某個事件時呼叫事件消費模組使用當前佔用的程式資源,如下圖所示,該圖中列出了 5 個不同的事件,在事件收集、分發者程式的一次處理過程中,這 5 個事件按照順序被收集後,將開始使用當前程式分發事件,從而呼叫相應的事件消費者來處理事件。當然,這種分發、呼叫也是有序的。

Nginx 處理事件的簡單模型:

深入 Nginx 之架構篇

由上圖可以看出,處理請求事件時,Nginx 的事件消費者只是被事件分發者程式短期呼叫而已,這種設計使得網路效能、使用者感知的請求時延都得到了提升,每個使用者的請求所產生的事件會及時響應,整個伺服器的網路吞吐量都會由於事件的及時響應而增大。當然,這也帶來一定的要求,即每個事件消費者都不能有阻塞行為,否則將會由於長時間佔用事件分發者程式而導致其他事件得不到及時響應,Nginx 的非阻塞特性就是由於它的模組都是滿足這個要求的。

請求的多階段非同步處理

多階段非同步處理請求與事件驅動架構是密切相關的,也就是說,請求的多階段非同步處理只能基於事件驅動架構實現。多階段非同步處理就是把一個請求的處理過程按照事件的觸發方式劃分為多個階段,每個階段都可以由事件收集、分發器來觸發。

處理獲取靜態檔案的 HTTP 請求時切分的階段及各階段的觸發事件如下所示:

深入 Nginx 之架構篇

這個例子中,該請求大致分為 7 個階段,這些階段是可以重複發生的,因此,一個下載靜態資源請求可能會由於請求資料過大,網速不穩定等因素而被分解為成百上千個上圖所列出的階段。

非同步處理和多階段是相輔相成的,只有把請求分為多個階段,才有所謂的非同步處理。當一個時間被分發到事件消費者中進行處理時,事件消費者處理完這個事件只相當於處理完 1 個請求的階段。什麼時候可以處理下一個階段呢?這隻能等待核心的通知,即當下一次事件出現時,epoll 等事件分發器將會獲取到通知,然後去呼叫事件消費者進行處理。

管理程式、多工作程式設計

Nginx 在啟動後,會有一個 master 程式和多個 worker 程式。master 程式主要用來管理worker 程式,包括接收來自外界的訊號,向各 worker 程式傳送訊號,監控 worker 程式的執行狀態以及啟動 worker 程式。 worker 程式是用來處理來自客戶端的請求事件。多個 worker 程式之間是對等的,它們同等競爭來自客戶端的請求,各程式互相獨立,一個請求只能在一個 worker 程式中處理。worker 程式的個數是可以設定的,一般會設定與機器 CPU 核數一致,這裡面的原因與事件處理模型有關。Nginx 的程式模型,可由下圖來表示:

深入 Nginx 之架構篇

在伺服器上檢視 Nginx 程式:

深入 Nginx 之架構篇

這種設計帶來以下優點:

1) 利用多核系統的併發處理能力

現代作業系統已經支援多核 CPU 架構,這使得多個程式可以分別佔用不同的 CPU 核心來工作。Nginx 中所有的 worker 工作程式都是完全平等的。這提高了網路效能、降低了請求的時延。

2) 負載均衡

多個 worker 工作程式通過程式間通訊來實現負載均衡,即一個請求到來時更容易被分配到負載較輕的 worker 工作程式中處理。這也在一定程度上提高了網路效能、降低了請求的時延。

3) 管理程式會負責監控工作程式的狀態,並負責管理其行為

管理程式不會佔用多少系統資源,它只是用來啟動、停止、監控或使用其他行為來控制工作程式。首先,這提高了系統的可靠性,當 worker 程式出現問題時,管理程式可以啟動新的工作程式來避免系統效能的下降。其次,管理程式支援 Nginx 服務執行中的程式升級、配置項修改等操作,這種設計使得動態可擴充套件性、動態定製性較容易實現。

記憶體池的設計

為了避免出現記憶體碎片,減少向作業系統申請記憶體的次數、降低各個模組的開發複雜度,Nginx 設計了簡單的記憶體池,它的作用主要是把多次向系統申請記憶體的操作整合成一次,這大大減少了 CPU 資源的消耗,同時減少了記憶體碎片。

因此,通常每一個請求都有一個簡易的獨立記憶體池(如每個 TCP 連線都分配了一個記憶體池),而在請求結束時則會銷燬整個記憶體池,把曾經分配的記憶體一次性歸還給作業系統。這種設計大大提高了模組開發的簡單些,因為在模組申請記憶體後不用關心它的釋放問題;而且因為分配記憶體次數的減少使得請求執行的時延得到了降低。同時,通過減少記憶體碎片,提高了記憶體的有效利用率和系統可處理的併發連線數,從而增強了網路效能。

相關文章