《開源軟體架構》中nginx章節
nginx
nginx(發音為“engine x”)是由俄羅斯軟體工程師Igor Sysoev編寫的免費開源Web伺服器。自2004年公開發布以來,nginx一直專注於高效能,高併發性和低記憶體消耗。Web伺服器功能之上的其他功能,如負載均衡,快取,訪問和頻寬控制,以及與各種應用程式高效整合的能力,有助於使nginx成為現代網站架構的不錯選擇。目前,nginx是網際網路上第二大最受歡迎的開源Web伺服器。
14.1. 為什麼高併發性很重要?
如今,網際網路如此廣泛和無處不在,我們很難想象它十年前還並不存在 。從簡單的HTML生成可點選文字,基於NCSA,然後是Apache Web伺服器,到全球超過20億使用者使用的永遠線上的通訊媒體,它已經有了很大的發展。隨著永久連線的個人電腦,移動裝置和最近的平板電腦的普及,網際網路格局正在迅速變化,整個經濟已經實現數字化連線。線上服務變得更加精緻,明顯偏向即時的實時資訊和娛樂。執行線上業務的安全方面也發生了重大變化。因此,網站現在比以前複雜得多,通常需要更多工程師的努力使網站變得健壯和可擴充套件。
對於一個網站架構師來說,最大的挑戰之一就是併發。自web服務出現以來,併發等級一直在不斷提高。一個受歡迎的網站同時為數十萬甚至數百萬的使用者提供服務並不罕見。十年前,併發的主要原因是客戶端速度慢——使用ADSL或撥號連線的使用者。現在,併發性是由移動客戶端和較新的應用程式架構的組合引起的,這些架構通常基於維護長連線來允許客戶端實時接收新聞、推文、好友動態等更新。導致併發性增加的另一個重要因素是現代瀏覽器行為的改變,這些瀏覽器同時開啟4到6個到網站的連線,以提高頁面載入速度。
為了說明慢客戶端的問題,設想一個簡單的基於Apache的web伺服器,它產生相對較短的100KB 的響應——一個帶有文字或影象的web頁面。生成或檢索此頁面只需零點幾秒,但是將其傳輸到頻寬為80kbps (10 KB/s)的客戶端需要10秒。本來,web伺服器提取100 KB的內容是相對快速地,然後它需要10秒鐘緩慢地將這些內容傳送到客戶端之後才能釋放連線 。現在假設你有1,000個同時連線的客戶端,它們請求類似的內容。如果每個客戶端只額外分配了1 MB的記憶體,那麼就會產生1000 MB(約1 GB)的額外記憶體,專門用於為1000個客戶端提供100 KB的內容。實際上,一個典型的基於Apache的web伺服器通常為每個連線分配超過1 MB的額外記憶體,遺憾的是,幾十kbps仍然通常是行動通訊的有效速度。儘管在某種程度上,通過增加作業系統核心套接字緩衝區的大小,可以改善將內容傳送到較慢的客戶端的情況,但這不是問題的通用解決方案,而且可能會產生不良的副作用。
對於長連線,處理併發的問題更加明顯,因為為了避免建立新的HTTP連線相關的延遲,客戶端將保持連線,並且對於每個連線的客戶端,web伺服器將分配一定數量的記憶體。
因此,為了處理由於受眾的增長帶來的增加的工作負載以及因此導致的更高的併發等級(並且能夠持續地處理),網站應該基於許多非常有效的構建模組。雖然等式的其他部分,如硬體(CPU、記憶體、磁碟)、網路容量、應用程式和資料儲存體系結構也很重要,但客戶端連線是在web伺服器軟體中接收和處理的。因此,web伺服器應該能夠隨著每秒同步連線和請求數量的增加情況來擴充套件,而非線性地擴充套件。
Apache不合適嗎?
Apache,這種web伺服器軟體在20世紀90年代初就有了自己的根基,直到今天仍然在很大程度上主宰著網際網路。最初,它的架構與當時已有的作業系統,硬體相匹配,同時也與網際網路當時發展的情況相匹配,當時的網站通常是執行單個Apache例項的獨立物理伺服器。到2000年代初,很明顯,獨立的web伺服器模型不能通過簡單的複製來滿足不斷增長的web服務的需求。儘管Apache為未來的開發提供了堅實的基礎,但它的架構是為每個新連線生成自身的副本,這並不適合網站的非線性可伸縮性。最終,Apache成為了一個通用的web伺服器,它專注於擁有許多不同的特性、各種第三方擴充套件以及普遍適用於幾乎任何web應用程式開發。然而,任何東西都是有代價的,在單個軟體中擁有如此豐富和通用的組合工具的缺點是可伸縮性較差,因為每個連線的CPU和記憶體使用量都在增加。
因此,當伺服器硬體、作業系統和網路資源不再是網站增長的主要制約因素時,全世界的web開發人員開始尋找一種更有效的執行web伺服器的方法。大約十年前,著名軟體工程師 Daniel Kegel 宣稱 ,“是時候讓web伺服器同時處理1萬個客戶端了”,並預言了我們現在所說的網際網路雲服務。 Kegel 的 C10K 清單激發了許多解決web伺服器優化問題的嘗試,以同時處理大量客戶端,nginx是其中最成功的一個。
為了解決10,000個併發連線的C10K問題,nginx採用了一種不同的體系結構——這種體系結構更適合於同時連線數和每秒請求數量的非線性擴充套件。nginx是基於事件的,所以它不遵循Apache為每個web頁面請求生成新程序或執行緒的風格。最終結果是,即使負載增加,記憶體和CPU使用仍然是可控的。nginx現在可以在使用典型硬體的伺服器上處理數萬個併發連線。
當nginx的第一個版本釋出時,它打算與Apache一起部署,這樣nginx就可以處理HTML、CSS、JavaScript和影象等靜態內容,從而從基於Apache的應用伺服器上解除安裝併發和延遲處理。在其開發過程中,nginx通過使用FastCGI、uswgi或SCGI協議以及分散式記憶體物件快取系統( 如memcached ),增加了與應用程式的整合。還添加了其他有用的功能,如具有負載均衡和快取的反向代理。這些額外的特性使nginx成為構建可伸縮web基礎設施的有效組合工具。
2012年2月,Apache 2.4.x 分支公開發布。儘管Apache的最新版本添加了新的多處理核心模組和新的代理模組,旨在增強擴充套件性性和效能,但是現在就判斷其效能、併發性和資源利用率是否與純事件驅動的web伺服器持平或更好還為時過早。不過,如果Apache應用伺服器能在新版本中更好地擴充套件,那就太好了,因為它可能會緩解後端瓶頸,這在典型的nginx + Apache web配置中通常仍然沒有得到解決。
使用nginx還有哪些優勢?
以高效能和高效率處理高併發一直是部署nginx的主要優點。然而,還有更多有趣的好處。
在過去的幾年中,web架構師已經接受了將應用程式基礎架構與web伺服器分離的思想。然而,以前存在的基於 LAMP (Linux、Apache、MySQL、 PHP、Python或Perl ) 的網站形式,現在可能變成不僅僅是基於 LEMP (“E”代表“ Engine x ”)的形式,而是越來越多的將web伺服器推向基礎設施的邊緣,並以不同的方式圍繞它整合相同或經過改進的應用程式和資料庫工具集。
nginx是非常適合,因為它提供了所需的關鍵功能方便地解除安裝併發,延遲處理,SSL(安全套接層),靜態內容,壓縮和快取、連線和請求的限制,甚至HTTP流媒體從應用程式層到更有效的邊緣web伺服器層。它還允許直接與memcached / Redis或其他“NoSQL”解決方案整合,從而在服務大量併發使用者時提高效能。
——– 隨著最近開發工具包和程式語言的廣泛使用,越來越多的公司正在改變他們的應用程式開發和部署習慣。nginx已經成為這些不斷變化的範例中最重要的元件之一,它已經幫助許多公司在預算內快速啟動和開發web服務。
隨著最近開發工具包和程式語言的廣泛使用,越來越多的公司正在改變他們的應用程式開發和部署的習慣。nginx已經成為這些不斷變化的範例中最重要的元件之一,它已經幫助許多公司在預算內快速啟動和開發web服務。
nginx的第一行程式碼是在2002年寫的。2004年,它根據 two-clause BSD 許可協議公開發布。從那以後,nginx使用者的數量一直在增長,他們貢獻想法,提交bug報告、建議和觀察,這些對整個社群都非常有幫助和益處。
nginx程式碼庫是原創的,完全是用C語言從頭開始編寫的。nginx已經被移植到許多架構和作業系統,包括Linux、FreeBSD、Solaris、Mac OS X、AIX和Microsoft Windows。nginx有它自己的庫,它的標準模組除了除了zlib、PCRE和OpenSSL 之外,只使用系統的C庫,這幾個模組如果不需要或者因為潛在的許可衝突,可以選擇將它們在構建中排除。
簡單介紹一下nginx的Windows版本。雖然nginx可以在Windows環境中工作,但nginx的Windows版本更像是一個概念驗證,而不是一個功能齊全的埠。nginx和Windows核心架構有一定的侷限性,它們目前還不能很好地互動。已知的Windows nginx版本的問題包括併發連線數量少得多、效能下降、沒有快取和頻寬控制。未來的Windows nginx版本將更接近主流功能。
14.2. nginx架構概述
傳統的基於程序或執行緒的併發連線處理模型涉及到使用單獨的程序或執行緒處理每個連線,以及阻塞網路或輸入/輸出操作。根據應用程式的不同,它在利用記憶體和CPU消耗方面可能非常低效。生成單獨的程序或執行緒需要準備新的執行時環境,包括分配堆和堆疊記憶體,以及建立新的執行上下文。建立這些還會花費額外的CPU時間,這最終會導致效能低下,因為執行緒會過多的切換上下文。所有這些複雜性都表現在Apache等較老的web伺服器體系結構中。這是在提供豐富的通用特性和優化伺服器資源使用之間的權衡。
從一開始,nginx就被認為是一種專業的工具,可以在實現網站動態增長的同時,獲得更高的效能、更密集和更經濟的伺服器資源使用,所以它採用了一種不同的模式。它實際上是受到了各種作業系統中正在開發的基於事件的高階機制的啟發。其結果是一個模組化的、事件驅動的、非同步的、單執行緒的、非阻塞的體系結構,它成為nginx程式碼的基礎。
nginx大量使用多路複用和事件通知,並將特定的任務分配給不同的程序。連線在稱為worker的有限數量的單執行緒程序中以高效的執行迴圈進行處理。在每個worker程序中,nginx每秒可以處理數千個併發連線和請求。
程式碼結構
nginx的worker程式碼包括核心模組和功能模組。nginx的核心負責維護一個緊密的執行迴圈,並在請求處理的每個階段執行模組程式碼的適當部分。模組構成了大多數展示層和應用層功能。模組從網路和儲存中讀寫、轉換內容、進行出站過濾、應用伺服器端包含操作,並在代理啟用時將請求傳遞給上游伺服器。
nginx的模組化架構通常允許開發人員在不修改nginx核心的情況下擴充套件web伺服器功能集。nginx模組版本略有不同,即核心模組、事件模組、階段處理程式、協議、變數處理程式、過濾器、上行流和負載均衡器。此時,nginx不支援動態載入模組;也就是說,模組在構建階段與核心一起編譯。但是,計劃在未來的主要版本中支援可載入模組和ABI。關於不同模組的角色的更詳細資訊可以在 14.4節 中找到。
在處理與接受、處理和管理網路連線和內容檢索相關的各種操作(如kqueue、epoll和event ports ) 時,nginx在Linux、Solaris和基於BSD的作業系統中使用事件通知機制和大量磁碟I/O效能增強。其目標是向作業系統提供儘可能多的提示,以便及時獲得針對入站和出站流量、磁碟操作、讀取或寫入套接字 、超時等的非同步反饋。對於執行在每個基於unix的作業系統上的nginx,對使用了多路複用和高階I/O操作的不同方法進行了大量優化。
nginx架構的高階概述如 圖14.1 所示。

Workers 模型
如前所述,nginx不會為每個連線生成一個新的程序或執行緒。相反,worker程序接受來自共享的“listen”套接字的新請求,並在每個worker程序內部執行高效的執行迴圈,來處理每個worker程序中的數千個連線。nginx沒有專門的仲裁或分配機制來決定worker程序的連線分配;這項工作是由OS核心機制完成的。在啟動時,將建立一組初始監聽套接字。然後,worker程序在處理HTTP請求和響應時,不斷地從這些套接字接收、讀寫。
執行迴圈是nginx worker程式碼中最複雜的部分。它包括全面的內部呼叫,並嚴重依賴非同步任務處理的思想。非同步操作通過模組化、事件通知、廣泛使用回撥函式和微調計時器來實現。總的來說,關鍵的原則是儘可能不阻塞。nginx阻塞的惟一情況是worker程序沒有足夠的磁碟儲存效能。
因為nginx不為每個連線派生新的程序或執行緒,所以記憶體使用非常有限,在絕大多數情況下非常高效。nginx還節省了CPU週期,因為沒有程序或執行緒的持續建立-銷燬模式。nginx所做的是檢查網路和儲存的狀態,初始化新連線,將它們新增到執行迴圈中,並非同步處理,直到完成,此時連線將被釋放並從執行迴圈中刪除。結合系統呼叫的謹慎使用和池和板式記憶體分配器等支援介面的精確實現,nginx通常可以在極端工作負載下達到中等到低的CPU使用率。
因為nginx生成幾個worker來處理連線,所以它可以很好地多核擴充套件。通常,每個核心使用一個單獨的worker能充分利用多核架構,並防止執行緒抖動和鎖定。不存在資源匱乏,資源控制機制被隔離在單執行緒worker程序中。該模型還允許跨物理儲存裝置實現更好的可伸縮性,促進更多的磁碟利用率,並避免磁碟I/O上的阻塞。因此,伺服器資源在多個worker程序之間共享工作負載的情況下得到了更有效的利用。
對於一些磁碟使用和CPU負載模式,應該調整nginx worker程序的數量。這裡的規則有些基礎,規則很基礎,系統管理員應該針對不同的工作負載嘗試一些配置。一般建議如下:如果負載模式是CPU密集型的(例如,處理大量TCP/IP、執行SSL或壓縮),那麼nginx worker程序的數量應該與CPU核心的數量相匹配;如果負載主要是磁碟I/O限制,(例如,服務於來自儲存的不同內容集,或者大量的代理),那麼worker的數量可能是CPU核心數量的1.5到2倍。一些工程師根據獨立儲存單元的數量來選擇worker的數量,但是這種方法的效率取決於磁碟儲存的型別和配置。
nginx的開發人員將在即將釋出的版本中需要解決的一個主要問題是如何避免磁碟I/O上的大部分阻塞。目前,如果沒有足夠的儲存效能來服務於特定worker產生的磁碟操作,該worker可能仍然會阻塞磁碟的讀寫。存在許多機制和配置檔案指令來減少這種磁碟I/O阻塞場景。最值得注意的是,sendfile和AIO等選項的組合通常會為磁碟效能帶來很大的空間。nginx的安裝應該基於資料集、nginx可用的記憶體數量和底層儲存架構來計劃。
現有worker模型的另一個問題與對嵌入式指令碼的支援有限。舉個例子,對於標準的nginx發行版,只支援嵌入Perl指令碼。對此有一個簡單的解釋:關鍵問題是嵌入式指令碼可能阻塞任何操作或意外退出。這兩種行為都會立即導致掛起worker,同時影響數千個連線。計劃進行更多的工作,使使用nginx的嵌入式指令碼更簡單、更可靠,並適用於更廣泛的應用程式。
nginx流程角色
nginx在記憶體中執行幾個程序;有一個主程序和幾個worker程序。還有一些特殊用途的程序,特別是快取載入器和快取管理器。nginx版本1.x中的所有程序都是單執行緒的。所有程序主要使用共享記憶體機制進行程序間通訊。主程序以root使用者執行。快取載入器、快取管理器和worker程序作為非特權使用者執行。
主流程負責以下任務:
- 讀取和驗證配置
- 建立、繫結和關閉套接字
- 啟動、終止和維護配置的worker程序數量
- 不中斷服務的重新配置
- 控制不間斷的二進位制升級(啟動新的二進位制並在必要時回滾)
- 重啟日誌檔案
- 編譯嵌入式Perl指令碼
worker程序接受、處理和處理來自客戶端的連線,提供反向代理和過濾功能,並完成nginx能夠完成的幾乎所有其他工作。關於監視nginx例項的行為,系統管理員應該關注worker,因為它們是反映web伺服器實際日常操作的程序。
快取載入程序負責檢查磁碟上的快取項,並用快取元資料填充nginx的記憶體資料庫。本質上,快取載入程序準備nginx例項來處理已經儲存在磁碟上的檔案,這些檔案位於一個特殊分配的目錄結構中。它遍歷目錄,檢查快取內容元資料,更新共享記憶體中的相關條目,然後在一切就緒並準備使用時退出。
快取管理器主要負責快取過期和失效。在正常的nginx操作過程中,它會保留在記憶體中,在出現故障時,由主程序重新啟動。
nginx快取簡要概述
nginx中的快取是在檔案系統上以分層資料儲存的形式實現的。快取鍵是可配置的,可以使用不同的特定的請求引數來控制進入快取的內容。快取鍵和快取元資料儲存在共享記憶體段中,快取載入器、快取管理器和worker程序可以訪問這些記憶體段。目前,除了作業系統的虛擬檔案系統機制所隱含的優化之外,沒有任何檔案的記憶體快取。每個快取的響應都放在檔案系統上的不同檔案中。層次結構(級別和命名細節)是通過nginx配置指令控制的。當將響應寫入快取目錄結構時,檔案的路徑和名稱將從代理URL的MD5雜湊派生。
將內容放入快取的過程如下:當nginx從上游伺服器讀取響應時,首先將內容寫入快取目錄結構之外的臨時檔案。當nginx完成處理請求時,它重新命名臨時檔案並將其移動到快取目錄。如果用於代理的臨時檔案目錄位於另一個檔案系統上,那麼將複製該檔案,因此建議將臨時目錄和快取目錄都儲存在同一個檔案系統上。當需要顯式清除快取目錄結構中的檔案時,刪除它們是非常安全的。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伺服器行為相關的配置都應該位於一組集中的配置檔案中。
配置檔案最初由主程序讀取和驗證。當從主程序派生worker程序時,worker程序可以使用已編譯的只讀形式的nginx配置。配置結構由常用的虛擬記憶體管理機制自動共享。
nginx配置有幾個不同的上下文,分別用於main、http、server、upstream 、 location
(以及用於郵件代理的mail)塊的指令。上下文不重疊。例如,不存在在main指令塊中location塊這樣的事情。此外,為了避免不必要的歧義,沒有任何類似於“全域性web伺服器”配置的東西。nginx的配置應該是整潔和邏輯清晰的,它允許使用者維護包含數千條指令的複雜配置檔案。在一次私人談話中,Sysoev說:“全域性伺服器配置中的位置、目錄和其他塊是我在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指令 ,並在任何可能的情況下使用它。
14.4. nginx內部
如前所述,nginx程式碼庫由一個核心和一些模組組成。nginx的核心負責提供web伺服器的基礎、web和郵件反向代理功能;它支援使用底層網路協議,構建必要的執行時環境,並確保不同模組之間的無縫互動。然而,大多數協議和應用程式特定的特性是由nginx模組完成的,而不是核心。
在內部,nginx通過管道或模組鏈處理連線。換句話說,每一個操作都有一個做相關工作的模組;例如,壓縮、修改內容、執行伺服器端包含、通過FastCGI或uwsgi協議與上游應用伺服器通訊或與memcached通訊。
有兩個nginx模組位於核心和真正的“功能”模組之間。這些模組是http和mail。這兩個模組在核心元件和底層元件之間提供了額外的抽象級別。在這些模組中,實現了與各自的應用層協議(如HTTP、SMTP或IMAP)來處理相關聯的事件序列。結合nginx核心,這些上層模組負責維護對各個功能模組的正確呼叫順序。雖然HTTP協議目前是作為http模組的一部分實現的,但是由於需要支援SPDY等其他協議(參見“ SPDY:用於更快的web的實驗性協議 ”),未來計劃將其分離為功能模組。
功能模組可以分為事件模組、階段處理程式、輸出過濾器、變數處理程式、協議、上行流和負載均衡器。大多數這些模組補充了nginx的HTTP功能,儘管事件模組和協議也用於 mail 模組。事件模組提供特定的基於os的事件通知機制,如kqueue或epoll。nginx使用的事件模組取決於作業系統功能和構建配置。協議模組允許nginx通過HTTPS、TLS/SSL、SMTP、POP3和IMAP進行通訊。
典型的HTTP請求處理週期如下所示。
- 客戶端傳送HTTP請求。
- nginx 核心根據與請求匹配的location配置選擇適當的階段處理程式。
- 如果配置了,負載均衡器將選擇上游伺服器進行代理。
- 階段處理器完成它的工作,並將每個輸出緩衝區傳遞給第一個過濾器。
- 第一個過濾器將輸出傳遞給第二個過濾器。
- 第二個過濾器將輸出傳遞給第三個(以此類推)。
- 最終的響應被髮送到客戶端。
nginx模組呼叫是完全可定製的。它是通過使用指向可執行函式的指標的一系列回撥來執行的。然而,這樣做的缺點是,它可能會給想要編寫自己的模組的程式設計師帶來很大的負擔,因為他們必須準確地定義模組應該如何以及何時執行。nginx API和開發人員的文件都得到了改進,並提供了更多的可用性來緩解這一問題。
可以附加模組的一些例子是:
- 在讀取和處理配置檔案之前
- location和server出現的每個配置指令
- 初始化主配置時
- 初始化伺服器(即,主機/埠)時
- 合併伺服器配置和主配置時
- 初始化location配置或與其父server配置合併時
- 主程序啟動或退出時
- 當新的worker程序啟動或退出時
- 處理請求時
- 過濾響應頭和響應體時
- 挑選,初始化並重新初始化對上游伺服器的請求時
- 處理來自上游伺服器的響應時
- 完成與上游伺服器的互動時
- When finishing an interaction with an upstream server
在一個worker程序中,生成響應的執行迴圈的操作序列如下所示:
- 開始
ngx_worker_process_cycle()。
- 使用特定的OS機制(
如epoll
或kqueue)
來處理事件。 - 接受事件並分派相關操作。
- 處理/代理請求頭和請求體。
- 生成響應內容(響應頭、響應體)並以流的形式傳送到客戶端。
- 完成請求。
- 重新初始化計時器和事件。
執行迴圈本身(步驟5和6)確保增量生成響應並將其流式傳輸到客戶端。
處理HTTP請求的更詳細檢視可能如下所示:
- 初始化請求處理。
- 處理請求頭。
- 處理請求體。
- 呼叫關聯的處理程式。
- 執行貫穿處理階段。
這就把我們帶到了各個處理階段。當nginx處理HTTP請求時,它會將其傳遞到多個處理階段。在每個階段都有要呼叫的處理程式。通常,階段處理程式處理一個請求並生成相關的輸出。階段處理程式附加到配置檔案中定義的位置。
階段處理程式通常做四件事:獲取位置配置、生成適當的響應、傳送響應頭和響應體。處理程式有一個引數:描述請求的特定結構。請求結構包含許多關於客戶端請求的有用資訊,例如請求方法、URI和請求頭。
當讀取HTTP請求頭時,nginx會查詢相關的虛擬伺服器配置。如果找到虛擬伺服器,請求將經過六個階段: .
- 伺服器重寫階段
- 定位階段
- 定位重寫階段(可以將請求恢復到上一階段)
- 訪問控制階段
- try_files 階段
- 日誌記錄階段
為了生成響應請求所需的內容,nginx將請求傳遞給適當的內容處理程式。根據確切的位置配置,nginx可能首先嚐試所謂的無條件處理程式,如perl、proxy_pass、flv、mp4等。如果請求不匹配上面的任何內容處理程式,則由以下處理程式中的一個按如下順序選擇:random index、index、autoindex、gzip_static、static。
索引模組的詳細資訊可以在nginx文件中找到,但是這些模組處理以斜槓結尾的請求。如果像mp4或autoindex這樣的專門模組不合適,則認為內容只是磁碟上的檔案或目錄(即靜態的),由 static 內容處理程式提供服務。對於一個目錄,它會自動重寫URI,使末尾斜槓始終存在(然後發出HTTP重定向)。
然後內容處理程式的內容被傳遞給過濾器。過濾器也附加到位置,並且可以為一個位置配置多個過濾器。過濾器的任務是操作處理程式生成的輸出。過濾器的執行順序在編譯時確定。對於開箱即用過濾器,它是預定義的,對於第三方過濾器,它可以在構建階段進行配置。在現有的nginx實現中,過濾器只能進行出站更改,目前還沒有編寫和附加過濾器來進行輸入內容轉換的機制。輸入過濾將出現在nginx的未來版本中。
過濾器遵循特定的設計模式。過濾器被呼叫後,開始工作,並呼叫下一個過濾器,直到呼叫鏈中的最後一個過濾器。然後,nginx完成響應。過濾器不必等待上一個過濾器完成。鏈中的下一個過濾器可以在上一個過濾器的輸入可用時立即開始自己的工作(在功能上非常類似於Unix管道)。反過來,在接收到來自上游伺服器的整個響應之前,就可以將生成的輸出響應傳遞給客戶端。
有頭過濾器和體過濾器;nginx分別將響應頭和響應體提供給關聯的過濾器。
頭過濾器包括三個基本步驟:
- 決定是否對此響應進行操作。
- 操作響應。
- 呼叫下一個過濾器。
體過濾器轉換生成的內容。體過濾器的例子包括:
gzip
在過濾器鏈之後,響應被傳遞給寫入器。除了寫入器之外,還有兩個額外的特殊用途過濾器,即 copy 過濾器和 postpone 過濾器。copy 過濾器負責用可能儲存在代理臨時目錄中的相關響應內容填充記憶體緩衝區。postpone 過濾器用於子請求。
子請求是請求/響應處理的一種非常重要的機制。子請求也是nginx最強大的方面之一。通過子請求,nginx可以從與客戶端最初請求的URL不同的URL返回結果。一些web框架稱之為內部重定向。然而,nginx更進一步——過濾器不僅可以執行多個子請求並將輸出組合成單個響應,而且子請求也可以巢狀和分層。子請求可以執行自己的子請求,孫請求也可以發起它的子請求。子請求可以對映到硬碟、其他處理程式或上游伺服器上的檔案。子請求對於在原始響應的資料插入額外內容最有用。例如,SSI(伺服器端包含)模組使用過濾器解析返回文件的內容,然後用指定URL的內容替換 include 指令。或者,它可以是一個過濾器的示例,該過濾器將文件的整個內容視為要檢索的URL,然後將新文件追加到URL上。
upstream 和負載均衡器也值得簡要描述。上游用於實現可以定義為反向代理的內容處理程式(proxy_pass處理程式)。upstream 模組主要準備傳送到上游伺服器(或“後端”)的請求,並從上游伺服器接收響應。這裡沒有對輸出過濾器的呼叫。
upstream 模組所做的事情是設定回撥等待上游伺服器準備好寫入和讀取時呼叫。存在實現以下功能的回撥:
- 製作要傳送到上游伺服器的請求緩衝區(或緩衝區鏈)
- 重新初始化/重置與上游伺服器的連線(在再次建立請求之前發生)
- 處理上游響應的第一位並儲存指向從上游伺服器接收的有效負載的指標
- 中止請求(在客戶端過早終止時發生)
- 當nginx從上游伺服器讀取完成時結束請求
- 整理響應體(例如移除trailer)
負載均衡模組附加到proxy_pass處理程器,當有多個上游伺服器符合條件時,提供選擇上游伺服器的能力。負載均衡器註冊一個啟用的配置檔案指令,提供額外的上游初始化函式(解析DNS中的上游伺服器名稱,等等),初始化連線結構,決定請求的路由,並更新統計資訊。目前nginx支援兩種標準的規則來平衡上游伺服器的負載:輪詢和ip-hash。
upstream 和負載均衡處理機制包括檢測失效的上游伺服器和將新請求重新路由到其他伺服器的演算法——儘管計劃進行大量額外工作來增強此功能。一般來說,計劃對負載均衡器進行更多的開發,在nginx的下一個版本中,用於在不同上游伺服器之間分配負載以及健康檢查的機制將得到極大的改進。
還有一些其他有趣的模組,它們提供了一組額外的變數供配置檔案中使用。雖然nginx中的變數是跨不同模組建立和更新的,但是有兩個模組完全專用於變數:geo 和 map。geo模組用於根據客戶端的 IP 地址進行跟蹤。這個模組可以建立依賴於客戶端 IP 地址的任意變數。另一個模組 map 允許從其他變數建立變數,本質上提供了靈活對映主機名和其他執行時變數的能力。這種模組可以稱為變數處理程式。
程序nginx中單個worker中實現的記憶體分配機制在某種程度上受到Apache的啟發。nginx記憶體管理的高階描述如下:對於每個連線,必要的記憶體緩衝區是動態分配,關聯,用來儲存和操作請求和響應的頭和體的,然後在連線釋放時釋放。值得注意的是,nginx儘量避免在記憶體中複製資料,並且大多數資料是通過指標值傳遞的,而不是通過呼叫memcpy。
再深入一點,當模組生成響應時,檢索到的內容被放入記憶體緩衝區,然後將記憶體緩衝區新增到緩衝區鏈路中。後續處理也適用於這個緩衝鏈。在nginx中,緩衝鏈非常複雜,因為根據模組型別的不同,有幾種不同的處理場景。例如,在實現體過濾模組時,精確地管理緩衝區可能非常棘手。這樣的模組一次只能操作一個緩衝區(鏈路),它必須決定是覆蓋輸入緩衝區、用新分配的緩衝區替換緩衝區,還是在相關緩衝區之前或之後插入新的緩衝區。更復雜的是,有時一個模組會接收多個緩衝區,因此它必須對一個不完整的緩衝區鏈進行操作。然而,此時nginx只提供了一個用於操作緩衝鏈的低階API,因此在進行任何實際實現之前,第三方模組開發人員應該非常熟悉nginx的這個神祕部分。
關於上述方法的一個注意事項是,存在為連線的整個生命週期分配的記憶體緩衝區,因此對於長生命週期的連線,會保留一些額外的記憶體。同時,在空閒的keepalive連線上,nginx只花費550位元組的記憶體。nginx未來版本的一個可能的優化是對長生命週期的連線重用和共享記憶體緩衝區。
管理記憶體分配的任務由nginx池分配器完成。共享記憶體區域用於接受互斥鎖、快取元資料、SSL會話快取以及與頻寬策略和管理(限制)相關的資訊。nginx中實現了一個slab分配器來管理共享記憶體分配。為了同時安全使用共享記憶體,可以使用許多鎖定機制(互斥鎖和訊號量)。為了組織複雜的資料結構,nginx還提供了一個紅黑樹實現。紅黑樹用於將快取元資料儲存在共享記憶體中,跟蹤非非正則表示式位置定義和其他一些任務。
不幸的是,上述所有內容從未以一致和簡單的方式進行描述,這使得為nginx開發第三方擴充套件的工作變得非常複雜。儘管存在一些關於nginx內部構件的優秀文件(例如由Evan miller貢獻的那些文件),但是這類文件需要大量的逆向工程工作,並且nginx模組的實現對許多人來說仍然是一門神祕的藝術。
儘管第三方模組開發存在一定的困難,但nginx使用者社群最近看到了許多有用的第三方模組。例如,有一個用於nginx的嵌入式Lua直譯器模組、用於負載平衡的額外模組、完整的WebDAV支援、高階快取控制和其他有趣的第三方工作,本章的作者鼓勵並將在將來支援這些工作。
14.5. 經驗總結
當Igor Sysoev開始編寫nginx時,大多數支援Internet的軟體都已經存在,而此類軟體的體系結構通常遵循遺留伺服器和網路硬體、作業系統和舊Internet體系結構的定義。然而,這並沒有阻止Igor認為他可以改進web伺服器領域的東西。因此,第一個經驗顯而易見,它是:總有改進的空間。
懷著開發更好的web軟體的想法,Igor花了大量時間開發初始程式碼結構,並研究了針對各種作業系統優化程式碼的不同方法。十年後,考慮到在版本1上多年的積極開發,他正在開發nginx 2.0版本的原型。很明顯,新體系結構的初始原型和初始程式碼結構對軟體產品的未來至關重要。
另一點值得一提的是,開發應該專注。nginx的Windows版本可能是一個很好的例子,說明了避免開發人員的核心能力或目標應用程式之外的工作耗費太多精力是值得的。它同樣適用於重寫引擎,出現多次嘗試使用更多特性增強nginx,以便與現有遺留設定向後相容。
最後但同樣重要的,值得一提的是,儘管nginx開發人員社群不是很大,但第三方模組和擴充套件一直是nginx廣受歡迎的重要原因。Evan Miller、Piotr Sikora、Valery Kholodkov、Zhang Yichun (agentzh)等優秀軟體工程師的工作得到了nginx使用者社群及其原始開發人員的高度讚賞。
翻譯:xiushao