高效能Nginx介紹(二)
14.4 nginx內部
如前所述,nginx程式碼庫由核心和許多模組組成。 nginx的核心是負責提供Web伺服器,Web和郵件反向代理功能的基礎;它支援使用底層網路協議,構建必要的執行時環境,並確保不同模組之間的無縫互動。但是,大多數協議和應用程式特定的功能都是由nginx模組完成的,而不是核心模組。
在內部,nginx通過模組的管道或鏈來處理連線。換句話說,對於每個操作,都有一個正在進行相關工作的模組;例如,壓縮,修改內容,執行伺服器端包括,通過FastCGI或uwsgi協議與上游應用伺服器通訊,或與memcached通訊。
有幾個nginx模組位於核心和真正的“功能”模組之間。這些模組是http和郵件。這兩個模組在核心和較低級別元件之間提供了額外的抽象級別。在這些模組中,實現了與諸如HTTP,SMTP或IMAP的相應應用層協議相關聯的事件序列的處理。結合nginx核心,這些上層模組負責維護對各個功能模組的正確呼叫順序。雖然HTTP協議目前作為http模組的一部分實現,但由於需要支援其他協議(如SPDY),因此計劃將其分離為功能模組(請參閱“SPDY:用於更快的Web的實驗協議” “)。
功能模組可分為事件模組,階段處理程式,輸出過濾器,變數處理程式,協議,上游和負載平衡器。大多數這些模組補充了nginx的HTTP功能,但事件模組和協議也用於郵件。事件模組提供特定的OS依賴事件通知機制,如kqueue或epoll。 nginx使用的事件模組取決於作業系統功能和構建配置。協議模組允許nginx通過HTTPS,TLS / SSL,SMTP,POP3和IMAP進行通訊。
典型的HTTP請求處理週期如下所示。
- 客戶端傳送HTTP請求。
- nginx核心根據與請求匹配的已配置位置選擇適當的階段處理程式。
- 如果配置為執行此操作,負載平衡器將選擇上游伺服器進行代理。
- 階段處理程式完成其工作並將每個輸出緩衝區傳遞給第一個過濾器。
- 第一個過濾器將輸出傳遞給第二個過濾器。
- 第二個過濾器將輸出傳遞給第三個(依此類推)。
- 最終響應將傳送給客戶端。
nginx模組呼叫是非常可定製的。它通過一系列回撥使用指向可執行函式的指標來執行。然而,這樣做的缺點是它可能給想要編寫自己的模組的程式設計師帶來很大的負擔,因為他們必須準確定義模組應該如何以及何時執行。 nginx API和開發人員的文件都在不斷改進,並且可以更多地用來緩解這個問題。
模組可以附加的一些示例是:
- 在讀取和處理配置檔案之前
- 對於位置的每個配置指令以及它出現的伺服器
- 初始化主配置時
- 當初始化伺服器(即主機/埠)時
- 當伺服器配置與主配置合併時
- 初始化位置配置或與其父伺服器配置合併時
- 主程序啟動或退出時
- 當新的工作程序啟動或退出時
- 處理請求時
- 過濾響應頭和正文時
- 挑選,啟動並重新啟動對上游伺服器的請求
- 處理來自上游伺服器的響應時
- 完成與上游伺服器的互動時
在worker中,導致生成響應的執行迴圈的操作序列如下所示:
- 開始ngx_worker_process_cycle()。
- 使用OS特定機制(例如epoll或kqueue)處理事件。
- 接受事件併發送相關操作。
- 程序/代理請求標頭和正文。
- 生成響應內容(標題,正文)並將其流式傳輸到客戶端。
- 完成請求。
- 重新初始化計時器和事件。
- 執行迴圈本身(步驟5和6)確保增量生成響應並將其流式傳輸到客戶端。
處理HTTP請求的更詳細檢視可能如下所示:
- 初始化請求處理。
- 流程標題。
- 流程體。
- 呼叫關聯的處理程式。
- 貫穿處理階段。
為了響應請求生成必要的內容,nginx將請求傳遞給合適的內容處理程式。根據確切的位置配置,nginx可以首先嚐試所謂的無條件處理程式,如perl,proxy_pass,flv,mp4等。如果請求與上述任何內容處理程式都不匹配,則由以下處理程式之一選擇它,按照這個確切的順序:隨機索引,索引,自動索引,gzip_static,靜態。
索引模組的詳細資訊可以在nginx文件中找到,但這些是使用尾部斜槓處理請求的模組。如果像mp4或autoindex這樣的專用模組不合適,則內容被認為只是磁碟上的檔案或目錄(即靜態),並由靜態內容處理程式提供服務。對於目錄,它會自動重寫URI,以便始終存在尾部斜槓(然後發出HTTP重定向)。
然後將內容處理程式的內容傳遞給過濾器。過濾器也附加到位置,並且可以為位置配置多個過濾器。過濾器執行操作處理程式生成的輸出的任務。過濾器執行的順序在編譯時確定。對於預先定義的開箱即用過濾器,對於第三方過濾器,可以在構建階段對其進行配置。在現有的nginx實現中,過濾器只能進行出站更改,並且目前沒有機制來編寫和附加過濾器來進行輸入內容轉換。輸入過濾將出現在nginx的未來版本中。
過濾器遵循特定的設計模式。呼叫過濾器,開始工作,並呼叫下一個過濾器,直到呼叫鏈中的最終過濾器。之後,nginx完成響應。過濾器不必等待前一個過濾器完成。鏈中的下一個過濾器可以在前一個過濾器的輸入可用時立即開始工作(功能上與Unix管道非常相似)。反過來,生成的輸出響應可以在接收到來自上游伺服器的整個響應之前傳遞給客戶端。
有標頭過濾器和身體過濾器; nginx分別將響應的標題和正文提供給關聯的過濾器。
標頭過濾器包含三個基本步驟:
- 決定是否對此響應進行操作。
- 操作響應。
- 呼叫下一個過濾器。
Body過濾器轉換生成的內容。身體過濾器的例子包括:
- 伺服器端包括
- XSLT過濾
- 影象過濾(例如,動態調整影象大小)
- charset修改
- gzip壓縮
- 分塊編碼
在過濾器鏈之後,響應將傳遞給writer。除了作者之外,還有一些額外的專用過濾器,即複製過濾器和推遲過濾器。複製過濾器負責使用可能儲存在代理臨時目錄中的相關響應內容填充記憶體緩衝區。推遲過濾器用於子請求。
子請求是請求/響應處理的非常重要的機制。子請求也是nginx最強大的方面之一。對於子請求,nginx可以從與客戶端最初請求的URL不同的URL返回結果。一些Web框架將此稱為內部重定向。但是,nginx更進一步 - 過濾器不僅可以執行多個子請求,而且可以將輸出組合成單個響應,但子請求也可以巢狀和分層。子請求可以執行其自己的子子請求,並且子子請求可以發起子子子請求。子請求可以對映到硬碟,其他處理程式或上游伺服器上的檔案。子請求對於根據原始響應中的資料插入其他內容非常有用。例如,SSI(伺服器端包含)模組使用過濾器來解析返回文件的內容,然後將include指令替換為指定URL的內容。或者,它可以是一個過濾器,將文件的整個內容視為要檢索的URL,然後將新文件附加到URL本身。
上游和負載平衡器也值得簡要描述。上游用於實現可以被識別為內容處理程式的內容,該內容處理程式是反向代理(proxy_pass處理程式)。上游模組主要準備將請求傳送到上游伺服器(或“後端”)並從上游伺服器接收響應。這裡沒有呼叫輸出過濾器。當上遊伺服器準備好被寫入和讀取時,上游模組確切地做的是設定要呼叫的回撥。存在實現以下功能的回撥:
負載平衡器模組連線到proxy_pass處理程式,以便在多個上游伺服器符合條件時提供選擇上游伺服器的功能。負載均衡器註冊啟用配置檔案指令,提供額外的上游初始化函式(以解析DNS中的上游名稱等),初始化連線結構,決定在何處路由請求以及更新統計資訊。目前,nginx支援兩種標準規則,用於對上游伺服器進行負載均衡:迴圈和ip-hash。
上游和負載平衡處理機制包括用於檢測失敗的上游伺服器以及將新請求重新路由到其餘伺服器的演算法 - 儘管計劃進行大量額外工作以增強此功能。通常,計劃對負載平衡器進行更多的工作,並且在下一版本的nginx中,將大大改進跨不同上游伺服器分配負載以及執行狀況檢查的機制。
還有一些其他有趣的模組提供了一組額外的變數供配置檔案使用。雖然nginx中的變數是在不同模組之間建立和更新的,但有兩個模組完全專用於變數:geo和map。地理模組用於根據客戶端的IP地址進行跟蹤。此模組可以建立依賴於客戶端IP地址的任意變數。另一個模組map允許從其他變數建立變數,實質上提供了對主機名和其他執行時變數進行靈活對映的能力。這種模組可以稱為變數處理程式。
在一個nginx工作者中實現的記憶體分配機制在某種程度上受到了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花了很多時間開發初始程式碼結構並研究為各種作業系統優化程式碼的不同方法。十年後,他正在開發nginx版本2.0的原型,考慮到版本1的多年積極開發。很明顯,新架構的初始原型和初始程式碼結構對於未來的重要性是非常重要的。一個軟體產品。
值得一提的另一點是發展應該集中。 Windows版本的nginx可能是一個很好的例子,說明如何避免在既不是開發人員的核心競爭力或目標應用程式的情況下稀釋開發工作。它同樣適用於重寫引擎,該引擎在多次嘗試增強nginx時出現,具有更多功能,以便與現有的舊設定向後相容。
最後但同樣重要的是,值得一提的是,儘管nginx開發者社群不是很大,但nginx的第三方模組和擴充套件一直是其受歡迎程度的重要組成部分。 Evan Miller,Piotr Sikora,Valery Kholodkov,Zhang Yichun(agentzh)以及其他才華橫溢的軟體工程師所做的工作得到了nginx使用者社群及其原始開發人員的讚賞。