介绍
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 允许管理员定义多个服务器块,这些块充当独立的虚拟 Web 服务器实例,因此它需要一个确定哪个服务器块将用于满足请求的过程。
它通过一套定义好的检查系统来实现这一点,以找到最佳匹配。在此过程中,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 尝试匹配位置块的方式。下面的修饰符会导致相关位置块被解释如下:
- (无):如果没有修饰符,位置将被解释为前缀匹配。这意味着将根据请求 URI 的开头来匹配给定的位置。
=
:如果使用等号,则该块将被视为匹配,如果请求 URI 与给定的位置完全匹配。~
:如果存在波浪号修饰符,此位置将被解释为区分大小写的正则表达式匹配。~*
:如果使用了波浪号和星号修饰符,则位置块将被解释为不区分大小写的正则表达式匹配。^~
:如果存在插入符和波浪号修饰符,并且此块被选为最佳的非正则表达式匹配,则不会进行正则表达式匹配。
演示位置块语法的示例
作为前缀匹配的示例,以下位置块可能被选中以响应请求URI,例如/site
、/site/page1/index.html
或/site/index.html
:
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 在这篇文章中解释了选择算法的这部分。
何时会跳转到其他位置块的评估?
一般来说,当选择位置块来提供请求时,请求完全在那个上下文中处理,从那时起。只有选定的位置和继承的指令决定了如何处理请求,而不会受到同级位置块的干扰。
虽然这是一个通用规则,可以让您以可预测的方式设计位置块,但重要的是要意识到,在所选位置的某些指令触发新的位置搜索时,会有时候。“只有一个位置块”规则的例外情况可能会影响实际服务请求的方式,并且可能与您设计位置块时的预期不一致。
一些可能导致此类内部重定向的指令是:
- 索引
- 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检查一个命名的文件或目录集是否存在。最后一个参数可以是一个URI,Nginx将对其进行内部重定向。
考虑以下配置:
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
处理,如果找不到任何内容,则可能退回到/fallback/index.html
(使用我们上面讨论的try_files
内部重定向)。
然而,如果请求是 /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将应用以服务每个请求的上下文。