了解 Nginx server 和 location 块选择算法

了解 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

本作品采用《CC 协议》,转载必须注明作者和本文链接

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

讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!