介绍
Nginx 是世界上最受欢迎的 Web 服务器之一。 它可以成功处理具有许多并发客户端连接的高负载,并且可以用作 Web 服务器、邮件服务器或反向代理服务器。
在本指南中,我们将讨论一些决定 Nginx 如何处理客户端请求的幕后细节。 了解这些想法可以帮助您在设计服务器和位置块时进行猜测,并且可以使请求处理看起来不那么不可预测。
Nginx 块配置
Nginx 在逻辑上将用于提供不同内容的配置划分为块,这些块存在于层次结构中。 每次发出客户端请求时,Nginx 都会开始确定应该使用哪些配置块来处理请求。 这个决策过程就是我们将在本指南中讨论的内容。
我们将讨论的主要块是 server 块和 location 块。
服务器块是 Nginx 配置的子集,它定义了一个虚拟服务器,用于处理定义类型的请求。 管理员经常配置多个服务器块,并根据请求的域名、端口和 IP 地址决定哪个块应该处理哪个连接。
位置块位于服务器块中,用于定义 Nginx 应如何处理对父服务器的不同资源和 URI 的请求。 可以使用这些块以管理员喜欢的任何方式细分 URI 空间。 这是一个非常灵活的模型。
Nginx 如何决定哪个服务器块将处理请求
由于 Nginx 允许管理员定义多个服务器块作为单独的虚拟 Web 服务器实例,它需要一个过程来确定将使用这些服务器块中的哪些来满足请求。
它通过一个定义的检查系统来做到这一点,该系统用于找到最佳匹配。 Nginx 在此过程中关注的主要服务器块指令是 listen
指令和 server_name
指令。
解析 listen
指令以查找可能的匹配项
首先,Nginx 查看请求的 IP 地址和端口。 它与每个服务器的 listen
指令匹配,以构建可能解析请求的服务器块列表。
listen
指令通常定义服务器块将响应的 IP 地址和端口。 默认情况下,任何不包含 listen
指令的服务器块都会被赋予 0.0.0.0:80
的监听参数(如果 Nginx 由正常的非 [ X170X]root 用户)。 这允许这些块响应端口 80 上任何接口上的请求,但此默认值在服务器选择过程中没有太大的权重。
listen
指令可以设置为:
- IP 地址/端口组合。
- 一个单独的 IP 地址,然后将在默认端口 80 上进行侦听。
- 一个单独的端口,它将监听该端口上的每个接口。
- 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 地址和端口收集与请求最匹配的服务器块列表。 这意味着,如果存在列出特定 IP 地址的匹配块,则不会选择在功能上使用
0.0.0.0
作为其 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
标头中的值完全匹配 。 如果找到,关联的块将用于服务请求。 如果找到多个完全匹配,则使用 first 一个。 - 如果没有找到完全匹配,Nginx 将尝试使用前导通配符匹配
server_name
的服务器块(由配置中名称开头的*
表示) . 如果找到,该块将用于服务请求。 如果找到多个匹配项,将使用 longest 匹配项来处理请求。 - 如果使用前导通配符未找到匹配项,Nginx 然后查找具有
server_name
的服务器块,该块使用尾随通配符匹配(由配置中以*
结尾的服务器名称指示) . 如果找到,则使用该块来服务请求。 如果找到多个匹配项,将使用 longest 匹配项来处理请求。 - 如果使用尾随通配符没有找到匹配项,Nginx 然后使用正则表达式评估定义
server_name
的服务器块(由名称前的~
表示)。 带有匹配“Host”标头的正则表达式的 firstserver_name
将用于服务请求。 - 如果没有找到正则表达式匹配,Nginx 然后为该 IP 地址和端口选择默认服务器块。
每个 IP 地址/端口组合都有一个默认服务器块,当无法通过上述方法确定操作过程时,将使用该块。 对于 IP 地址/端口组合,这将是配置中的第一个块或包含 default_server
选项作为 listen
指令的一部分的块(这将覆盖第一个找到的算法)。 每个 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
具有适合的起始通配符。 将选择以通配符开头的最长匹配来满足请求。
在此示例中,如果请求具有 www.example.org
的 Host
标头,则将选择第二个服务器块:
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
指令。 将选择first匹配的正则表达式来响应请求。
例如,如果请求的 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$; . . . }
如果以上步骤都不能满足请求,则请求将被传递到 default 服务器以获取匹配的 IP 地址和端口。
匹配位置块
与 Nginx 用于选择将处理请求的服务器块的过程类似,Nginx 也有一个既定的算法来决定服务器中的哪个位置块用于处理请求。
位置块语法
在我们介绍 Nginx 如何决定使用哪个位置块来处理请求之前,让我们回顾一下您可能在位置块定义中看到的一些语法。 位置块位于服务器块(或其他位置块)中,用于决定如何处理请求 URI(请求中位于域名或 IP 地址/端口之后的部分)。
位置块通常采用以下形式:
location optional_modifier location_match { . . . }
上面的 location_match
定义了 Nginx 应该检查请求 URI 的对象。 上例中修饰符的存在与否会影响 Nginx 尝试匹配位置块的方式。 下面的修饰符将导致关联的位置块被解释如下:
- (none):如果不存在修饰符,则该位置被解释为 prefix 匹配。 这意味着给定的位置将与请求 URI 的开头进行匹配以确定匹配。
- =:如果使用等号,如果请求 URI 与给定位置完全匹配,则此块将被视为匹配。
- ~:如果存在波浪号修饰符,则此位置将被解释为区分大小写的正则表达式匹配。
- ~*:如果使用波浪号和星号修饰符,则位置块将被解释为不区分大小写的正则表达式匹配。
- ^~:如果存在克拉和波浪号修饰符,并且如果该块被选为最佳非正则表达式匹配,则不会进行正则表达式匹配。
演示位置块语法的示例
作为前缀匹配的示例,可以选择以下位置块来响应类似于 /site
、/site/page1/index.html
或 /site/index.html
的请求 URI:
location /site { . . . }
为了演示精确的请求 URI 匹配,此块将始终用于响应类似于 /page1
的请求 URI。 它将 not 用于响应 /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 { . . . }
如您所见,修饰符指示应如何解释位置块。 然而,这 not 告诉我们 Nginx 用来决定将请求发送到哪个位置块的算法。 接下来我们将讨论这个问题。
Nginx 如何选择使用哪个位置来处理请求
Nginx 选择用于服务请求的位置,其方式与选择服务器块的方式类似。 它运行一个为任何给定请求确定最佳位置块的过程。 了解这个过程是能够可靠、准确地配置 Nginx 的关键要求。
记住我们上面描述的位置声明的类型,Nginx 通过将请求 URI 与每个位置进行比较来评估可能的位置上下文。 它使用以下算法执行此操作:
- Nginx 首先检查所有基于前缀的位置匹配(所有不涉及正则表达式的位置类型)。 它根据完整的请求 URI 检查每个位置。
- 首先,Nginx 寻找完全匹配。 如果发现使用
=
修饰符的位置块与请求 URI 完全匹配,则立即选择此位置块来为请求提供服务。 - 如果没有找到精确的(使用
=
修饰符)位置块匹配,Nginx 然后继续评估非精确前缀。 它发现给定请求 URI 的最长匹配前缀位置,然后评估如下: 如果最长匹配的前缀位置有 ^~ 修饰符,那么 Nginx 将立即结束搜索并选择该位置来服务请求。 如果最长匹配前缀位置不使用 ^~ 修饰符,则匹配由 Nginx 暂时存储,以便可以转移搜索的焦点。 - 在确定并存储最长匹配前缀位置后,Nginx 继续评估正则表达式位置(区分大小写和不区分大小写)。 如果在最长匹配前缀位置 内有任何正则表达式位置 ,Nginx 会将这些位置移动到其正则表达式位置列表的顶部以进行检查。 Nginx 然后尝试按顺序匹配正则表达式位置。 立即选择与请求 URI 匹配的 first 正则表达式位置来为请求提供服务。
- 如果没有找到与请求 URI 匹配的正则表达式位置,则选择先前存储的前缀位置来为请求提供服务。
重要的是要理解,默认情况下,Nginx 将优先提供正则表达式匹配而不是前缀匹配。 然而,它 首先评估 前缀位置,允许管理员通过使用 =
和 ^~
修饰符指定位置来覆盖这种趋势。
还需要注意的是,虽然前缀位置通常基于最长、最具体的匹配进行选择,但在找到第一个匹配位置时会停止正则表达式评估。 这意味着配置中的定位对正则表达式位置有很大的影响。
最后,重要的是要理解正则表达式匹配 within 当 Nginx 评估正则表达式位置时,最长前缀匹配将“跳线”。 在考虑任何其他正则表达式匹配之前,将按顺序评估这些。 Maxim Dounin 是一位非常乐于助人的 Nginx 开发人员,他在 这篇文章 中解释了选择算法的这一部分。
位置块评估何时跳转到其他位置?
Generally speaking, when a location block is selected to serve a request, the request is handled entirely within that context from that point onward. 只有选定的位置和继承的指令确定如何处理请求,而不受兄弟位置块的干扰。
尽管这是一条允许您以可预测的方式设计您的位置块的一般规则,但重要的是要意识到有时新的位置搜索会由所选位置内的某些指令触发。 “只有一个位置块”规则的例外情况可能会影响请求的实际服务方式,并且可能与您在设计位置块时的期望不一致。
一些可能导致这种类型的内部重定向的指令是:
- 指数
- 尝试文件
- 改写
- 错误页面
让我们简要介绍一下。
如果 index
指令用于处理请求,它总是会导致内部重定向。 精确位置匹配通常用于通过立即结束算法的执行来加速选择过程。 但是,如果您进行 目录 的精确位置匹配,则请求很有可能被重定向到不同的位置以进行实际处理。
在此示例中,第一个位置与 /exact
的请求 URI 匹配,但为了处理请求,该块继承的 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
指令。 当 last
参数与 rewrite
指令一起使用时,或者根本不使用参数时,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
。 然后将在第二个位置块之外提供请求。
在发送 301
或 302
状态码时,return
指令会发生相关情况。 这种情况下的不同之处在于,它会以外部可见重定向的形式产生一个全新的请求。 当使用 redirect
或 permanent
标志时,rewrite
指令也会出现同样的情况。 但是,这些位置搜索不应该是意外的,因为外部可见的重定向总是会导致新的请求。
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 将应用的上下文以服务每个请求。