簡介
Nginx 是世界上最受歡迎的網頁伺服器選擇之一。它能夠成功處理大量的同時用戶端連線。同時,它也可以作為郵件、網頁或反向代理伺服器運作。
本指南旨在概述引導 Nginx 如何處理用戶端請求的幕後方法。我們將揭開伺服器與位置 區塊 設計的神秘面紗,並解釋如何最好地減少處理請求時看似不可預測的情況。
首先,這裡有一份關於如何在您的 Ubuntu 伺服器上安裝 Nginx 的完整教學。現在,讓我們開始吧!
使用 Nginx 進行區塊設定
Nginx 的邏輯方法涉及將針對不同用途的設定分類到獨立且更具邏輯性的內容區塊中。這些區塊將存在於階層式結構中。當用戶端發出請求時,Nginx 會啟動一個程序,藉此決定哪些設定區塊最適合用來處理該請求。我們將專注於這個決策過程。
我們將討論的主要區塊為 server 區塊與 location 區塊。Server 區塊是 Nginx 建立的設定子集,用於定義哪個虛擬伺服器將負責處理特定類型的請求。它們最常基於傳入請求的 IP 位址、網域名稱或連接埠。管理員會設定多個 server 區塊。然後,他們需要決定應該由哪個連線來處理該請求。
Location 區塊位於 server 區塊之內。這些是決定如何以及可以使用哪些資源來處理傳入其特定父伺服器之請求的決策者。此模型非常靈活。URI 空間可以設定為以管理員認為最佳的任何方式來使用這些區塊。
使用 Nginx 決定哪個區塊將處理哪個請求
Nginx 允許定義多個 server 區塊。它們全部都作為不同的虛擬網頁伺服器運作。因此,需要有一種方法來劃分哪個伺服器將處理特定的傳入請求。這是透過定義好的檢查系統來尋找最適合請求效能的配置來實現的。
Nginx 主要處理兩個主要的 server 區塊指令:listen 與 server_name.
使用「Listen」指令尋找可能的匹配項
Nginx 首先評估的是請求的連接埠和 IP 位址。然後,它會將其與每個伺服器的 listen 指令進行比對。對伺服器列表的這種解析有助於篩選出僅能解決該請求的 server 區塊。
通常,listen 指令會定義特定 server 區塊負責回應的連接埠和 IP 位址。未包含 listen 指令的 server 區塊預設會接收 0.0.0.0:80 的監聽參數。如果 Nginx 是由一般的非 root 使用者執行,則 listen 參數會定義為 0.0.0.0:8080。這意味著無論介面為何,如果區塊來自連接埠 80,以此方式定義的區塊將會對其進行回應。然而,在選擇伺服器的過程中,此預設值的權重並不高。
您可以將 listen 指令設定為:
- 在預設連接埠 (80) 上監聽請求的單一 IP 位址。
- 監聽該連接埠上任何介面的單一連接埠。
- 連接埠與 IP 位址的組合。
- 設定的 Unix socket 路徑(此選項僅在請求跨越不同伺服器傳遞時才會有影響)。
Nginx 在決定將請求傳送到哪個 server 區塊時,將會執行一組規則。這些規則取決於 listen 指令的特定設定。它們如下:
- 如果 listen 指令不完整,缺失的部分將取得其預設值。這意味著 IP 位址和連接埠將被強制補全為預設值,以便處理請求。
- 在這種情況下,不包含 listen 指令的區塊將使用預設值 0.0.0.0:80。
- 缺少連接埠且 IP 地址為 111.111.111.111 的區塊將會變成 111.111.111.111:80。
- 當沒有 IP 地址時,具有連接埠 8888 的區塊將獲取預設的 IP 地址並附加其後,以建立 0.0.0.0:8888。
- 在確定 IP 地址和連接埠後,Nginx 接著會尋找與該連接埠相匹配的伺服器區塊。
- 如果只找到一個特定的匹配項,這就是該伺服器區塊。如果有多個符合條件的區塊,Nginx 將會求助於 server_name 指令,以進一步篩選出具體的目標伺服器區塊。
只有在無法從 listen 指令中找到具有精確匹配特異性級別的伺服器區塊時,Nginx 才會評估 server_name 指令。如果 example.com 位於連接埠 80 且 IP 為 192.168.1.10,則此範例中的第一個區塊將始終是處理該請求的區塊。無論 server_name 指令的內容為何,情況都是如此:

如果有多個符合條件且特異性匹配的伺服器區塊,則會將 server_name 指令納入考量。
使用「Server_Name」指令尋找可能的匹配項
如果 listen 指令的特異性相同,Nginx 將檢查請求’的「Host」標頭。此值最初會包含用戶端想要存取的網域 IP。Nginx 將利用每個仍符合條件的候選伺服器區塊內部的 server_name 指令。它會根據一個公式進行這些評估,公式如下:
- Nginx 的首次嘗試將是識別一個其 server_name 與請求中的「Host」標頭值完全匹配的區塊。如果找到了,包含該精確匹配項的區塊將是用於處理該請求的區塊。如果找到多個區塊,它將選擇清單中的第一個。
- 如果沒有精確匹配項,Nginx 接著會嘗試使用 server_name,透過在設定中的伺服器區塊名稱開頭使用萬用字元 * 來尋找匹配的伺服器區塊。使用此方法找到一個區塊即表示已確定該伺服器區塊。如果找到多個匹配項,則將由最長匹配的區塊來處理該請求。
- 如果沒有匹配的前置萬用字元,Nginx 將嘗試尋找具有匹配後置萬用字元的伺服器區塊。換句話說,這將是設定中結尾帶有 * 的伺服器名稱。如果找到一個,它將用於該請求。而如果找到多個,Nginx 將再次使用最長的匹配項。
- 如果在兩次萬用字元嘗試後仍無匹配項,Nginx 將評估那些使用正規表示式(在名稱前以 ~ 表示)定義 server_name 的伺服器區塊。第一個其運算式與「Host」標頭匹配的 server_name 實例,將被視為處理該請求的伺服器區塊。
- 如果此時仍無匹配項,Nginx 將使用該連接埠和 IP 地址組合的預設伺服器區塊。
每個連接埠/IP 地址組合都將有一個指定的伺服器區塊。如果確定用於處理請求的合適伺服器區塊的規則無效,則將使用它。這將是設定中第一個在 listen 指令中包含 default_server 選項的區塊(它將覆蓋最初找到的演算法)。每個 IP 地址/連接埠組合最多只能有一個 default_server 設定。
伺服器區塊選擇範例
如果定義的 server_name 與「Host」標頭值完全匹配,它將是選擇用於請求處理的伺服器區塊。以下範例顯示請求的「Host」標頭被指定為「host1.example.com」。在這種情況下,它將選擇第二個伺服器:

在沒有精確匹配的情況下,Nginx 將檢查是否存在帶有萬用字元的 server_name。如果不存在,將選擇以萬用字元開頭的最長匹配項。在以下內容中,「www.example.org」位於「Host」標頭中。這意味著它將選擇第二個區塊:

若沒有以萬用字元開頭的相符項,Nginx 會繼續進行結尾萬用字元檢查。系統將選擇以該萬用字元結尾的最長相符項來處理請求。在此範例中,「Host」標頭為「www.example.com」,因此它將選擇第三個 server 區塊:

如果仍然沒有相符項,Nginx 將嘗試使用標準運算式來比對 server_name 指令。系統會選擇這些運算式中的第一個來處理請求。如果「Host」為「www.example.com」,則會選擇第二個 server 區塊來處理該請求:

若仍然沒有相符項,請求將發送至設定了相符預設伺服器的 IP 位址與連接埠組合。
Location 區塊比對
Nginx 還需要建立一個演算法,用以決定伺服器上的哪一個 location 區塊將負責回應請求。
Location 區塊的語法
在解釋 Nginx 如何決定指定哪個 location 區塊來處理請求之前,我們將先複習 location 區塊定義中的語法。如前所述,location 區塊位於 server 區塊(以及其他 location 區塊)之內。其目的是決定如何處理請求的 URI。URI 是請求中位於 IP 位址與連接埠或網域名稱之後的部分。
Location 區塊通常如下所示:

Nginx 會根據 location_match 檢查請求的 URI。不論是否存在上述修飾符,都會決定 Nginx 嘗試比對區塊的方式。根據修飾符的不同,location 區塊將依據以下規則進行解析:
- 無修飾符: 在沒有任何修飾符的情況下,location 將被解析為字首比對。這意味著所提供的 location 將與請求 URI 的開頭進行比對,以確定是否正確相符。
- =: 等號表示只要請求的 URI 與提供的 location 完全相符,該區塊就會被視為相符項。
- ~: 波浪號修飾符表示 location 區塊的比對將區分大小寫。
- ~*: 波浪號與星號的組合修飾符表示 location 區塊在尋找相符項時將不區分大小寫。
- ^~: 如果波浪號修飾符前面有脫字號,只要此區塊被選為最佳非正規表示式相符項,就不會進行正規表示式比對。
Location 區塊語法範例
為了展示字首比對的範例,此 location 區塊將被選中以回應格式為 /site、/site/page1/index.html 或 /site/index/html 的請求 URI:

為了示範精確的 URI 比對,此區塊將始終用於回應格式為 /page1 的 URI 請求,而非 /page1/index.html 請求 URI。如果選中了此區塊,且它使用索引頁面滿足了請求,則請求的實際處理程序將在內部重新導向到另一個 location:

例如,對於必須使用區分大小寫的運算式來解析的 location,以下區塊無法處理 /FLOWER.PNG 的請求。然而,它將處理 /tortoise.jpg 的請求:

接下來,觀察一個允許不區分大小寫比對的區塊,它與上面的區塊類似。在這種情況下,該區塊可以同時處理 //tortoise.jpg 與 /FLOWER.PNG:

最後一種變體是,如果確定某個區塊是最佳的非正規表示式相符項,該區塊將阻止進行正規表示式比對。此區塊可以處理 /costumes/ninja.html 的請求:

具體來說,修飾符決定了 location 區塊的判定方式。然而,這並沒有告訴我們 Nginx 使用什麼決策演算法來識別請求應發送至哪個 location 區塊。我們接下來將探討這個問題。
選擇將由 Nginx 處理請求的 Location
Nginx 選擇處理請求的 location 的方法與選擇 server 區塊的方式類似。換句話說,它透過執行一個流程來為每個請求確定最佳的 location。為了準確且相應地設定 Nginx,您必須了解這個流程。
記住前面提到的 location 宣告,Nginx 同樣透過將來自給定請求的 URI 與每個 location 進行比較,來檢查每個 location 的資格,從而使用潛在的 location 上下文。在此過程中,它應用以下演算法:
- 首先,Nginx 檢查所有不包含正規表示式的 location 類型。它透過尋找所有基於 location 的前綴比對來做到這一點。為此,它會根據請求的完整 URI 檢查 location。
- Nginx 開始尋找精確比對。一旦識別出使用 = 修飾符的 location 區塊,就會將其與 URI 請求進行比較。如果兩者完全匹配,則會立即選擇該 location 區塊來處理該請求。
- 如果沒有完全符合 = 修飾符比較的 location,Nginx 將繼續評估非精確的前綴。一旦確定了與請求 URI 匹配的最長前綴 location,它將進行以下評估:
- 如果具有最長前綴比對的 location 使用了 ^~ 修飾符,則會立即選擇此 location。
- 如果具有最長前綴的 location 未使用 ^~ 修飾符,則 Nginx 會暫時保留該比對,以便將搜尋焦點轉移。
- 一旦找到並儲存了最長前綴 location 比對,Nginx 就會轉向評估正規表示式 location。這些包括區分大小寫和不區分大小寫的比對。如果最長比對的前綴 location 中包含任何正規表示式 location,Nginx 將重新調整列表,將這些位置放在 location 列表的頂部附近。重新排序的列表中第一個與請求 URI 匹配的表示式,將是選擇用來處理該請求的 location。
- 如果沒有找到滿足請求 URI 的正規表示式,則會選擇先前儲存的 location 來處理該請求。
預設情況下,Nginx 將正規表示式比對的優先級置於優先前綴比對之上。然而,它確實會先評估前綴 location,以便管理人員可以使用 = 和 ^~ 修飾符來覆蓋此傾向。
另一個重要的重點是,雖然前綴 location 通常基於最特定、最長的比對,但一旦識別出第一個比對,正規表示式檢查就會停止。這意味著設定檔中的位置對於正規表示式 location 具有實際影響。
最後需要提到的一點是,在 Nginx 的 location 評估過程中,最長前綴比對中的正規表示式比對基本上會「插隊」。這些將被放置在列表的頂部,並在其他正規表示式之前進行評估。
在 Location 區塊評估中,何時會跳轉到其他 Location?
通常,一旦評估了請求並選擇了處理該請求的 location 區塊,它將完全在該上下文中進行處理。這意味著只有繼承的指令和選定的 location 才是處理請求的決定因素,而不需要任何同級 location 區塊的輸入。
雖然這是一個允許對 location 區塊進行可預測設計的一般原則,但有時 location 內部的某些指令也可以觸發新的搜尋。換句話說,「僅一個 location 區塊」的規則有一些例外。這些例外可能與 location 區塊的預期不符。因此,它們可能無法按預期處理請求。
這些內部重新導向最終可能會由於某些指令而顯現,包括:
- index
- rewrite
- error_page
- try_files
如果您使用 index 指令,在處理請求時它總是會導致內部重定向。雖然尋找位置匹配通常會結束演算法執行以加快選擇過程,但如果找到的位置匹配是一個目錄,該請求很可能會被重定向到另一個位置以進行正式處理。
例如,以下第一個位置與請求 URI /exact 匹配。然而,為了處理該請求,位置區塊繼承的 index 指令會將請求重定向到第二個區塊:

對於這種情況,如果執行需要保留在主要區塊內,則需要另一種方案來處理對該目錄的請求。一種方法是為該區塊設定一個無效的 index,並改為啟用 auto index:

雖然這種方法在少數情況下可能有效,但在大多數情況下大體上並不實用。精確的目錄匹配對於需要重寫請求的情況非常有用。這將觸發全新的位置搜尋。
另一個可用於重新評估處理位置的指令是 try_files 指令。它指示 Nginx 專門檢查指定的一組檔案或目錄是否存在,最後一個搜尋條件是 Nginx 內部重定向的 URI。
讓我們考慮以下設定:

如果有對 /blahblah 的請求,第一個位置將接收它。在 /var/www/main 目錄中找不到 blahblah 檔案將觸發對 blahblah.html 的後續搜尋。然後它將在 /var/www/main 目錄中尋找名為 blahblah 的子目錄。如果所有這些檢查都失敗,它將重定向到 /fallback/index.html。這將觸發另一個位置搜尋,並由另一個位置區塊接管。然後,它將處理檔案 /var/www/another/fallback/index.html。
另一個會導致重定向到另一個位置區塊的指令是 rewrite 指令。當使用 last 參數時,Nginx 將根據 rewrite 指令的結果搜尋新的匹配位置。如果將上一個範例修改為包含此 rewrite 指令,顯而易見的是,無需執行 try_files 指令即可將請求重定向到另一個位置:

在此範例中,對 /rewrite/hello 的請求最初將由第一個位置處理。在被重寫為 /hello 後,將觸發第二次位置搜尋。它將與第一個位置匹配。它將由 try_file 指令處理,如果沒有命中,則可能會退回到 /fallback/index.html。
然而,如果發送對 /rewrite/fallback/hello 的請求,將會找到與第一個區塊的匹配。因此,將再次處理重寫,但這次的結果是 /fallback/hello。該請求將在另一個位置區塊上進行處理。
當您使用 return 指令傳送 301 或 302 狀態碼時,也會發生類似的情況。唯一的區別是會產生一個新請求,並表現為非常明顯的重定向。同樣地,當您套用 permanent 或 redirect 旗標時,rewrite 指令也會發生這種情況。
另一個可以導致類似於 try_again 的內部重定向的指令是 error_page 指令。當您在處理中遇到特定的錯誤碼時,可以使用它。當設定了 try_files 指令時,error_page 指令很可能永遠不會被執行。這是因為該指令將處理請求的整個生命週期。
讓我們考慮以下範例:

在這種情況下,每個請求都將由第一個區塊處理,並從 /var/www/main 提供檔案。這不適用於那些以 /another 開頭的請求。但如果找不到檔案,將會啟動內部重定向到 /another/whoops/html。這將導致另一個位置搜尋。反過來,它會將請求導向第二個區塊,並從 /var/www/another/whoops.html 處理該檔案。
顯而易見,理解 Nginx 何時會觸發新的 location 搜尋,有助於在處理請求時更好地預測系統行為。
結論
當管理員理解 Nginx 處理用戶端請求的方法時,他們的工作會變得無比簡單。這能讓管理員確定請求將進入哪個 server 區塊。他們還可以根據請求的 URI 確定將選擇哪個 location 區塊。總的來說,這也使管理員能夠在處理每個請求時,追蹤 Nginx 套用的上下文。
最後,您可以查看我們部落格上專注於 Nginx 的其他教學。它們將幫助您更好地從這款世界上最受歡迎的網頁伺服器中受益:
- 網頁伺服器的世界:Apache vs. Nginx
- 如何在 Ubuntu 20.04 上使用 Let’s Encrypt 保護 Nginx 安全
- 為 Nginx 自動執行 Let’s Encrypt SSL 憑證續期
- 使用 Docker Compose 部署 Laravel、Nginx 和 MySQL
祝您運算愉快!
留言
目前尚無留言。成為第一個留言的人吧。