[譯] 開源專案之 Nginx

razertory發表於2018-10-16

nginx(讀作 "engine x")是一位名叫 Igor Sysoev 的俄羅斯軟體工程師開發的。自 2004 年釋出以來,nginx 就一直專注於實現高效能,高併發和低記憶體佔用。nginx 的額外功能,比如:負載均衡、快取和流量控制以及高效整合在 Web 服務上的能力,使得它成為了當今網站架構的必選。如今,nginx 已經成為網際網路中第二受歡迎的開源 Web 伺服器。

14.1 高併發為何如此重要?

如今,網際網路早已無處不在,我們已經很難想象十年前沒有網際網路的樣子。現在的網際網路發生了翻天覆地的變化,從基於 NSCA 的可以點選 HTML 頁面和基於 Apache 的 Web 服務,到如今能夠實現超過 20 億人實時的溝通。隨著 PC、手機和平板的的蔓延,網際網路已經將全球經濟數字化。面向資訊和娛樂的線上服務變得更加優質。線上商業活動的安全方面也發生了明顯變化。因此,網站也比以前更加的複雜並且需要大量的工程投入來確保魯棒性和可擴充套件性。

併發性成為了網站架構設計的最大挑戰之一。自從 web 服務開始的時候,併發性的等級就在持續上升。對於一個熱門網站來說,支援幾百甚至是幾百萬使用者同時訪問來說也不是什麼稀罕事情。20 年前,產生併發的原因主要還是客戶端的 ADSL 或者撥號(dial-up)連線。如今,併發的產生來源於手機端和以及新型的應用架構,這些架構主要可以支援長連線來提供新聞、資訊流釋出和朋友間的 feed 流等等。另一方面,導致高併發還由於現代瀏覽器的工作發生變化,通常是為了提高網頁載入速度同時開啟 4 到 6 個連線。

為了表述清楚緩慢這種問題,設想一下,一個基於 Apache 的,可以提供 100KB 大小帶有文字或者圖片的簡單 web 伺服器。生成或者重新產生這個網頁只需要極少不到一秒的時間。但是在頻寬只有 80kps 的情況下(下載速度 10kb/s),傳輸資料到客戶端卻會花掉 10s。本質上,伺服器產生 100kb 資料的速度是相對較快的,隨後在傳輸資料到客戶端直至釋放連線的過程卻是相對較慢的。現在設想,你同時有 1,000 個獨立的客戶端連到你的伺服器並且請求同樣的內容。如果對於每個獨立的連線,都會佔用額外的 1MB 記憶體,那麼對於 1,000 個連線來說就對導致多佔用 1000 MB(1G)的記憶體,而這些僅僅是為了給 1000 個客戶端提供 100kb 的內容。實際上,一個典型的 Apache 伺服器通常會為了一個連線佔用超過 1MB 的記憶體,遺憾的是幾十 k 的頻寬足夠讓手機之間高效通訊。儘管從某種程度而言,傳送資料給客戶端是慢的,提高作業系統核心的 socket 緩衝大小是可以的,這個不是一個通常的解決方法,並且會有不良影響。

在持久連線中,處理併發會做起來總比說起來有更多的問題,因為要在新建 HTTP 連線的時候避免延遲,讓客戶端保持連線並且確保對於每個連線服務端都能夠保證有足夠記憶體可供使用。

因此,為了能夠處理因為使用者量增長產生高併發由此帶來的負載上升,網站的就必須基於通過一定數目的高效模組來架設。同時,在從獲得客戶端連線請求,到處理完請求期間,像硬體(CPU,memory,disk),網路容量以及資料儲存也是同樣重要的。因此,web 伺服器需要能在同時請求數和每秒請求頻率這兩方面都擁有擴充套件性。

Apache 不合適嗎?

Apache,開始於 1990s,如今依舊統治著網際網路。最初它的架構滿足於當時的作業系統和硬體,同時也滿足於當時的只有一個獨立的物理機執行一個 Apache 伺服器的網際網路狀態。在 2000 年始,一個獨立的伺服器難以滿足增長起來的 Web 服務的情況越來越明顯。儘管 Apache 提供了一個可靠的基金會用於未來發展,然而,它這種為了每個新連線複製自身的架構,已經不再適用於非線性的網站擴張。最終,Apache 成為了一個有著許多不同特性,第三方擴充套件,和一些普遍用於 web 應用開發的功能的 web 伺服器。然而,沒有什麼東西是十全十美的,Apache 有者豐富功能的同時,對於每個連線產生的 CPU 和記憶體消耗使得它不能很好的擴充套件。

因此,當伺服器的硬體、作業系統和網路條件成為了網站增長的瓶頸時,全世界的 web 工程師開始尋找一種更加高效的方法。大約十年前,一位名叫 Daniel Kegel 的傑出工程師宣稱:"是時候讓 web 服務能夠支援 10k 併發了。"同時他還預測了我們現在會叫網際網路雲服務。c10k 問題一產生,就引來了許許多多的解決方案用以優化實時的高併發。nginx 成為了其中最出色的解決方案之一。

為了解決 C10k 問題中的 10,000 個實時的連線,nginx 用了一種與眾不同的架構,這種架構會更適合在同時處理大量的連線和一秒鐘內完成多次請求環境中,問題規模的增長是非線性的。nginx 是事件驅動的(event-based,所以它不會用 Apache 的那種為每一個 web 請求都申請一個程式或者執行緒。結果便是,即使負載升高,記憶體和 CPU 都還是處於掌控之中。nginx 目前可以在一臺普通的機器上,同時處理上萬的併發。

nginx 的第一個版本主要是和 Apache 伺服器一起部署,用來單獨處理原本是 Apache 處理的 HTML, CSS, JavaScript 和圖片這樣的靜態資源。在隨後的迭代中,nginx 支援像 FastCGI, ,uswgi 或者 SCGI 協議整合到應用當中部署,並且可以利用像 memcached 這樣的分散式快取系統。同時像反向代理,負載均衡這樣的特性也隨之加上。這些額外的特點讓 nginx 成為了構建可擴充套件性 web 服務的高效的基礎元件的工具之一。

2012 年二月,Apache 2.4.x 分支釋出。儘管,這個最新版本的 Apache 增加了多核處理器支援模組和用於提升可擴充套件性和併發的模組,然而它的效能,併發能力,以及資源利用能力與純事件驅動的 web 伺服器比,依舊難以望其項背。 很樂意看到新版的 Apache 伺服器有著更好的可擴充套件性,儘管這樣可以減少自身的瓶頸,然而像典型的 nginx-plus-Apache 配置依舊會被使用。

使用 nginx 會有更多的優勢嗎?

能夠高效能地處理高併發一直是部署了 nginx 之後獲得的最主要的好處。然而,還有一些更有趣的東西。

在過去幾年中,網站架構就一直在擁抱解耦並從 web 伺服器中拆分出一些基礎元件。然而,那些原本存在於 LAMP-based 的網站中的基礎元件,在 LEMP-based(E 代表著 Nginx 的讀音) 的網站中,卻能讓 web 伺服器成為基礎元件以及用一種不同的方式去整合相同的或者改進了的應用和資料庫工具。

nginx 非常適合做這個,因為它可以方便提供一個併發支援,延遲超時處理,SSL 支援,靜態檔案支援,壓縮和快取,甚至是 http 流媒體的高效的層級,而這些功能原本處於應用層。nginx 也可以直接整合一些像 Redis/memcached 這樣的 NoSQL 用以優化大使用者量場景。

當近代的開發語言和工具流行起來的時候,越來越多的公司正在改變他們的開發和部署方式。nginx 成為了改變過程中最重要的部分,同時,nginx 讓很多公司在有限的預算中,快速地啟動開發他們的服務。

nginx 是從 2002 年開始開發。到 2004 年,它以 two-clause BSD license 釋出。隨後,nginx 使用者量開始增高,修改建議,bug 報告,觀察報告等都在社群中不斷完善 ngix。

nginx 最初的原始碼是用 C 完成的。nginx 已經可以部署在許多架構和作業系統中,比如 Linux, FreeBSD, Solaris, Mac OS X, AIX and Microsoft Windows。nginx 擁有自己的庫並且並沒有大量使用 C 標準庫,一些像 zlib, PCRE and OpenSSL 這一類的庫因為有證照衝突而沒有被採用。

在 Windows 上部署 nginx 更像是一個實現 nginx 的理論證明而不是一個功能完善的專案。由於核心限制,nginx 的一些功能特性並不能發揮出來。在 windows 上的 nginx 併發能力、效能會更低,也沒有快取和頻寬策略。將來 windows 上的 nginx 版本會繼續完善。

14.2. nginx 架構總覽

傳統的解決併發的方式是每個單獨的請求一個程式或者執行緒,並且網路和 io 操作都是阻塞式的。在傳統的應用當中,這種做法會由於 CPU 和記憶體開銷導致低效。開啟一個獨立的程式或者執行緒會需要預載入一個新的執行時環境和上下文。這些東西也會佔用一些額外的 CPU 時間,執行緒頻繁輪換導致的上下文切換帶來的開銷最終導致了低效能。這些問題在一些舊的 web 服務架構,比如 Apache 中得到了證實。這是在提供豐富普遍特性與優化伺服器開銷之前的一種權衡。

從最早開始,nginx 就被設定為在網站使用者動態增長期間,用來提高網站效能和伺服器資源利用率的工具,以至於它擁有一種與眾不同的模型。這是受一些作業系統的事件驅動概念啟發。這也產生了 nginx 的核心架構:模組化,事件驅動,非同步,單執行緒,非阻塞。

nginx 大量採用多路複用(multiplex)和事件通知,並對每個 nginx 程式分配了特定的任務。連線被有限個數單執行緒的 worker 程式高效輪詢(run-loop)處理。 每個 worker 都可以同時處理數千個併發連線和每秒請求。

Code Structure 程式碼結構

worker 程式碼包含了核心和功能模組。nginx 核心負責維護一個緊湊的輪詢,並在處理請求的每個階段都執行模組中對應的部分。模組構成了大部分表示層和應用層功能。模組從網路和儲存介質中進行資料的讀寫,傳輸內容,過濾出站內容,執行服務端的動作和當代理功能被開啟的時候傳遞請求到被代理的(upstream)伺服器。

nginx 模組化的架構可以讓開發者在不修改核心程式碼的情況下加入一些自定也的擴充套件。nginx 模組稍微有點不同,比如核心模組、事件模組、階段處理器、協議、變數處理器、filter,upstream 和負載均衡。目前,nginx 不再支援動態載入模組。模組在 nginx build 階段就會被編譯。然而,在將來 nginx 會在主版本上提供 loadable 模組和 ABI。更多關於不同模組的資訊詳見 Section 14.4.

在處理一些關於網路接收,處理和管理以及內容檢索的時候,nginx 使用了事件通知(event notification)機制以及一些作業系統( Linux, Solaris and BSD-based)的磁碟 IO 優化,比如:kqueue, epoll, and event ports。目的是為作業系統提供儘可能多的提示,以便為入站和出站流量、磁碟操作、socket 讀寫、超時等獲取及時的非同步反饋。針對 nginx 執行的每個 unix-like 的作業系統,對多路複用和高階 I/O 操作使用不同的方法進行了大量優化。

更多 nginx 架構高階概述詳見 Figure 14.1.

[譯] 開源專案之 Nginx

Figure 14.1: Diagram of nginx's architecture

Workers 的模型

正如之前提到的,nginx 並不為每個連線開一個程式或者執行緒。相反,worker 程式為每個新連線都採用一個共用的監聽 socket 並在輪詢中高效處理著數千個連線。對於 nginx 的 worker,沒有采用一些特別的連線機制,都是由作業系統核心來完成的。一旦啟動,一些監聽 socket 就會被建立。worker 就會持續地接受連線,處理 http 請求和從對應的這些 socket 中讀寫資料。

輪詢是 nginx 程式碼中最複雜的部分。它包括了綜合(comprehensive)的內部呼叫和依賴大量的非同步任務處理思想。非同步操作通過模組化,事件通知,函式回撥和計時器實現。總體上,關鍵在於儘可能的非阻塞。唯一讓 nginx worker 阻塞的只有磁碟不足的情況。

因為 nginx 不會為每個連線新開程式或者執行緒,記憶體佔用在很多場景下都不會高。nginx 節約了 cpu 佔用也是因為沒有程式執行緒的建立和銷燬。nginx 要做的就是檢查網路和儲存,建立新連線,把新連線加入到輪詢,並且在完成之前都非同步處理。nginx 謹慎採用了一些系統呼叫比如資源池化和記憶體分配,以至於在極端的情況下也不會有很高的 CPU 佔用。

由於 nginx 處理連線就開了幾個 worker,在多核情況下可以很好的擴充套件。大致就是一個核心一個 worker,這樣每個 worker 充分利用 cpu 核心,避免了執行緒切換和鎖等待。不會產生資源不足並且每個單執行緒的 worker 程式中都存在資源管理策略。這種模型允許在不同儲存裝置之間有更好的擴充套件性,促進磁碟利用並且避免了磁碟 IO 阻塞。總的來說,伺服器資源在多個 worker 工作的情況下被更高效使利用了。

對於某些磁碟使用和 CPU 負載模式,應該調整 nginx worker 的數量。這些規則在這裡有點基礎,系統管理員應該基於他們的工作負載嘗試一些配置。一般建議如下:如果負載模式是 CPU 密集型的—例如,處理大量 TCP/IP、執行 SSL 或壓縮,nginx worker 的數量應該與 CPU 核心的數量相匹配;如果負載主要是磁碟 I/O 限制。例如,從儲存中提供不同的內容,或者大量的反向代理,workers 的數量可能是核心數量的 1.5 到 2 倍。有些工程師根據單個儲存單元(磁碟分割槽)的數量來選擇 workers 的數量,這種方法的效率取決於磁碟儲存的型別和配置。

nginx 開發人員在即將釋出的版本中要解決的一個主要問題是如何避免磁碟 I/O 上的大部分阻塞。目前,如果沒有足夠的儲存效能來服務於由特定的 worker 生成的磁碟操作,那麼 worker 仍然可能阻塞從磁碟讀取 / 寫入。存在許多機制和配置檔案指令來減輕此類磁碟 I/O 阻塞場景。最值得注意的是,sendfile 和 AIO 等選項的組合通常會為磁碟效能帶來很大的空間。應該根據資料儲存、可用的記憶體大小和底層儲存體系結構來計劃 nginx 的安裝。

現有 worker 模型的另一個問題是關於內嵌指令碼支援的限制。首先,使用標準的 nginx 發行版,只支援嵌入 Perl 指令碼。對此有一個簡單的解釋:關鍵問題是內嵌指令碼可能阻止任何操作或意外退出。這兩種型別的行為都會立即導致 worker 被掛起,同時影響數千個連線。需要更多的工作來讓 nginx 的嵌入式指令碼更簡單、更可靠、適合更多的應用程式。

nginx 程式角色

nginx 在記憶體中執行幾個程式;有一個 master 程式和幾個 worker 程式。還有一些特殊用途的程式,特別是快取載入器和快取管理器。版本 1.x 中的所有程式都是單執行緒的。所有程式主要使用共享記憶體機制進行程式間通訊。主程式作為 root 使用者執行。快取載入器、快取管理器和 worker 作為非特權使用者執行。

master 程式主要有以下任務

  • 讀取並驗證配置檔案
  • 建立、繫結和關閉 socket
  • 啟動,終止和維護配置好了個數的 worker 程式
  • 不中斷情況下重新載入配置
  • 控制熱更新(從二進位制檔案啟動和必要情況下回滾)
  • 開啟日誌檔案
  • 編譯內嵌的 perl 指令碼

worker 程式接受和處理來自客戶機的連線,提供反向代理和過濾功能,並完成 nginx 能夠做的幾乎所有其他事情。關於監視 nginx 例項的狀況,系統管理員應該關注 worker 程式,因為他們是反映 web 伺服器實際日常操作的過程。

快取載入器程式負責檢查磁碟上的快取項,並使用快取後設資料填充 nginx 的記憶體資料庫。實際上,快取載入器準備 nginx 例項來處理已經儲存在磁碟上的檔案,這些檔案位於一個特別分配的目錄結構中。它遍歷目錄,檢查快取內容後設資料,更新共享記憶體中的相關條目,然後在一切都乾淨且可以使用時退出。

快取管理器主要負責快取過期和失效。在正常的 nginx 操作過程中,它保持在記憶體中,在失敗的情況下由主程式重新啟動。

nginx 快取簡覽

nginx 中的快取是以檔案系統上分層資料儲存的形式實現的。快取 key 是可配置的,可以使用不同的特定於請求的引數來控制進入快取的內容。快取 key 和快取後設資料儲存在共享記憶體段中,快取載入器、快取管理器和 worker 程式可以訪問共享記憶體段。目前,除了作業系統的虛擬檔案系統機制產生的優化之外,沒有任何檔案的是快取在記憶體當中。每個快取的讀取都放在檔案系統上的不同檔案中。層次結構(級別和命名細節)是通過 nginx 配置指令控制的。當將響應寫入快取目錄結構時,路徑和檔案的名稱來自代理 URL 的 MD5 值。

在快取中放置內容的過程如下:當 nginx 從 upstream 伺服器讀取響應時,內容首先被寫入快取目錄結構之外的臨時檔案中。當 nginx 完成對請求的處理後,它會重新命名臨時檔案並將其移動到快取目錄中。如果用於代理的臨時檔案目錄位於另一個檔案系統上,則會複製該檔案,因此建議將臨時目錄和快取目錄儲存在同一個檔案系統上。當需要顯式清除快取目錄結構中的檔案時,從快取目錄結構中刪除檔案也是相當安全的。nginx 有第三方的擴充套件,可以遠端控制快取的內容,並且計劃了更多的工作來讓這個功能可以整合到主發行版中。

14.3. nginx 配置

nginx 的配置系統受到了 Igor Sysoev 使用 Apache 的經驗的啟發。他的主要觀點是,對於 web 伺服器來說,可伸縮的配置系統是必不可少的。當使用大量虛擬伺服器、目錄、位置和資料集維護大型複雜配置時,會遇到擴充套件問題。在一個相對較大的 web 設定中,如果在應用程式和系統工程師都沒有正確地完成,那麼它可能是一個噩夢。

因此,nginx 配置的目的是簡化日常操作,並提供進一步擴充套件 web 伺服器配置的簡單方法。

nginx 的配置儲存在許多純文字檔案中,這些檔案通常位於 /usr/local/etc/nginx/etc/nginx。主配置檔案通常稱為 nginx.conf。為了保持它的整潔,部分配置可以放在單獨的檔案中,這些檔案可以自動包含在主檔案中。然而,這裡應該注意到 nginx 目前不支援 apache 風格的分散式配置(即”。htaccess 檔案)。所有與 nginx web 伺服器行為相關的配置都應該駐留在一組集中的配置檔案中。

配置檔案最初由 master 程式讀取和驗證。當 worker 程式從 master 程式 fork 時,worker 程式可以使用編譯後的只讀形式 nginx 配置。配置結構由通常的虛擬記憶體管理機制自動共享。

nginx 配置有幾個不同的內容:main, http, server, upstream, location (同時 mail 相當於郵件服務代理)。配置檔案內容不重疊。例如,在 main 中不存在 location。此外,為了避免不必要的歧義,沒有任何類似於“全域性 web 伺服器”配置的東西。nginx 的配置是乾淨和合乎邏輯的,允許使用者維護包含數千個指令的複雜配置檔案。在一次私人談話中,Sysoev 說,“全域性伺服器配置中的 location、directory 和其他塊是我在 Apache 中不喜歡的特性,所以這就是為什麼它們從未在 nginx 中實現的原因。”

配置檔案語法、格式和定義遵循所謂的 c 風格約定。這種生成配置檔案的特殊方法已經被各種開源和商業軟體應用程式所使用。從設計上講,c 風格的配置非常適合巢狀描述,具有邏輯性,易於建立、閱讀和維護,並受到許多工程師的喜愛。nginx 的 c 風格配置也很容易自動化。

雖然 nginx 的一些指令類似於 Apache 配置的某些部分,但是設定一個 nginx 例項卻是完全不同的體驗。例如,nginx 支援重寫規則,儘管需要管理員手動修改遺留的 Apache 重寫配置以匹配 nginx 風格。重寫引擎的實現也不同。

一般來說,nginx 設定還支援一些原始機制,作為精簡 web 伺服器配置的一部分非常有用。簡單地提到變數和try_files指令是有意義的,這些指令對於 nginx 來說是唯一的。nginx 變數被開發出來是為了提供一個更強大的機制來控制 web 伺服器的執行時配置。變數經過優化以快速解析,並在內部預編譯為索引。根據需要進行解析,通常,變數的值只計算一次,並在特定請求的生命週期內快取。變數可以與不同的配置指令一起使用,為描述條件請求處理行為提供了額外的靈活性。

“try_files”指令最初旨在以更合適的方式逐步替換條件“if”配置語句,它的設計目的是快速有效地嘗試 / 匹配不同的 uri 到內容的對映。總的來說,try_files 指令工作得很好,並且非常高效和有用。更多詳情推薦讀者去 try_files directive

14.4. nginx 內部

如前所述,nginx 程式碼庫由核心和許多模組組成。 nginx 的核心是負責提供 Web 伺服器,Web 和郵件反向代理功能的基礎;它支援使用底層網路協議,構建必要的執行時環境,並確保不同模組之間的無縫互動。但是,大多數協議和特定的應用程都是由 nginx 功能模組完成的,而不是核心模組。

在內部,nginx 通過由模組組成的的管道或模組鏈來處理連線。換句話說,對於每個操作,都有一個正在進行相關工作的模組;例如,壓縮,修改內容,執行伺服器端,通過 FastCGI 或 uwsgi 協議與 upstream 應用伺服器通訊,或與 memcached 通訊。

有幾個 nginx 模組位於核心和真正的“功能”模組之間。這些模組是httpmail。這兩個模組在核心和較低階別元件之間提供了額外的抽象級別。在這些模組中,實現了與諸如 HTTP,SMTP 或 IMAP 的相應應用層協議相關聯的事件序列的處理。結合 nginx 核心,這些上層模組負責維護對各個功能模組的正確呼叫順序。雖然 HTTP 協議目前是作為http模組的一部分實現的,但由於需要支援 SPDY 等其他協議,因此計劃將來將其分離為功能模組。更多 SPDY 協議詳見 SPDY: An experimental protocol for a faster web

功能模組可分為事件模組,階段處理程式,輸出 filter,變數處理程式,協議,上游和負載平衡器。大多數這些模組補充了 nginx 的 HTTP 功能,但事件模組和協議也用於mail。事件模組提供特定的 OS 依賴事件通知機制,如kqueueepoll。 nginx 使用的事件模組取決於作業系統功能和構建配置。協議模組允許 nginx 通過 HTTPS,TLS / SSL,SMTP,POP3 和 IMAP 進行通訊。

典型的 HTTP 請求處理週期如下所示。

  1. 客戶端傳送 http 請求。
  2. nginx core 依據配置檔案中的 location 選擇合適的階段處理器。
  3. 如果配置生效,負載均衡器就會選擇一個 upstream 伺服器代理。
  4. 階段處理器執行任務,並把緩衝區的內容傳遞給第一個 filter。
  5. 第一個 filter 將內容傳遞給第二個 filter
  6. 第二個 filter 傳遞給第三個(迭代執行)
  7. 將最後的 response 傳送給客戶端。

nginx 模組呼叫是非常可定製的。它使用指向可執行函式的指標來執行一系列回撥。然而,這樣做的缺點是它可能給想要編寫自己的模組的程式設計師帶來很大的負擔,因為他們必須準確定義模組應該如何以及何時執行。 nginx API 和開發人員的文件都在不斷改進,並且可以更多地用來緩解這個問題。

下面這些列子是可以新增模組的位置:

  • 在讀和處理配置檔案之前
  • 在每個伺服器出現以及配置檔案指向的地方
  • 當 主配置 被初始化的時候
  • 當伺服器被初始化的時候
  • 當 server configuration 被合併到 主配置的時候
  • 當 location configuration 初始化或者合併到 parent server configuraton 的時候
  • 當 master 程式啟動或者存在的時候
  • 當一個新的 worker 程式啟動或者存在的時候
  • 當處理一個請求的時候
  • 當過濾請求 header 和請求 body 的時候
  • 當請求轉發到 upstream 伺服器的時候
  • 伺服器中的響應的時候
  • 當完成與一個 upstream 伺服器的互動的時候

在 worker 程式中,導致生成響應的執行迴圈的 action 序列如下所示:

  1. 啟動 ngx_worker_process_cycle().
  2. 使用作業系統特定的機制來處理事件(such as epoll or kqueue
  3. 接收事件並且分發給相關的 action
  4. 處理 / 代理請求 header 和 body
  5. 產生響應內容 (header, body) 並傳遞給客戶端
  6. 結束請求
  7. 重啟 timers,events

輪詢本身(步驟 5 和 6)確保增量生成響應並將其流式傳輸到客戶端。

處理 HTTP 請求的更詳細過程可能如下所示

  1. 初始化請求處理
  2. 處理 header
  3. 處理 body
  4. 呼叫相關的 nginx 處理器
  5. 執行每個處理階段

這將我們帶到了每個階段。當 nginx 處理 HTTP 請求時,它會將其傳遞給許多處理階段。在每個階段都有處理程式可以呼叫。通常,階段處理程式處理請求並生成相關輸出。階段處理程式被附加到配置檔案中定義的位置。

階段處理程式通常執行以下四項操作:獲取位置配置,生成適當的響應,傳送 header 以及傳送 body。處理程式有一個引數:描述請求的特定結構。請求結構有很多關於客戶端請求的有用資訊,例如請求 method,URI 和 header。

讀取 HTTP 請求 header 時,nginx 會查詢關聯的虛擬伺服器配置。如果找到虛擬伺服器,請求將經歷六個階段:

  1. 伺服器重寫階段
  2. location 階段
  3. location 重寫階段(將請求帶回到上一個階段)
  4. 連線控制階段
  5. try_files 階段
  6. 日誌階段

為了響應請求生成必要的內容,nginx 將請求傳遞給合適的內容處理程式。根據確切的位置配置,nginx 可能首先嚐試所謂的無條件處理程式,如perlproxy_passflvmp4等。如果請求與上述任何內容處理程式都不匹配,則由以下處理程式之一按照以下順序選擇:random indexindexautoindexgzip_staticstatic

索引模組的詳細資訊可以在 nginx 文件中找到,但這些是使用尾部斜槓處理請求的模組。如果像mp4autoindex這樣的專用模組則不合適,內容被認為只是磁碟上的檔案或目錄(即靜態),並由static內容處理程式提供服務。對於目錄,它會自動重寫 URI,以便始終存在尾部斜槓(然後發出 HTTP 重定向)。

然後將內容處理程式的內容傳遞給 filter。filter 也附加到 location,並且可以為 location 配置多個 filter。filter 執行操作處理程式生成的輸出的任務。對於預先定義的開箱即用 filter,執行的順序在編譯時就確定。對於第三方 filter,可以在構建階段對其進行配置。在現有的 nginx 實現中,filter 只能進行出站更改,並且目前沒有機制來編寫和附加 filter 來進行輸入內容轉換。輸入過濾將出現在 nginx 的未來版本中。

filter 遵循特定的設計模式。呼叫 filter,開始工作,並呼叫下一個 filter,直到呼叫鏈中的最終 filter。之後,nginx 完成響應。filter 不必等待前一個 filter 完成。呼叫鏈中的下一個 filter 可以在上一個 filter 的輸入可用時立即開始工作(功能上與 Unix 管道非常相似)。反過來,生成的輸出響應可以在接收到來自上游伺服器的整個響應之前傳遞給客戶端。

還有 header filter 和 body filter;nginx 會分別用相關的 filter 來給相應 header 和 body 新增資料

header filter 主要有下面三個步驟

  1. 決定是否對這個響應進行操作
  2. 操作這個響應
  3. 呼叫下一個 filter

body filter 修改生成的資料,下面是 body filter 的一些案例

  • 服務端 includes
  • XSLT 過濾
  • 圖片過濾(比如修改圖片尺寸)
  • 修改編碼
  • gzip壓縮
  • chunked encoding

在 filter chain 之後,響應將傳遞給 writer。除了 writer 之外,還有一些額外特殊用途的 filter,即copypostponefilter。 copyfilter 負責使用可能儲存在代理臨時目錄中的相關響應內容填充記憶體緩衝區。 postponefilter 用於子請求。

子請求是請求 / 響應處理的非常重要的機制。子請求也是 nginx 最強大的方面之一。對於子請求,nginx 可以從與客戶端最初請求的 URL 不同的 URL 返回結果。一些 Web 框架將此稱為內部重定向。但是,nginx 更進一步 - 過濾器不僅可以執行多個子請求,而且可以將輸出組合成單個響應,但子請求也可以巢狀和分層。子請求可以執行其自己的子子請求,並且子子請求可以發起子子子請求。子請求可以對映到硬碟,其他處理程式或上游伺服器上的檔案。子請求對於根據原始響應中的資料插入其他內容非常有用。例如,SSI(伺服器端包含)模組使用過濾器來解析返回文件的內容,然後將“include”指令替換為指定 URL 的內容。或者,它可以是一個過濾器,將文件的整個內容視為要檢索的 URL,然後將新文件附加到 URL 本身

upstream 和負載均衡器也值得簡要描述。upstream 用於實現可以被識別為反向代理(proxy_pass處理程式的內容。upstream 模組主要準備將請求傳送到 upstream 伺服器(或“後端”)並接收響應。這裡沒有呼叫輸出 filter。當 upstream 伺服器準備好被寫入和讀取時,upstream 模組確切地做的是設定要呼叫的回撥。存在實現以下功能的回撥:

  • 建立的請求緩衝被髮送到 upstream 伺服器的
  • 重新連線到 upstream 伺服器(在請求產生之前)
  • 處理 upstream 伺服器響應的內容並且儲存指向從 upstream 伺服器內容的指標。
  • 放棄請求(主要是客戶端過早斷開連線)
  • 從 upstream 伺服器讀完內容之後結束請求
  • 整理響應 body(比如刪除 http 響應 trailer)

負載均衡器模組連線到proxy_pass處理程式,以便在多個 upstream 伺服器符合條件時提供選擇上游伺服器的功能。負載均衡器註冊啟用配置檔案指令,提供額外的上游初始化函式(以解析 DNS 中的上游名稱等),初始化連線結構,決定在何處路由請求以及更新統計資訊。目前,nginx 支援兩種標準規則,用於對 upstream 伺服器進行負載均衡:迴圈和 ip-hash。

upstream 和負載均衡處理機制包括用於檢測失敗的上游伺服器以及將新請求重新路由到其餘伺服器的演算法 - 儘管計劃進行大量額外工作以增強此功能。總的來說,nginx 開發團隊計劃對負載均衡器進行更多的工作,並且在下一版本的 nginx 中,將大大改進跨不同上游伺服器分配負載以及執行狀況檢查的機制。

還有一些其他有趣的模組提供了一組額外的變數供配置檔案使用。雖然 nginx 中的變數是在不同的模組中建立和更新的,但有兩個模組完全專用於變數:geomapgeo模組用於根據客戶端的 IP 地址進行跟蹤。此模組可以建立依賴於客戶端 IP 地址的任意變數。另一個模組map允許從其他變數建立變數,實質上提供了對主機名和其他執行時變數進行靈活對映的能力。這種模組可以稱為變數處理程式。

在單個 nginx worker 中實現的記憶體分配機制在某種程度上受到了 Apache 的啟發。nginx 記憶體管理的高度概述如下:對於每個連線,必要的記憶體緩衝區被動態分配,連結,用於儲存和操作請求和響應的頭部和主體,然後在連線釋放時釋放。值得注意的是,nginx 試圖儘可能避免在記憶體中複製資料,並且大多數資料都是通過指標值傳遞的,而不是通過呼叫memcpy

更深入一點,當模組生成響應時,將檢索到的內容放入記憶體緩衝區,然後將其新增到緩衝鏈連結中。後續處理也適用於此緩衝鏈連結。緩衝鏈在 nginx 中非常複雜,因為有幾種處理方案因模組型別而異。例如,在實現 body filter 模組時精確管理緩衝區可能非常棘手。這樣的模組一次只能在一個緩衝區(鏈路的鏈路)上執行,它必須決定是否覆蓋輸入緩衝區,用新分配的緩衝區替換緩衝區,或者在有問題的緩衝區之前或之後插入新的緩衝區。更復雜的是,有時模組會收到幾個緩衝區,因此它必須有一個不完整的緩衝區鏈。但是,此時 nginx 只提供了一個用於操作緩衝區鏈的低階 API,因此在進行任何實際實現之前,第三方模組開發人員應該能夠熟練使用 nginx 這個神祕的部分。

關於上述方法的註釋是在連線的整個生命週期中分配了記憶體緩衝區,因此對於長期連線,保留了一些額外的記憶體。同時,在空閒的 keepalive 連線上,nginx 只花費 550 個位元組的記憶體。對 nginx 的未來版本進行可能的優化將是重用和共享記憶體緩衝區以實現長期連線。

管理記憶體分配的任務由 nginx 池分配器完成。共享記憶體區域用於接受互斥鎖,快取後設資料,SSL 會話快取以及與頻寬管制和管理(限制)相關的資訊。在 nginx 中實現了一個 slab 分配器來管理共享記憶體分配。為了同時安全地使用共享記憶體,可以使用許多鎖定機制(互斥鎖和訊號量)。為了組織複雜的資料結構,nginx 還提供了一個紅黑樹實現。紅黑樹用於將快取後設資料儲存在共享記憶體中,跟蹤非正規表示式位置定義以及其他幾項任務。

遺憾的是,上述所有內容從未以一致和簡單的方式描述,因此開發 nginx 的第三方擴充套件的工作非常複雜。雖然存在關於 nginx 內部的一些好的文件 - 例如,由 Evan Mille r 生成的那些文件 - 需要大量的逆向工程工作,並且 nginx 模組的實現仍然是許多人的黑科技。

儘管與第三方模組開發相關的某些困難,nginx 使用者社群最近看到了許多有用的第三方模組。例如,有一個用於 nginx 的嵌入式 Lua 直譯器模組,用於負載均衡的附加模組,完整的 WebDAV 支援,高階快取控制以及本章作者鼓勵並將在未來支援的其他有趣的第三方工作。(參考 Open Resty -- 譯者注)

14.5. 收穫

當 Igor Sysoev 開始編寫 nginx 時,大多數給網際網路賦能的軟體已經存在,並且這種軟體的體系結構通常遵循傳統伺服器和網路硬體,作業系統和舊的網際網路體系結構。然而,這並沒有阻止 Igor 認為他能夠繼續改進 Web 伺服器領域的東西。所以,雖然第一課可能看起來很簡單,但事實是:總有改進的餘地。

考慮到更好的 Web 軟體的想法,Igor 花了很多時間開發初始程式碼結構並研究為各種作業系統優化程式碼的不同方法。十年後,參考在版本 1 上的多年積極開發,他如今正在開發 nginx 版本 2.0 的原型。很明顯,一個軟體產品的新架構的初始原型和初始程式碼結構對於未來的重要性是非常重要的。

值得一提的另一點是發展應該集中。Windows 版本的 nginx 可能是一個很好的例子,說明如何避免在既不是開發人員的核心競爭力或目標應用程式的情況下稀釋開發工作。它同樣適用於重寫引擎,該引擎在多次嘗試增強 nginx 時出現,具有更多功能以便與現有的舊設定向後相容。

但值得一提的是,儘管 nginx 開發者社群不是很大,但 nginx 的第三方模組和擴充套件一直是其受歡迎程度的重要組成部分。 Evan Miller,Piotr Sikora,Valery Kholodkov,Zhang Yichun(agentzh 中文名:章亦春)以及其他才華橫溢的軟體工程師所做的工作得到了 nginx 使用者社群及其原始開發人員的讚賞。


This work is made available under the Creative Commons Attribution 3.0 Unported license. Please see the full description of the license for details.

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄