瞭解 Nginx server 和 location 塊選擇演算法

chuoke發表於2019-12-23

瞭解 Nginx server 和 location 塊選擇演算法

 原文:Understanding Nginx Server and Location Block Selection Algorithms | DigitalOcean

 介紹

Nginx是世界上最受歡迎的 Web 伺服器之一。它可以通過許多併發客戶端連線成功處理高負載,並且可以輕鬆地用作 Web 伺服器,郵件伺服器或反向代理伺服器。

在本指南中,我們將討論一些幕後細節,這些細節決定了 Nginx 如何處理客戶端請求。理解這些想法可以幫助您避免設計 server 和 location 塊的猜測,並使請求處理看起來更加可預測。

 Nginx 塊配置

Nginx 在邏輯上將旨在提供不同內容的配置劃分為多個塊,這些塊以分層結構存在。每次發出客戶端請求時,Nginx 都會開始確定應使用哪些配置塊來處理請求的過程。我們將在本指南中討論這個決策過程。

我們將討論的主要塊是 server 塊和 location 塊。

server 塊是 Nginx 配置的子集,該配置定義用於處理已定義型別的請求的虛擬伺服器。管理員通常配置多個 server 塊,然後根據請求的域名,埠和 IP 地址決定哪個塊應處理哪個連線。

location 塊位於 server 塊內,用於定義 Nginx 應如何處理對父伺服器的不同資源和 URI 的請求。可以使用管理員喜歡的任何方式細分 URI 空間。這是一個非常靈活的模型。

 Nginx 如何決定哪個伺服器塊將處理請求

由於 Nginx 允許管理員定義充當獨立虛擬 Web 伺服器例項的多個 server 塊,因此需要一個過程來確定將使用這些 server 塊中的哪個來滿足請求。

它通過用於確定最佳匹配的已定義檢查系統來完成此任務。Nginx 在此過程中關注的主要 server 塊指令是 listen 指令和 server_name 指令。

 解析 “listen” 指令以查詢可能的匹配項

首先,Nginx 檢視請求的 IP 地址和埠。它將其與 listen 每個伺服器的指令相匹配,以構建可能解決請求的伺服器塊列表。

該 listen 指令通常定義 server 塊將響應的 IP 地址和埠。預設情況下,任何不包含 listen 指令的伺服器塊都被賦予 0.0.0.0:80(或 0.0.0.0:8080 如果 Nginx 由普通的非 root 使用者執行的)偵聽引數。這允許這些塊響應埠 80 上任何介面上的請求,但是此預設值在伺服器選擇過程中沒有太大的作用。

該 listen 指令可以設定為:

- IP 地址/埠組合。

- 一個單獨的 IP 地址,它將在預設埠 80 上監聽。

- 一個單獨的埠,它將偵聽該埠上的每個介面。

- Unix 套接字的路徑。

最後一個選項通常僅在不同伺服器之間傳遞請求時才有意義。

在嘗試確定將請求傳送到哪個 server 塊時,Nginx 將首先嚐試 listen 使用以下規則根據指令的特殊性來決定:

- Nginx  listen 通過將缺失值替換為其預設值來轉換所有“不完整”的指令,以便可以通過其 IP 地址和埠來評估每個塊。這些翻譯的一些示例是:

  - 沒有 listen 指令的塊使用 0.0.0.0:80

  - 設定為 111.111.111.111 沒有埠的 IP 地址的塊將變為 111.111.111.111:80

  - 設定為 8888 沒有 IP 地址的埠的塊將變為 0.0.0.0:8888

- 然後,Nginx 嘗試根據 IP 地址和埠來收集最符合請求的 server 塊列表。這意味著 0.0.0.0 如果存在列出特定 IP 地址的匹配塊,則不會選擇任何功能上用作其 IP 地址(與任何介面匹配)的塊。無論如何,埠必須完全匹配。

- 如果只有一個最具體的匹配項,則將使用該伺服器塊來滿足請求。如果有多個伺服器塊具有相同的特異性匹配級別,Nginx 則開始評估每個 server 塊的 server_name 指令。

重要的是要理解,Nginx 僅 server_name 在需要區分與 listen 指令中特定級別的匹配的 server 塊時才評估指令。例如,如果 example.com 託管在 192.168.1.10 的埠 80 上,example.com 則在本示例中,儘管 server_name 第二個塊中有指令,但對第一個塊的請求始終會得到滿足。

server {
    listen 192.168.1.10;

    . . .
}

server {
    listen 80;

    server_name example.com;

    . . .
}

如果多個 server 塊以相同的特異性匹配,則下一步是檢查 server_name 指令。

 解析 “server_name” 指令以選擇匹配項

接下來,為了進一步評估具有相同特定 listen 指令的請求,Nginx 檢查請求的 “Host” t頭。此值儲存客戶端實際嘗試訪問的域名或 IP 地址。

Nginx 嘗試通過檢視 server_name 仍然是選擇候選物件的每個伺服器塊中的指令來找到與其找到的值的最佳匹配。Nginx 使用以下規則評估這些值:

- Nginx 將首先嚐試用一個準確匹配到請求中的 “Header” 頭的 server_name 來找到一個 server 模組。如果找到了,相關的塊將被用來服務請求。如果找到多個完全匹配,則使用第一個。

- 如果未找到完全匹配的內容,則 Nginx 將嘗試  server_name 使用前導萬用字元(* 在配置中名稱開頭由 a 表示)找到具有匹配的 server 塊。如果找到一個,該塊將用於處理請求。如果找到多個匹配項,則最長的匹配項將用於處理請求。

- 如果使用前導萬用字元未找到匹配項,Nginx 之後用server_name 使用尾隨萬用字元(由*config 中以 a 結尾的伺服器名稱表示)查詢與匹配的 server 塊。如果找到一個,則使用該塊來處理請求。如果找到多個匹配項,則最長的匹配項將用於處理請求。

- 如果使用結尾的萬用字元未找到匹配項,則 Nginx 評估 server_name 使用正規表示式定義的伺服器塊(~名稱前用 a 表示)。一個 server_name 帶有與 “ Host” 頭匹配的正規表示式的內容將用於處理請求。

- 如果找不到正規表示式匹配項,則 Nginx 為該IP地址和埠選擇預設伺服器塊。

每個 IP 地址/埠組合都有一個預設 server 塊,當無法通過上述方法確定操作過程時,將使用該預設伺服器塊。對於 IP 地址/埠組合,這將是配置中的第一個塊,或者是包含該 default_server 選項作為 listen 指令一部分的塊(它將覆蓋第一個發現的演算法)。default_server 每個 IP 地址/埠組合只能有一個宣告。

 例子

如果存在 server_name 與 “Host” 頭值完全匹配的定義,則選擇該伺服器塊來處理請求。

在此示例中,如果請求的 “Host” 頭設定為 “ host1.example.com”,則將選擇第二臺伺服器:

server {
    listen 80;

    server_name *.example.com;

    . . .
}

server {
    listen 80;

    server_name host1.example.com;

    . . .
}

如果找不到完全匹配的內容,則 Nginx 然後檢查是否存在 server_name 帶有合適的起始萬用字元。以萬用字元開頭的最長匹配將被選擇來滿足請求。

在此示例中,如果請求的 “Host” 頭為 “ www.example.org ”,則將選擇第二個伺服器塊:

server {
    listen 80;

    server_name www.example.*;

    . . .

}

server {
    listen 80;

    server_name *.example.org;

    . . .
}

server {
    listen 80;

    server_name *.org;

    . . .
}

如果找不到與起始萬用字元匹配的內容,則 Nginx 將在表示式末尾使用萬用字元檢視是否存在匹配項。此時,將選擇以萬用字元結尾的最長匹配來滿足請求。

例如,如果請求的 “ Host” 標頭設定為 “ www.example.com ”,則將選擇第三個伺服器塊:

server {
    listen 80;

    server_name host1.example.com;

    . . .
}

server {
    listen 80;

    server_name example.com;

    . . .
}

server {
    listen 80;

    server_name www.example.*;

    . . .
}

如果找不到萬用字元匹配項,則 Nginx 將繼續嘗試匹配 server_name 使用正規表示式的指令。所述第一匹配正規表示式將被選擇,以響應該請求。

例如,如果請求的 “Host” 標頭設定為 “ www.example.com ”,則將選擇第二個伺服器塊以滿足請求:


server {
    listen 80;

    server_name example.com;

    . . .
}

server {
    listen 80;

    server_name ~^(www|host1).*.example.com$;

    . . .
}

server {
    listen 80;

    server_name ~^(subdomain|set|www|host1).*.example.com$;

    . . .
}

如果以上步驟均不能滿足請求,則該請求將被傳遞到預設伺服器以獲取匹配的 IP 地址和埠。

 匹配 location 塊

與 Nginx 用於選擇將處理請求的伺服器塊的過程類似,Nginx 也具有確定的演算法,用於確定 server 中的哪個 location 塊用於處理請求。

 location 塊語法

在介紹 Nginx 如何決定使用哪個 location 塊來處理請求之前,讓我們回顧一下您可能在 location 塊定義中看到的一些語法。location 塊位於 server 塊(或其他位置塊)中,並用於決定如何處理請求 URI(請求名稱中位於域名或 IP 地址/埠之後的部分)。

location 塊通常採用以下形式:

location optional_modifier location_match {
    . . .
}

在上面的 location_match 定義了 Nginx 應該針對什麼樣的請求 URI 檢查。上例中修飾符的存在或不存在會影響 Nginx 嘗試匹配 location 塊的方式。下面的修飾符將導致關聯的 location 塊的解釋如下:

(無):如果不存在修飾符,則該位置將解釋為字首匹配。這意味著給定的 location 將與請求 URI 的開頭進行匹配以確定匹配。

=:如果使用等號,則如果請求URI完全匹配給定的位置,則此塊將被視為匹配。

~:如果存在波浪號修飾符,則此位置將被解釋為區分大小寫的正規表示式匹配。

- *`~`**:如果使用了波浪號和星號修飾符,則位置塊將被解釋為不區分大小寫的正規表示式匹配。

^~:如果存在克拉和波浪號修飾符,並且選擇了該塊作為最佳非正規表示式匹配項,則不會發生正規表示式匹配項。

 展示 location 塊語法的示例

作為字首匹配的一個例子,以下 location 塊可以被選擇為響應於請求的 URI 的樣子 /site/site/page1/index.html 或 /site/index.html

location /site {
    . . .
}

為了演示精確的請求 URI 匹配,將始終使用此塊來響應看起來像的請求 URI  /page1。它不會用於響應 /page1/index.html 請求 URI 。請記住,如果選擇此塊並且使用索引頁滿足了請求,則將進行內部重定向到另一個位置,該位置將是請求的實際處理程式:

location = /page1 {
    . . .
}

作為應解釋為區分大小寫的正規表示式的位置的示例,此塊可用於處理對 /tortoise.jpg 的請求 ,但不能用於 /FLOWER.PNG :

location ~ .(jpe?g|png|gif|ico)$ {
    . . .
}

下面顯示了一個類似於上述的不區分大小寫的匹配塊。在這裡,無論是 /tortoise.jpg    /FLOWER.PNG 都可以通過此塊處理:

location ~* .(jpe?g|png|gif|ico)$ {
    . . .
}

最後,如果確定為最佳非正規表示式匹配,則此塊將防止正規表示式匹配發生。它可以處理以下請求 /costumes/ninja.html

location ^~ /costumes {
    . . .
}

如您所見,修飾符指示應如何解釋 location 塊。但是,這並沒有告訴我們 Nginx 用於確定將請求傳送到哪個 location 塊的演算法。接下來,我們將繼續討論。

 Nginx 如何選擇用於處理請求的 Location

Nginx 以與選擇 server 塊類似的方式選擇將用於服務請求的 location。它貫穿一個過程,該過程為任何給定請求確定最佳 location 塊。瞭解此過程是能夠可靠,準確地配置 Nginx 的關鍵要求。

考慮到我們上面描述的 location 宣告的型別,Nginx 通過將請求 URI 與每個 location 進行比較來評估可能的 location 上下文。它使用以下演算法執行此操作:

- Nginx 首先檢查所有基於字首的 location 匹配(所有不涉及正規表示式的 location 型別)。它根據完整的請求 URI 檢查每個 location。

- 首先,Nginx 尋找完全匹配。如果發現使用 = 修飾符的 location 塊與請求 URI 完全匹配,則立即選擇該位置塊來處理請求。

- 如果沒有找到精確的(帶有 = 修飾符)location 塊匹配,則 Nginx 繼續評估不精確的字首。它發現給定請求 URI 的最長匹配字首 location,然後對其求值如下:

  - 如果最長的匹配字首 location 具有 ^~ 修飾符,則 Nginx 將立即結束其搜尋並選擇此 location 來滿足請求。

  - 如果最長的匹配字首 location 使用 ^~ 修飾符,則該匹配項暫時由 Nginx 儲存,以便可以移動搜尋的焦點。

- 確定並儲存了最長的匹配字首 location 後,Nginx 繼續評估正規表示式 location(區分大小寫和不區分大小寫)。如果有任何的正規表示式的 location 包括  在最長字首匹配的 location 裡,Nginx 會將這些移動到它的正規表示式的 location 列表的頂部進行檢查。然後,Nginx 嘗試順序匹配正規表示式 location。第一個匹配上請求 URI 的正規表示式的 location 被立即選擇來服務該請求。

- 如果未找到與請求 URI 匹配的正規表示式的 location,則選擇先前儲存的字首 location 來處理請求。

重要的是要了解,預設情況下,Nginx 將優先於字首匹配來提供正規表示式匹配。但是,它首先評估字首 location,從而允許管理員通過使用 = 和 ^~ 修飾符指定 location 來覆蓋此趨勢。

同樣重要的是要注意,儘管字首 location 通常是根據最長,最具體的匹配選擇的,但是當找到第一個匹配的 location 時,正規表示式評估就會停止。這意味著配置中的定位對於正規表示式 location 具有廣泛的意義。

最後,重要的是要理解,當 Nginx 評估正規表示式 location 時,最長字首匹配的正規表示式匹配將 “插隊”。在考慮其他任何正規表示式匹配項之前,將按順序評估這些值。一個非常樂於助人的 Nginx 開發人員 Maxim Dounin 在這篇文章解釋了選擇演算法的這一部分。  

 Location 塊評估何時會跳轉到其他 Locations?

一般而言,當選擇 location 塊來服務請求時,從該點開始,將在該上下文中完全處理該請求。只有選定的 location 和繼承的指令才能確定如何處理請求,而不會受到同級 location 塊的干擾。

儘管這是一條通用規則,可讓您以可預測的方式設計 location 塊,但重要的是要意識到,有時某些 location 中的某些指令會觸發新的 location 搜尋。“僅一個 location 塊” 規則的例外可能會對請求的實際服務方式產生影響,並且可能與您在設計 location 塊時的期望不一致。

可能導致這種內部重定向的一些指令是:

index

try_files

rewrite

error_page

讓我們簡要介紹一下。

如果 index 偽指令用於處理請求,則始終會導致內部重定向。精確的 location 匹配通常用於通過立即結束演算法的執行來加快選擇過程。但是,如果您將一個精確的 location 匹配作為一個目錄,則很有可能將請求重定向到其他 location 以進行實際處理。

在此示例中,第一個 location 與請求 URI 匹配 /exact,但是為了處理請求,index 該塊繼承的指令將內部重定向啟動到第二個塊:

index index.html;

location = /exact {
    . . .
}

location / {
    . . .
}

在上述情況下,如果您確實需要將執行保留在第一個塊中,則必須提出另一種方法來滿足對目錄的請求。例如,您可以 index 為該塊設定一個無效的並開啟 autoindex

location = /exact {
    index nothing_will_match;

    autoindex on;
}

location  / {
    . . .
}

這是防止 index 上下文切換的一種方法,但對於大多數配置而言可能沒有用。通常,目錄上的完全匹配對於諸如 rewrite 請求(這也會導致新的 location 搜尋)之類的事情很有幫助。

可以重新評估處理 location 的另一個例項是 try_files 指令。該指令告訴 Nginx 檢查是否存在一組命名的檔案或目錄。最後一個引數可以是 Nginx 將對其進行內部重定向的 URI。

考慮以下配置:

root /var/www/main;

location / {
    try_files $uri $uri.html $uri/ /fallback/index.html;
}

location /fallback {
    root /var/www/another;
}

在上面的示例中,如果請求 /blahblah,則第一個 location 將首先獲得該請求。它將嘗試 在 /var/www/main 目錄中找到叫 blahblah 的檔案。如果找不到它,它將繼續通過搜尋名為 blahblah.html 的檔案。然後,它將嘗試檢視 /var/www/main目錄中是否有一個名為 blahblah/ 的資料夾。如果所有這些嘗試均失敗,它將重定向到 /fallback/index.html。這將觸發另一個 location 搜尋,該搜尋將被第二個 location 塊捕獲。這將提供檔案 /var/www/another/fallback/index.html

可能導致 location 被忽略的另一個指令是 rewrite 指令。當將 last 引數與 rewrite 指令一起使用時,或者根本不使用任何引數時,Nginx 將根據 rewrite 結果搜尋新的匹配 location。

例如,如果我們修改最後一個示例以包括 rewrite,則可以看到有時將請求直接傳遞到第二個 location,而無需依賴 try_files 指令:

root /var/www/main;

location / {
    rewrite ^/rewriteme/(.*)$ /$1 last;

    try_files $uri $uri.html $uri/ /fallback/index.html;
}

location /fallback {
    root /var/www/another;
}

在上面的示例中,/rewriteme/hello 最初的請求將由第一個 location 塊處理。它將被重寫為 /hello 並搜尋 location。在這種情況下,它將再次匹配第一個 location,並且 try_files 照常進行處理, 如果未找到任何內容,則可能會跳回 /fallback/index.html(使用try_files上面討論的內部重定向)。

但是,如果請求 /rewriteme/fallback/hello,則第一個塊將再次匹配。重寫將再次應用,這一次導致以 /fallback/hello 為結果。然後,將在第二個 location 塊之外提供該請求。

return 傳送 301 或 302 狀態程式碼時,該指令會發生相關情況。在這種情況下的區別在於,它以外部可見重定向的形式導致了一個全新的請求。rewrite 當使用 redirect 或者  permanent 標誌時,指令也會發生這種情況。但是,這些 location 搜尋不應意外,因為外部可見的重定向始終會導致新的請求。

該 error_page 指令可以導致內部重定向,類似於由建立的重定向 try_files。此偽指令用於定義遇到某些狀態程式碼時應發生的情況。如果 try_files 已設定,則可能永遠不會執行此操作,因為該指令可處理請求的整個生命週期。

考慮以下示例:

root /var/www/main;

location / {
    error_page 404 /another/whoops.html;
}

location /another {
    root /var/www;
}

每個請求(以開頭/another的請求除外)都將由第一個塊處理,該第一個塊將處理中的檔案 /var/www/main。但是,如果未找到檔案(狀態為 404),/another/whoops.html 則將發生內部重定向到,從而導致新的 location 搜尋,該搜尋最終將落在第二個塊上。該檔案將送出 /var/www/another/whoops.html

如您所見,瞭解 Nginx 觸發新 location 搜尋的情況可以幫助您預測發出請求時的行為。

 結論

瞭解 Nginx 處理客戶端請求的方式可以使您作為管理員的工作更加輕鬆。您將能夠知道 Nginx 將根據每個客戶端請求選擇哪個 server 塊。您還將能夠知道如何根據請求 URI 選擇 location 塊。總體而言,瞭解 Nginx 選擇不同塊的方式將使您能夠跟蹤 Nginx 為服務每個請求而應用的上下文。

 原文:Understanding Nginx Server and Location Block Selection Algorithms | DigitalOcean

初出茅廬,一知半解,望有識之士多多指教。抱拳...

相關文章