介绍

Nginx 是世界上最受欢迎的 Web 服务器之一。它可以成功处理具有许多并发客户端连接的高负载,并且可以用作 Web 服务器、邮件服务器或反向代理服务器。

在本指南中,我们将讨论一些决定 Nginx 如何处理客户端请求的幕后细节。理解这些想法有助于在设计server 和 location 块时进行预测,并使请求处理看起来不那么不可预测。

Nginx 块配置

Nginx 在逻辑上将旨在为不同内容提供服务的配置划分为块,这些块以分层结构存在。每次发出客户端请求时,Nginx 都会开始一个过程,以确定应该使用哪些配置块来处理请求。我们将在本指南中讨论这个决策过程。

我们将讨论的主要块是server 块和location 块。

server 块是 Nginx 配置的一个子集,它定义了一个虚拟服务器,用于处理已定义类型的请求。管理员通常配置多个服务器块,并根据请求的域名、端口和 IP 地址决定哪个块应该处理哪个连接。

location 块位于server 块中,用于定义 Nginx 应如何处理对父服务器的不同资源和 URI 的请求。URI 空间可以按照管理员喜欢使用这些块的任何方式进行细分。这是一个非常灵活的模型。

Nginx 如何决定哪个server 块将处理请求

由于 Nginx 允许管理员定义多个server 块作为单独的虚拟 Web 服务器实例,因此它需要一个过程来确定将使用这些server 块中的哪些来满足请求。

它通过定义的检查系统来实现这一点,这些检查系统用于找到可能的最佳匹配。Nginx 在此过程中关注的主要server 块指令是listen指令和server_name指令。

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

首先,Nginx 查看请求的 IP 地址和端口。它将这与listen每个服务器的指令相匹配,以构建可能解析请求的服务器块列表。

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

listen指令可以设置为:

  • IP 地址/端口组合。
  • 一个单独的 IP 地址,然后将侦听默认端口 80。
  • 一个单独的端口,它将侦听该端口上的每个接口。
  • Unix 套接字的路径。

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

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

  • Nginxlisten通过用默认值替换缺失值来转换所有“不完整”指令,以便每个块都可以通过其 IP 地址和端口进行评估。一些例子是:
    • 没有listen指令的块使用 value 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 地址(以匹配任何接口)的块。在任何情况下,端口都必须完全匹配。
  • 如果只有一个最具体的匹配项,则该server 块将用于为请求提供服务。如果有多个server 块具有相同级别的特异性匹配,Nginx 然后开始评估server_name每个server 块的指令。

了解 Nginx 仅server_name在需要区分与listen指令中相同级别的特定性匹配的server 块时才会评估指令很重要。例如,如果example.com托管在 的端口80192.168.1.10example.com则在此示例中,尽管server_name第二个块中有指令,请求将始终由第一个块提供服务。

server {
    listen 192.168.1.10;

    . . .

}

server {
    listen 80;
    server_name example.com;

    . . .

}

如果有多个server 块以相同的特异性匹配,下一步是检查server_name指令。

解析server_name指令以选择匹配

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

Nginx 试图通过查看server_name仍然是候选的每个server 块中的指令来找到它找到的值的最佳匹配。Nginx 使用以下公式评估这些:

  • Nginx 将首先尝试找到一个与请求头server_name中的值完全匹配的server 块。如果找到,将使用关联的块来为请求提供服务。如果找到多个完全匹配,则使用一个。Host
  • 如果没有找到完全匹配,Nginx 将尝试server_name使用前导通配符(由*配置中名称开头的a 表示)查找匹配的服务器块。如果找到,则该块将用于为请求提供服务。如果找到多个匹配项,将使用最长的匹配项来为请求提供服务。
  • 如果使用前导通配符未找到匹配项,Nginx 将server_name使用尾随通配符(由*配置中以 a 结尾的服务器名称表示)查找具有匹配项的server 块。如果找到,则该块用于为请求提供服务。如果找到多个匹配项,将使用最长的匹配项来为请求提供服务。
  • 如果使用尾随通配符未找到匹配项,Nginx 将评估定义server_nameusing 正则表达式(由~名称前的a 表示)的server 块。第一个 server_name带有与“Host”标头匹配的正则表达式的将用于为请求提供服务。
  • 如果没有找到匹配的正则表达式,Nginx 会为该 IP 地址和端口选择默认的server 块。

每个 IP 地址/端口组合都有一个默认server 块,当无法通过上述方法确定操作过程时,将使用该块。对于 IP 地址/端口组合,这将是配置中的第一个块,或者是包含default_server作为listen指令一部分的选项的块(这将覆盖首先找到的算法)。default_server每个 IP 地址/端口组合只能有一个声明。

例子

如果有一个server_nameHost标头值完全匹配的定义,则选择该server 块来处理请求。

在此示例中,如果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 块:

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 块:

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 块来满足请求:

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 也有一个既定的算法来决定服务器中的哪个location 块用于处理请求。

location 块语法

在我们介绍 Nginx 如何决定使用哪个location 块来处理请求之前,让我们回顾一下您可能在location 块定义中看到的一些语法。location 块位于server 块(或其他location 块)中,用于决定如何处理请求 URI(请求的一部分,位于域名或 IP 地址/端口之后)。

位置块通常采用以下形式:

location optional_modifier location_match {

    . . .

}

location_match上面定义了Nginx的应检查请求URI反对。上例中修饰符的存在与否影响了 Nginx 尝试匹配location 块的方式。下面的修饰符将导致相关的location 块被解释如下:

  • (none):如果不存在修饰符,则该位置被解释为前缀匹配。这意味着给定的位置将与请求 URI 的开头匹配以确定匹配。
  • =: 如果使用等号,如果请求 URI 与给定的位置完全匹配,则此块将被视为匹配。
  • ~:如果存在波浪号修饰符,则此位置将被解释为区分大小写的正则表达式匹配。
  • ~*:如果使用波浪号和星号修饰符,则位置块将被解释为不区分大小写的正则表达式匹配。
  • ^~: 如果存在 carat 和 tilde 修饰符,并且如果此块被选为最佳非正则表达式匹配,则不会进行正则表达式匹配。

展示location 块语法的示例

作为前缀匹配的一个例子,以下location 块可以被选择为响应于请求的URI的样子/site/site/page1/index.html/site/index.html

location /site {

    . . .

}

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

location = /page1 {

    . . .

}

作为应解释为区分大小写的正则表达式的location 示例,此块可用于处理对 的请求/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,其方式与它选择server 块的方式类似。它通过一个过程来确定任何给定请求的最佳location 块。了解这个过程是能够可靠、准确地配置 Nginx 的关键要求。

记住我们上面描述的位置声明的类型,Nginx 通过将请求 URI 与每个位置进行比较来评估可能的location 上下文。它使用以下算法执行此操作:

  • Nginx 首先检查所有基于前缀的location 匹配(所有不涉及正则表达式的location 类型)。它根据完整的请求 URI 检查每个位置。
  • 首先,Nginx 寻找完全匹配。如果=发现使用修饰符的location 块与请求 URI 完全匹配,则立即选择该location 块来为请求提供服务。
  • 如果没有找到精确的(使用=修饰符)位置块匹配,Nginx 然后继续评估非精确前缀。它发现给定请求 URI 的最长匹配前缀位置,然后按如下方式评估:
    • 如果最长匹配的前缀location 有^~修饰符,则 Nginx 将立即结束其搜索并选择此location 来为请求提供服务。
    • 如果最长匹配前缀location 没有使用^~修饰符,则匹配由 Nginx 暂时存储,以便可以移动搜索的焦点。
  • 在确定并存储最长匹配前缀位置后,Nginx 继续评估正则表达式位置(区分大小写和不区分大小写)。如果有任何的正则表达式的location 的最长前缀匹配的location ,Nginx的将这些移动到它的正则表达式的location 列表的顶部进行检查。然后 Nginx 尝试按顺序匹配正则表达式location。所述第一所述请求URI匹配正则表达式的location 被立即选择来服务该请求。
  • 如果找不到与请求 URI 匹配的正则表达式location,则选择先前存储的前缀location 来为请求提供服务。

重要的是要理解,默认情况下,Nginx 将优先于前缀匹配提供正则表达式匹配。但是,它首先评估前缀位置,允许管理员通过使用=^~修饰符指定位置来覆盖这种趋势。

同样重要的是要注意,虽然前缀位置通常根据最长、最具体的匹配进行选择,但在找到第一个匹配location时会停止正则表达式评估。这意味着配置中的定位对正则表达式location 有很大的影响。

最后,要明白,正则表达式匹配它是非常重要的最长前缀匹配将“跳线”,当Nginx的正则表达式的计算结果的location。在考虑任何其他正则表达式匹配之前,将按顺序对这些进行评估。

location 块评估何时跳转到其他location?

一般来说,当一个location 块被选择来服务一个请求时,从那个点开始,请求就完全在那个上下文中处理。只有选定的location 和继承的指令决定了请求的处理方式,不受同级location 块的干扰。

尽管这是允许您以可预测的方式设计location 块的一般规则,但重要的是要意识到有时会由所选位置内的某些指令触发新的位置搜索。“只有一个location 块”规则的例外可能会影响请求的实际服务方式,并且可能与您在设计location 块时的期望不一致。

可能导致这种类型的内部重定向的一些指令是:

  • index
  • try_files
  • rewrite
  • error_page

让我们简单地回顾一下这些。

如果该index指令用于处理请求,则它总是会导致内部重定向。精确的location匹配通常用于通过立即结束算法的执行来加速选择过程。但是,如果您将精确位置匹配为目录,则很有可能将请求重定向到不同的location 以进行实际处理。

在此示例中,第一个位置与 的请求 URI 匹配/exact,但为了处理该请求,index块继承的指令启动到第二个块的内部重定向:

index index.html;

location = /exact {

    . . .

}

location / {

    . . .

}

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

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

location  / {

    . . .

}

这是防止 index切换上下文的一种方法,但它可能对大多数配置没有用。大多数情况下,目录上的完全匹配对于重写请求(这也会导致新的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 最初将收到请求。它将尝试找到blahblah/var/www/main目录中调用的文件。如果找不到,它将通过搜索名为blahblah.html. 然后它会尝试查看目录中是否有一个被调用blahblah//var/www/main目录。所有这些尝试都失败了,它将重定向到/fallback/index.html. 这将触发另一个location 搜索,该搜索将被第二个location 块捕获。这将为文件提供服务/var/www/another/fallback/index.html

另一个可能导致location 块传递的rewrite指令是指令。当lastrewrite指令中使用参数时,或者根本不使用参数时,Nginx 将根据重写的结果搜索新的匹配location 。

例如,如果我们修改最后一个示例以包含重写,我们可以看到请求有时会直接传递到第二个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发送301302状态代码时,指令会发生相关情况。这种情况下的不同之处在于,它会以外部可见的重定向形式产生全新的请求。rewrite使用redirectorpermanent标志时,指令也会发生同样的情况。但是,这些位置搜索不应出乎意料,因为外部可见的重定向总是会导致新的请求。

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 将应用的上下文,以便为每个请求提供服务。

发表评论