简介
Nginx 是世界上最受欢迎的 Web 服务器选择之一。它能够成功处理大量的并发客户端连接。同时,它还可以用作邮件、Web 或反向代理服务器。
本指南旨在概述指导 Nginx 如何处理客户端请求的幕后方法。我们将揭开 server 和 location 块 设计的神秘面纱,并解释如何最好地减少处理请求时看似不可预测的情况。
首先,这里有一份关于如何在 Ubuntu 服务器上安装 Nginx的完整教程。现在,让我们开始吧!
使用 Nginx 进行块配置
Nginx 的逻辑方法包括将用于不同目的的配置分类到独立的、更具逻辑性的内容块中。这些块将存在于分层结构中。当客户端发出请求时,Nginx 会启动一个过程,以此确定这些配置块中哪一个最适合处理该请求。我们将重点关注这一决策过程。
我们将讨论的主要块是 server 块和 location 块。Server 块是 Nginx 建立的配置子集,定义了哪个虚拟服务器将负责处理特定类型的请求。它们最常基于传入请求的 IP 地址、域名或端口。管理员会配置多个 server 块。然后,他们需要决定应该由哪个连接来处理请求。
Location 块位于 server 块内。它们是决定如何以及可以使用哪些资源来处理其特定父服务器的传入请求的决策者。这种模型非常灵活。可以配置 URI 空间,以管理员认为最佳的任何方式使用这些块。
使用 Nginx 决定哪个块将处理哪个请求
Nginx 允许定义多个 server 块。它们都作为不同的虚拟 Web 服务器运行。因此,需要有一种方法来划定哪个服务器将处理特定的传入请求。这是通过一套定义的检查系统来寻找最适合请求性能的配置来实现的。
Nginx 主要处理两个主要的 server 块指令:listen 和 server_name.
使用 ‘Listen’ 指令寻找可能的匹配项
Nginx 首先评估的是请求的端口和 IP 地址。然后,它将其与每个服务器的 listen 指令进行匹配。对服务器列表的这种解析有助于隔离出那些能够解决相关请求的 server 块。
通常,listen 指令将定义特定 server 块负责响应的端口和 IP 地址。不包含 listen 指令的 server 块默认会接收 0.0.0.0:80 的监听参数。如果 Nginx 是由普通的非 root 用户运行的,则监听参数定义为 0.0.0.0:8080。这意味着无论接口是什么,如果块来自端口 80,以这种方式定义的块都将对其做出响应。然而,在选择服务器的过程中,这个默认值的权重并不高。
您可以将 listen 指令配置为:
- 在默认端口 (80) 上监听请求的单个 IP 地址。
- 监听该端口上任何接口的单个端口。
- 端口和 IP 地址的组合。
- 设定的 Unix 套接字路径(此选项仅在请求跨不同服务器传递时产生影响)。
在决定将请求发送到哪个 server 块时,Nginx 将执行一套规则。这些规则取决于 listen 指令的具体配置。它们如下:
- 如果 listen 指令不完整,缺失的部分将获取其默认值。这意味着 IP 地址和端口将被强制补全为默认值,以便处理请求。
- 在这种情况下,不包含 listen 指令的块将使用默认值 0.0.0.0:80。
- 缺少端口且 IP 地址为 111.111.111.111 的块将变为 111.111.111.111:80。
- 当没有 IP 地址时,带有端口 8888 的块将获取默认 IP 地址并进行追加,以创建 0.0.0.0:8888。
- 确定了 IP 地址和端口后,Nginx 将寻找与该端口匹配的 server 块。
- 如果它只找到一个特定的匹配项,这就是该 server 块。如果有多个符合条件的块,Nginx 将求助于 server_name 指令,以进一步细化到具体的 server 块。
只有在没有从 listen 指令中找到具有精确匹配级别的 server 块时,Nginx 才会评估 server_name 指令。如果 example.com 位于端口 80,IP 为 192.168.1.10,则此示例中的第一个块将始终是处理该请求的块。无论 server_name 指令的内容如何,情况都是如此:

如果有多个具有匹配特异性的合格服务块,那么将考虑 server_name 指令。
使用 “Server_Name” 指令寻找可能的匹配项
如果 listen 指令的特异性相同,Nginx 将检查请求’的 “Host” 头部。该值最初包含客户端想要访问的域名的 IP。Nginx 将利用每个仍符合条件的 server 块候选对象内部的 server_name 指令。它根据一个公式进行这些评估。公式如下:
- Nginx 的首次尝试将是识别一个其 server_name 与请求中的 “Host” 头部值完全匹配的块。如果找到了,包含该精确匹配项的块将用于处理该请求。如果找到多个块,它将选择列表中的第一个。
- 如果没有精确匹配项,Nginx 接下来将尝试使用 server_name 来寻找通过使用 *(配置中 server 块名称开头的通配符)进行匹配的 server 块。通过这种方法找到一个块意味着该 server 块已被确定。如果找到多个匹配项,则最长匹配项将是处理该请求的块。
- 如果没有匹配的前导通配符,Nginx 将尝试寻找具有匹配的尾随通配符的 server 块。换句话说,这将是配置中带有 * 的 server 名称。如果找到了一个,它将用于该请求。而如果找到多个,Nginx 将再次使用最长的匹配项。
- 如果在两次通配符尝试后仍然没有匹配项,Nginx 将评估那些通过使用典型表达式(在名称前由 ~ 指定)来定义 server_name 的 server 块。第一个其表达式与 “Host” 头部匹配的 server_name 实例将被视为用于处理请求的 server 块。
- 如果此时仍然没有匹配项,Nginx 将使用该端口和 IP 地址组合的默认 server 块。
每个端口/IP 地址组合都将有一个指定的 server 块。如果确定用于处理请求的合适 server 块的规则没有结果,则将使用它。这将是配置中第一个在 listen 指令中包含 default_server 选项的块(它将覆盖最初找到的算法)。每个 IP 地址/端口组合最多只能有一个 default_server 设置。
Server 块选择示例
如果定义的 server_name 与 “Host” 头部值完全匹配,它将是选择用于请求处理的 server 块。以下示例显示了指定为 “host1.example.com” 的请求的 “Host” 头部。在这种情况下,它将选择第二个 server:

如果没有精确匹配项,Nginx 将检查是否存在带有通配符的 server_name。如果没有,将选择以通配符开头的最长匹配项。在以下内容中,“www.example.org” 位于 “Host” 头部。这意味着它将选择第二个块:

如果没有以通配符开头的匹配,Nginx 将继续进行尾部通配符检查。将选择以通配符结尾的最长匹配来处理请求。在此示例中,“Host”请求头为“www.example.com”,因此它将选择第三个 server 块:

如果仍然没有匹配项,Nginx 将尝试使用正则表达式匹配 server_name 指令。这些表达式中的第一个将被选择用于处理请求。如果“Host”是“www.example.com,”第二个 server 块将成为处理该请求的选择:

如果仍然没有匹配项,请求将转到设置了匹配默认服务器的 IP 地址和端口组合。
Location 块匹配
Nginx 还需要建立一个算法,通过该算法决定服务器上的哪个 location 块将负责响应请求。
Location 块的语法
在解释 Nginx 如何决定指定哪个 location 块来处理请求之前,我们将先回顾一下 location 块定义中的语法。如前所述,location 块位于 server 块(以及其他 location 块)中。它们的目的在于决定如何处理请求 URI。URI 是请求中位于 IP 地址和端口或域名之后的部分。
Location 块通常看起来像这样:

Nginx 将根据 location_match 检查请求的 URI。是否存在上述修饰符将决定 Nginx 尝试匹配这些块的方式。根据修饰符的不同,location 块将按照以下规则进行解析:
- 无修饰符: 没有任何修饰符时,location 将被解析为前缀匹配。这意味着所提供的 location 将与请求中 URI 的开头进行比对,以确定正确的匹配。
- =: 等号表示只要请求的 URI 与提供的 location 完全匹配,该块就会被视为匹配。
- ~: 波浪号修饰符表示 location 块的匹配将区分大小写。
- ~*: 波浪号和星号的组合修饰符表示 location 块在寻求匹配时不区分大小写。
- ^~: 如果波浪号修饰符前面有一个脱字符,只要该块被选为最佳非正则表达式匹配,就不会进行正则表达式匹配。
Location 块语法示例
为了展示前缀匹配的示例,以下 location 块将被选择用于响应格式为 /site、/site/page1/index.html 或 /site/index/html 的请求 URI:

为了演示所需 URI 匹配,该块将始终用于响应格式为 /page1 的 URI 请求,而不是 /page1/index.html 请求 URI。如果这是被选中的块,并且它使用索引页满足了请求,则请求的实际处理程序将在内部重定向到另一个 location:

例如,对于必须使用区分大小写表达式解析的 location,以下块无法处理对 /FLOWER.PNG 的请求。但是,它将处理对 /tortoise.jpg 的请求:

接下来,观察一个允许不区分大小写匹配的块,它与上面的块类似。在这种情况下,该块可以同时处理 //tortoise.jpg 和 /FLOWER.PNG:

最后一种变体是,如果确定某个块是最佳非正则表达式匹配,则该块将阻止进行正则表达式匹配。这一个可以处理对 /costumes/ninja.html 的请求:

确切地说,修饰符决定了 location 块的确定方式。然而,这并没有告诉我们 Nginx 使用什么决策算法来识别请求应发送到的 location 块。接下来让我们讨论这个问题。
选择由 Nginx 处理请求的 Location
Nginx 选择处理请求的 location 的方法与选择 server 块的方法类似。换句话说,它通过运行一个过程来确定每个请求的最佳 location。为了准确且相应地配置 Nginx,您必须了解这一过程。
考虑到前面提到的 location 声明,Nginx 同样通过将给定请求的 URI 与每个 location 进行比较,来检查其是否符合条件,从而使用潜在的 location 上下文。在此过程中,它应用了以下算法:
- 首先,Nginx 检查所有不包含正则表达式的 location 类型。它通过寻找所有基于 location 的前缀匹配来实现这一点。为此,它会将 location 与请求的完整 URI 进行比对。
- Nginx 开始寻找精确匹配。一旦识别出使用 = 修饰符的 location 块,就会将其与 URI 请求进行比较。如果两者完全匹配,则会立即选择该 location 块来处理该请求。
- 如果没有与 = 修饰符完全匹配的 location,Nginx 将继续评估非精确的前缀。一旦确定了与请求的 URI 匹配的最长前缀 location,它将进行以下评估:
- 如果具有最长前缀匹配的 location 使用了 ^~ 修饰符,则将立即选择该 location。
- 如果具有最长前缀的 location 未使用 ^~ 修饰符,则 Nginx 会暂时保留该匹配,以便将搜索焦点转移。
- 一旦找到并存储了最长前缀 location 匹配,Nginx 就会转向评估正则表达式 location。这些包括区分大小写和不区分大小写的匹配。如果最长匹配前缀 location 内部包含任何正则 location,Nginx 将重新调整列表,将这些内容放在 location 列表的顶部附近。重新排序后的列表中第一个与请求 URI 匹配的表达式,将是选择用来服务该请求的 location。
- 如果没有找到满足请求 RI 的正则表达式,则将选择之前存储的 location 来处理该请求。
默认情况下,Nginx 将正则表达式匹配的优先级置于优先前缀匹配之上。然而,它确实会先评估前缀 location,以便管理端可以使用 = 和 ^~ 修饰符来覆盖这一倾向。
另一个重要的要点是,虽然前缀 location 通常基于最具体、最长的匹配,但正则表达式检查在识别出第一个匹配后就会立即停止。这意味着配置中的位置对正则表达式 location 具有实际影响。
最后需要指出的一点是,在最长前缀匹配范围内的正则表达式匹配,在 Nginx 的 location 评估期间基本上会“插队”。这些将被放置在列表的顶部,并先于其他正则表达式进行评估。
在 Location 块评估中,何时会跳转到其他 Location?
通常,一旦对请求进行了评估并选择了处理该请求的 location 块,它将完全在该上下文中进行处理。这意味着只有继承的指令和选定的 location 才是处理请求的决定因素,而不需要任何同级 location 块的输入。
虽然这是一个允许对 location 块进行可预测设计的通用指令,但有时 location 内的某些指令也会触发新的搜索。换句话说,“仅一个 location 块”规则有一些例外。这些例外可能与 location 块的预期不符。因此,它们可能无法按预期处理请求。
这些内部重定向最终可能会由于某些指令而显现,包括:
- index
- rewrite
- error_page
- try_files
如果您使用 index 指令,它在请求处理期间总是会导致内部重定向。虽然寻找 location 匹配通常会结束算法执行以加快选择过程,但如果找到的 location 匹配是一个目录,该请求很可能会被重定向到另一个 location 以进行正式处理。
例如,以下第一个 location 与请求 URI /exact 匹配。然而,为了处理该请求,location 块继承的 index 指令会将请求重定向到第二个块:

对于这种情况,如果执行需要保留在主块内,则需要另一种方案来处理对目录的请求。一种方法是为相关块设置一个无效的 index,并激活 auto index:

虽然这种方法在少数情况下可能有效,但在大多数情况下大体上并不实用。精确的目录匹配对于需要重写请求的情况非常有用。这将触发全新的 location 搜索。
另一个可用于重新评估处理 location 的指令是 try_files 指令。它告诉 Nginx 专门检查指定的一组文件 or 目录是否存在,最后一个搜索条件是 Nginx 内部重定向到的 URI。
让我们考虑以下配置:

如果有对 /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 参数时,Nginx 将根据 rewrite 指令的结果搜索新的匹配 location。如果将上一个示例修改为现在包含此 rewrite 指令,显然可以在不执行 try_files 指令的情况下将请求重定向到另一个 location:

在这个例子中,对 /rewrite/hello 的请求最初将由第一个 location 处理。在它被重写为 /hello 之后,将触发第二次 location 搜索。它将与第一个 location 匹配。它将由 try_file 指令处理,如果未命中,可能会恢复为 /fallback/index.html。
然而,如果对 /rewrite/fallback/hello 发起请求,将找到与第一个块的匹配。因此,将再次处理 rewrite,但这次的结果是 /fallback/hello。该请求将在另一个 location 块上进行处理。
当您使用 return 指令发送 301 或 302 状态码时,也会发生类似的情况。唯一的区别是会产生一个新的请求,并表现为非常明显的重定向。同样,当您应用 permanent 或 redirect 标志时,rewrite 指令也会发生这种情况。
另一个可以导致类似于 try_again 的内部重定向的指令是 error_page 指令。当您在处理中遇到特定的错误代码时,可以使用它。当设置了 try_files 指令时,error_page 指令很可能永远不会被执行。那是因为该指令将处理请求的整个生命周期。
让我们考虑以下示例:

在这种情况下,每个请求都将由第一个块处理,该块从 /var/www/main 提供文件。这不适用于那些以 /another 开头的请求。但如果找不到文件,将启动内部重定向到 /another/whoops/html。这将导致另一个 location 搜索。反过来,它会将请求引导到第二个块,并从 /var/www/another/whoops.html 中处理该文件。
显然,理解 Nginx 何时会触发新的 location 搜索,有助于在处理请求时更好地预测系统行为。
结论
当管理员理解了 Nginx 处理客户端请求的方法时,他们的工作就会变得简单得多。这使管理员能够确定请求将转到哪个 server 块。他们还可以根据请求 URI 确定将被选择的 location 块。总的来说,它还使管理员能够在处理每个请求时追踪 Nginx 应用的上下文。
最后,您可以查看我们的博客上专注于 Nginx 的其他教程。它们将帮助您更好地受益于这款世界上最流行的 Web 服务器之一:
- Web 服务器的世界:Apache vs. Nginx
- 如何在 Ubuntu 20.04 上使用 Let’s Encrypt 保护 Nginx 安全
- 自动为 Nginx 续订 LetsEncrypt SSL 证书
- 使用 Docker Compose 部署 Laravel、Nginx 和 MySQL
祝您使用愉快!
评论
暂无评论。发表第一条评论吧。