理解 Nginx 服务器和位置块选择算法

介紹

Nginx 是世界上最流行的 Web 伺服器之一。它可以成功處理具有許多並發客戶端連接的高負載,並且可以作為 Web 伺服器、郵件伺服器或反向代理伺服器運作。

在本指南中,我們將討論一些決定 Nginx 如何處理客戶端請求的幕後細節。了解這些想法可以幫助排除設計伺服器和位置區塊的猜測,使請求處理看起來不那麼不可預測。

Nginx 區塊配置

Nginx 將用於提供不同內容的配置邏輯上分成區塊,這些區塊位於分層結構中。每次客戶端發送請求時,Nginx 開始決定應使用哪些配置區塊來處理該請求。這個決策過程是我們在本指南中要討論的。

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

A server block is a subset of Nginx’s configuration that defines a virtual server used to handle requests of a defined type. Administrators often configure multiple server blocks and decide which block should handle which connection based on the requested domain name, port, and IP address.

A location block lives within a server block and is used to define how Nginx should handle requests for different resources and URIs for the parent server. The URI space can be subdivided in whatever way the administrator likes using these blocks. It is an extremely flexible model.

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

由於Nginx允許管理員定義多個伺服器區塊,這些區塊可作為獨立的虛擬網頁伺服器實例,因此它需要一個程序來確定哪個伺服器區塊將用於滿足請求。

它通過一套定義好的檢查系統來實現這一點,以找到最佳匹配。Nginx在此過程中關心的主要伺服器區塊指令是listen指令和server_name指令。

解析listen指令以找到可能的匹配

首先,Nginx查看請求的IP地址和端口。它將這與每個伺服器的listen指令進行匹配,以建立一個可能解析請求的伺服器區塊列表。

listen 指令通常定义了服务器块将响应的 IP 地址和端口。默认情况下,任何不包含 listen 指令的服务器块都会被赋予 0.0.0.0:80(或者如果 Nginx 是由普通非root用户运行,则为 0.0.0.0:8080)的监听参数。这允许这些块在端口 80 上的任何接口上响应请求,但是这个默认值在服务器选择过程中并不占据很大的权重。

listen 指令可以设置为:

  • 一个 IP 地址/端口组合。
  • A lone IP address which will then listen on the default port 80.
  • A lone port which will listen to every interface on that port.
  • Unix 套接字的路径。

最后一种选项通常只在不同服务器之间传递请求时才会有影响。

在尝试确定将请求发送到哪个服务器块时,Nginx 首先会根据 listen 指令的特异性来决定,使用以下规则:

  • Nginx 通过以下规则翻译所有“不完整”的 listen 指令,通过用默认值替换缺失值,以便每个块可以通过其 IP 地址和端口进行评估。一些示例翻译如下:
    • 没有 listen 指令的块使用值 0.0.0.0:80
    • 设置为 IP 地址 111.111.111.111 且没有端口的块变为 111.111.111.111:80
    • 设置为端口 8888 且没有 IP 地址的块变为 0.0.0.0:8888
  • Nginx 然后尝试根据 IP 地址和端口收集与请求最匹配的服务器块列表。这意味着任何将 0.0.0.0 作为其 IP 地址(以匹配任何接口)的功能块,如果有列出特定 IP 地址的匹配块,则不会被选择。在任何情况下,端口必须完全匹配。
  • 如果只有一个最具体的匹配,那么该服务器块将用于提供请求。如果有多个具有相同特异性匹配级别的服务器块,则 Nginx 将开始评估每个服务器块的 server_name 指令。

重要的是要理解,当 Nginx 需要区分在 listen 指令中匹配到相同特异性水平的服务器块时,它只会评估 server_name 指令。例如,如果 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_name 指令。

解析 server_name 指令以选择匹配项

接下来,为了进一步评估具有相同特定 listen 指令的请求,Nginx 会检查请求的 Host 标头。该值保存了客户端实际尝试访问的域名或 IP 地址。

Nginx 会尝试通过查看仍然是选择候选项的每个服务器块中的 server_name 指令来找到与其找到的值最匹配的项。Nginx 使用以下公式进行评估:

  • Nginx 首先会尝试查找一个 server_name 完全匹配请求的 Host 标头中的值的服务器块。如果找到了这样的块,相关联的块将用于提供请求。如果找到多个完全匹配项,则使用第一个匹配项。
  • 如果没有找到完全匹配项,Nginx 然后会尝试查找一个使用前置通配符匹配的 server_name 的服务器块(在配置中以 * 开头)。如果找到了一个,则将使用该块来提供请求。如果找到多个匹配项,则使用最长匹配项来提供请求。
  • 如果没有使用前置通配符找到匹配项,Nginx 然后会查找一个使用尾部通配符匹配的 server_name 的服务器块(在配置中以 * 结尾)。如果找到了一个,则将使用该块来提供请求。如果找到多个匹配项,则使用最长匹配项来提供请求。
  • 如果使用尾部萬用字元找不到匹配項,則 Nginx 將評估使用正則表達式定義 server_name 的服務器塊(在名稱前面加上 ~)。具有與“Host”標頭匹配的正則表達式的第一個 server_name 將用於處理請求。
  • 如果找不到正則表達式匹配,則 Nginx 將選擇該 IP 地址和端口的默認服務器塊。

每個 IP 地址/端口組合都有一個默認服務器塊,當無法使用上述方法確定採取的操作時將使用該塊。對於 IP 地址/端口組合,這將是配置中的第一個塊,或者是包含 listen 指令的 default_server 選項的塊(這將覆蓋首次找到的算法)。每個 IP 地址/端口組合只能有一個 default_server 声明。

範例

如果定義了一個正確匹配 Host 標頭值的 server_name,則選擇該服務器塊來處理請求。

在此示例中,如果請求的 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地址和端口的默认服务器。

匹配位置块

與 Nginx 選擇處理請求的伺服器區塊相似,Nginx 也有一個確定在伺服器中使用哪個位置區塊來處理請求的建立的演算法。

位置區塊語法

在我們討論 Nginx 如何決定使用哪個位置區塊來處理請求之前,讓我們先了解一些你可能在位置區塊定義中看到的語法。位置區塊位於伺服器區塊(或其他位置區塊)中,用於決定如何處理請求 URI(請求中位於域名或 IP 位址/埠之後的部分)。

位置區塊通常具有以下形式:

location optional_modifier location_match {

    . . .

}

上面的 location_match 定義了 Nginx 應該檢查請求 URI 的位置。在上面的示例中,修改器的存在或不存在會影響 Nginx 嘗試匹配位置區塊的方式。以下修改器將導致相應的位置區塊被解釋如下:

  • (none):如果沒有修改器,則該位置將被解釋為前綴匹配。這意味著將與請求 URI 的開頭進行匹配以確定匹配。
  • =:如果使用等號,則該區塊將在請求 URI 完全匹配位置時被認為是匹配的。
  • ~:如果存在波浪线修改器,则此位置将被解释为区分大小写的正则表达式匹配。
  • ~*:如果使用波浪线和星号修饰符,则位置块将被解释为不区分大小写的正则表达式匹配。
  • ^~:如果存在插入符和波浪线修改器,并且此块被选为最佳非正则表达式匹配,则不会进行正则表达式匹配。

演示位置块语法的示例

作为前缀匹配的示例,以下位置块可能被选中以响应类似/site/site/page1/index.html/site/index.html的请求URI:

location /site {

    . . .

}

作为精确请求URI匹配的演示,此块将始终用于响应类似/page1的请求URI。 它将用于响应/page1/index.html请求URI。 请记住,如果选择了此块并且使用索引页面满足了请求,则将进行内部重定向到另一个位置,该位置将是请求的实际处理程序:

location = /page1 {

    . . .

}

作為應該被解釋為區分大小寫正則表達式的位置示例,這個區塊可以用來處理對/tortoise.jpg的請求,但不適用於/FLOWER.PNG

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

    . . .

}

A block that would allow for case-insensitive matching similar to the above is shown below. Here, both /tortoise.jpg and /FLOWER.PNG could be handled by this block:

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

    . . .

}

最後,如果確定是最佳非正則表達式匹配,此區塊將防止正則表達式匹配發生。它可以處理對/costumes/ninja.html的請求:

location ^~ /costumes {

    . . .

}

如您所見,修飾符指示位置區塊應該如何解釋。然而,這並不告訴我們Nginx用於決定將請求發送到哪個位置塊的算法。我們將在接下來進行介紹。

Nginx選擇要用來處理請求的位置的方式

Nginx選擇用於提供請求的位置的方式類似於它選擇服務器塊的方式。它運行一個流程,以確定任何給定請求的最佳位置塊。理解這個過程是能夠可靠和準確地配置Nginx的關鍵要求。

牢記我們上面描述的位置聲明類型,Nginx通過將請求URI與每個位置進行比較來評估可能的位置上下文。它使用以下算法進行此操作:

  • Nginx 開始通過檢查所有基於前綴的位置匹配(不涉及正則表達式的所有位置類型)。它對每個位置都與完整的請求 URI 進行匹配。
  • 首先,Nginx 尋找完全匹配。如果找到了使用 = 修飾符的位置塊來完全匹配請求 URI,則立即選擇此位置塊來處理請求。
  • 如果沒有找到精確(使用 = 修飾符)的位置塊匹配,則 Nginx 繼續評估非精確前綴。它會找到給定請求 URI 的最長匹配前綴位置,然後按照以下方式進行評估:
    • 如果最長匹配前綴位置有 ^~ 修飾符,那麼 Nginx 將立即結束搜索並選擇此位置來處理請求。
    • 如果最長匹配前綴位置 沒有 使用 ^~ 修飾符,則 Nginx 會暫時將匹配存儲,以便可以轉移搜索的焦點。
  • 在确定并存储了最长匹配前缀位置之后,Nginx继续评估正则表达式位置(区分大小写和不区分大小写均包括在内)。如果在最长匹配前缀位置之内存在任何正则表达式位置,Nginx将会将这些位置移动到其正则表达式位置检查列表的顶部。然后,Nginx依次尝试与正则表达式位置进行匹配。选择服务请求的是第一个与请求URI匹配的正则表达式位置。
  • 如果找不到与请求URI匹配的正则表达式位置,则将之前存储的前缀位置选定为服务请求的位置。

重要的是要理解,默认情况下,Nginx会优先服务于正则表达式匹配而不是前缀匹配。但是,它首先评估前缀位置,使管理员可以通过使用“=”和“^~”修饰符来覆盖这种倾向。

另外值得注意的是,尽管前缀位置通常基于最长、最具体的匹配进行选择,但是在找到第一个匹配位置时,正则表达式评估将会停止。这意味着配置中的位置安排对于正则表达式位置具有广泛的影响。

最後,重要的是要了解,當 Nginx 評估正則表達式位置時,正則表達式匹配在最長前綴匹配內將“跳過行”。這些將按順序進行評估,然後才考慮任何其他正則表達式匹配。極大地幫助了 Nginx 開發人員 Maxim Dounin 在這篇文章中解釋了這部分的選擇算法。

何時位置塊評估會跳到其他位置?

一般來說,當選擇一個位置塊來處理請求時,從那一點開始,請求將完全在該上下文中進行處理。僅選定的位置和繼承的指令確定了請求如何被處理,沒有來自同級位置塊的干擾。

儘管這是一個通用規則,可以讓您以可預測的方式設計位置塊,但重要的是要意識到,有時選定位置內的某些指令會觸發新的位置搜索。對“只有一個位置塊”規則的例外可能會影響請求的實際處理方式,並且可能與您設計位置塊時的預期不一致。

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

  • index
  • try_files
  • rewrite
  • error_page

让我们简要地讨论一下。

如果使用index指令处理请求,它始终会导致内部重定向。精确的位置匹配通常用于加快选择过程,通过立即结束算法的执行来实现。然而,如果您进行一个精确的位置匹配,而这个位置是一个目录,那么请求很可能会被重定向到另一个位置进行实际处理。

在这个例子中,第一个位置是由请求URI /exact匹配的,但是为了处理请求,被块继承的index指令会引发一个内部重定向到第二个块:

index index.html;

location = /exact {

    . . .

}

location / {

    . . .

}

在上面的情况下,如果您真的需要执行保持在第一个块中,您将不得不想出一种不同的方法来满足对目录的请求。例如,您可以为该块设置一个无效的index并打开autoindex

location = /exact {
    index nothing_will_match;
    autoindex on;
}

location  / {

    . . .

}

这是防止index切换上下文的一种方式,但对于大多数配置来说可能并不实用。对目录进行精确匹配通常对诸如重写请求(这也会导致新的位置搜索)之类的事情有帮助。

另一个处理位置可能被重新评估的情况是使用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,則首先會收到第一個位置的請求。它將嘗試在/var/www/main目錄中查找名為blahblah的文件。如果找不到,它將繼續尋找名為blahblah.html的文件。然後,它將嘗試查看/var/www/main目錄中是否有一個名為blahblah/的目錄。如果所有這些嘗試都失敗,它將重定向到/fallback/index.html。這將觸發另一個位置搜索,將被第二個位置塊捕獲。這將提供/var/www/another/fallback/index.html文件。

另一個可能導致位置塊轉交的指令是rewrite指令。當使用rewrite指令的last參數,或者完全不使用參數時,Nginx將根據重寫的結果搜索新的匹配位置。

例如,如果我們修改最後一個示例以包含一個重寫,我們可以看到有時請求直接傳遞到第二個位置,而不依賴於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的請求將首先由第一個位置塊處理。它將被重寫為/hello,並將搜索位置。在這種情況下,它將再次匹配第一個位置,並像往常一樣由try_files處理,如果沒有找到任何內容(使用我們上面討論的try_files內部重定向),則可能返回/fallback/index.html

然而,如果請求的是 /rewriteme/fallback/hello,則第一個區塊將再次匹配。重新寫入將再次應用,這次將結果轉換為 /fallback/hello。然後,該請求將由第二個位置區塊提供服務。

A related situation happens with the return directive when sending the 301 or 302 status codes. The difference in this case is that it results in an entirely new request in the form of an externally visible redirect. This same situation can occur with the rewrite directive when using the redirect or permanent flags. However, these location searches shouldn’t be unexpected, since externally visible redirects always result in a new request.

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,從而導致新的位置搜索,最終會落在第二個區塊上。此文件將從 /var/www/another/whoops.html 提供服務。

正如您所看到的,了解 Nginx 觸發新位置搜索的情況可以幫助預測發出請求時將看到的行為。

結論

了解 Nginx 處理客戶端請求的方式可以讓您作為管理員的工作更輕鬆。您將能夠知道 Nginx 將根據每個客戶端請求選擇哪個伺服器塊。您還將能夠根據請求 URI 知道將選擇哪個位置塊。總的來說,了解 Nginx 選擇不同塊的方式將使您能夠追蹤 Nginx 將應用以處理每個請求的上下文。

Source:
https://www.digitalocean.com/community/tutorials/understanding-nginx-server-and-location-block-selection-algorithms