介绍
在本指南中,我们将讨论 Nginx 的 http 代理功能,它允许 Nginx 将请求传递到后端 http 服务器以进行进一步处理。 Nginx 通常被设置为反向代理解决方案,以帮助扩展基础架构或将请求传递到其他非设计用于处理大型客户端负载的服务器。
在此过程中,我们将讨论如何使用 Nginx 内置的负载均衡功能进行横向扩展。 我们还将探索缓冲和缓存,以提高客户端代理操作的性能。
一般代理信息
如果您过去仅将 Web 服务器用于简单的单一服务器配置,您可能想知道为什么需要代理请求。
从 Nginx 代理到其他服务器的一个原因是能够扩展您的基础架构。 Nginx 旨在同时处理许多并发连接。 这使其成为客户联络点的理想选择。 服务器可以将请求传递给任意数量的后端服务器来处理大部分工作,从而将负载分散到您的基础架构中。 此设计还为您提供了轻松添加后端服务器或根据需要将其关闭以进行维护的灵活性。
http 代理可能有用的另一个实例是使用可能不是为直接处理来自生产环境中客户端的请求而构建的应用程序服务器。 许多框架都包含 Web 服务器,但它们中的大多数不如 Nginx 等为高性能而设计的服务器那么健壮。 将 Nginx 放在这些服务器之前可以为用户带来更好的体验并提高安全性。
Nginx 中的代理是通过操纵针对 Nginx 服务器的请求并将其传递给其他服务器进行实际处理来完成的。 请求的结果被传回 Nginx,然后 Nginx 将信息传递给客户端。 此实例中的其他服务器可以是远程机器、本地服务器,甚至是 Nginx 中定义的其他虚拟服务器。 Nginx 代理请求的服务器称为 上游服务器 。
Nginx 可以将请求代理到使用 http(s)、FastCGI、SCGI 和 uwsgi 或 memcached 协议通过针对每种代理类型的单独指令集进行通信的服务器。 在本指南中,我们将重点介绍 http 协议。 Nginx 实例负责传递请求并将任何消息组件按摩成上游服务器可以理解的格式。
解构基本 HTTP 代理通行证
最直接的代理类型涉及将请求传递给可以使用 http 进行通信的单个服务器。 这种类型的代理被称为通用“代理通行证”,由恰当命名的 proxy_pass
指令处理。
proxy_pass
指令主要在位置上下文中找到。 它在位置上下文中的 if
块和 limit_except
上下文中也有效。 当请求与内部带有 proxy_pass
指令的位置匹配时,请求将被转发到指令给出的 URL。
我们来看一个例子:
# server context location /match/here { proxy_pass http://example.com; } . . .
在上面的配置片段中,proxy_pass
定义中的服务器末尾没有给出 URI。 对于符合此模式的定义,客户端请求的 URI 将按原样传递给上游服务器。
例如,当此块处理对 /match/here/please
的请求时,请求 URI 将作为 http://example.com/match/here/please
发送到 example.com
服务器。
让我们看一下替代方案:
# server context location /match/here { proxy_pass http://example.com/new/prefix; } . . .
在上面的示例中,代理服务器在末尾定义了一个 URI 段 (/new/prefix
)。 当在 proxy_pass
定义中给出 URI 时,与 location 定义匹配的请求部分在传递期间被此 URI 替换。
例如,在 Nginx 服务器上对 /match/here/please
的请求将作为 http://example.com/new/prefix/please
传递给上游服务器。 /match/here
被 /new/prefix
取代。 这是需要牢记的重要一点。
有时,这种替换是不可能的。 在这些情况下,proxy_pass
定义末尾的 URI 将被忽略,来自客户端的原始 URI 或由其他指令修改的 URI 将被传递给上游服务器。
例如,当使用正则表达式匹配位置时,Nginx 无法确定 URI 的哪一部分与表达式匹配,因此它发送原始客户端请求 URI。 另一个例子是在同一位置使用重写指令,导致客户端 URI 被重写,但仍在同一块中处理。 在这种情况下,将传递重写的 URI。
了解 Nginx 如何处理标头
可能无法立即清楚的一件事是,如果您希望上游服务器正确处理请求,那么传递的不仅仅是 URI 也很重要。 代表客户端来自 Nginx 的请求看起来与直接来自客户端的请求不同。 其中很大一部分是与请求一起出现的标头。
当 Nginx 代理请求时,它会自动对从客户端接收到的请求标头进行一些调整:
- Nginx 摆脱了任何空头。 将空值传递给另一个服务器是没有意义的; 它只会使请求膨胀。
- 默认情况下,Nginx 会将任何包含下划线的标头视为无效。 它将从代理请求中删除这些。 如果您希望 Nginx 将这些解释为有效,您可以将
underscores_in_headers
指令设置为“on”,否则您的标头将永远无法到达后端服务器。 - “Host”标头被重写为
$proxy_host
变量定义的值。 这将是上游的 IP 地址或名称和端口号,直接由proxy_pass
指令定义。 - “连接”标题更改为“关闭”。 此标头用于发送有关两方之间建立的特定连接的信息。 在这种情况下,Nginx 将此设置为“关闭”,以向上游服务器指示一旦原始请求得到响应,此连接将被关闭。 上游不应该期望这个连接是持久的。
我们可以从上面推断的第一点是,您 不 想要传递的任何标头都应该设置为空字符串。 具有空值的标头将从传递的请求中完全删除。
从上述信息中可以看出的下一点是,如果您的后端应用程序将处理非标准标头,您必须确保它们 没有 有下划线。 如果您需要使用下划线的标头,您可以在配置中将 underscores_in_headers
指令设置为“on”(在 http 上下文或 IP 地址的默认服务器声明的上下文中有效/端口组合)。 如果您不这样做,Nginx 会将这些标头标记为无效并在传递到上游之前静默删除它们。
“Host”标头在大多数代理场景中都特别重要。 如上所述,默认情况下,这将设置为 $proxy_host
的值,该变量将包含直接取自 proxy_pass
定义的域名或 IP 地址和端口。 这是默认选择的,因为它是 Nginx 可以确保上游服务器响应的唯一地址(因为它是直接从连接信息中提取的)。
“主机”标头的最常见值如下:
$proxy_host
:这将“主机”标头设置为从proxy_pass
定义中获取的域名或 IP 地址和端口组合。 从 Nginx 的角度来看,这是默认的和“安全的”,但通常不是代理服务器正确处理请求所需要的。$http_host
:将“Host”标头设置为来自客户端请求的“Host”标头。 客户端发送的标头在 Nginx 中始终作为变量可用。 变量将以$http_
前缀开头,后跟小写的标题名称,任何破折号都替换为下划线。 尽管$http_host
变量在大多数情况下都有效,但当客户端请求没有有效的“Host”标头时,这可能会导致传递失败。$host
:此变量按优先顺序设置:请求行本身的主机名、客户端请求的“Host”标头或与请求匹配的服务器名。
在大多数情况下,您需要将“Host”标头设置为 $host
变量。 它是最灵活的,通常会为代理服务器提供尽可能准确填写的“主机”标头。
设置或重置标题
要调整或设置代理连接的标头,我们可以使用 proxy_set_header
指令。 例如,要更改我们讨论过的“Host”标头,并添加一些与代理请求常见的附加标头,我们可以使用如下内容:
# server context location /match/here { proxy_set_header HOST $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://example.com/new/prefix; } . . .
上述请求将“Host”标头设置为 $host
变量,该变量应包含有关被请求的原始主机的信息。 X-Forwarded-Proto
标头为代理服务器提供有关原始客户端请求架构的信息(无论是 http 还是 https 请求)。
X-Real-IP
设置为客户端的 IP 地址,以便代理可以根据此信息正确做出决策或记录。 X-Forwarded-For
标头是一个列表,其中包含到目前为止客户端已通过代理的每个服务器的 IP 地址。 在上面的示例中,我们将其设置为 $proxy_add_x_forwarded_for
变量。 该变量采用从客户端检索到的原始 X-Forwarded-For
标头的值,并将 Nginx 服务器的 IP 地址添加到末尾。
当然,我们可以将 proxy_set_header
指令移到服务器或 http 上下文中,允许在多个位置引用它:
# server context proxy_set_header HOST $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location /match/here { proxy_pass http://example.com/new/prefix; } location /different/match { proxy_pass http://example.com; }
为负载平衡代理连接定义上游上下文
在前面的示例中,我们演示了如何对单个后端服务器进行简单的 http 代理。 Nginx 允许我们通过指定可以将请求传递到的整个后端服务器池来轻松扩展此配置。
我们可以通过使用 upstream
指令来定义服务器池来做到这一点。 此配置假定列出的任何一个服务器都能够处理客户端的请求。 这使我们几乎可以毫不费力地扩展我们的基础架构。 upstream
指令必须在 Nginx 配置的 http 上下文中设置。
让我们看一个简单的例子:
# http context upstream backend_hosts { server host1.example.com; server host2.example.com; server host3.example.com; } server { listen 80; server_name example.com; location /proxy-me { proxy_pass http://backend_hosts; } }
在上面的示例中,我们设置了一个名为 backend_hosts
的上游上下文。 定义后,此名称将可在代理通行证中使用,就像它是常规域名一样。 如您所见,在我们的服务器块中,我们将向 example.com/proxy-me/...
发出的任何请求传递给我们上面定义的池。 在该池中,通过应用可配置算法来选择主机。 默认情况下,这只是一个简单的循环选择过程(每个请求将依次路由到不同的主机)。
更改上游平衡算法
您可以通过在上游上下文中包含指令或标志来修改上游池使用的平衡算法:
- (round robin):在没有其他平衡指令存在时使用的默认负载平衡算法。 上游上下文中定义的每个服务器依次按顺序传递请求。
- least_conn:指定应始终将新连接提供给活动连接数最少的后端。 这在与后端的连接可能会持续一段时间的情况下特别有用。
- ip_hash:这种平衡算法根据客户端的IP地址将请求分发到不同的服务器。 前三个八位字节用作决定服务器处理请求的关键。 结果是客户端倾向于每次都由同一个服务器提供服务,这有助于会话一致性。
- hash:这种平衡算法主要用于memcached代理。 服务器根据任意提供的散列键的值进行划分。 这可以是文本、变量或组合。 这是唯一需要用户提供数据的平衡方法,数据是应该用于散列的密钥。
更改平衡算法时,块可能如下所示:
# http context upstream backend_hosts { least_conn; server host1.example.com; server host2.example.com; server host3.example.com; } . . .
在上面的示例中,将根据连接最少的服务器来选择服务器。 ip_hash
指令可以以相同的方式设置以获得一定数量的会话“粘性”。
至于 hash
方法,您必须提供要散列的密钥。 这可以是任何你想要的:
# http context upstream backend_hosts { hash $remote_addr$remote_port consistent; server host1.example.com; server host2.example.com; server host3.example.com; } . . .
上面的示例将根据客户端 ip 地址和端口的值分发请求。 我们还添加了可选参数 consistent
,它实现了 ketama 一致性哈希算法。 基本上,这意味着如果您的上游服务器发生变化,对您的缓存的影响最小。
设置服务器权重以进行平衡
在后端服务器的声明中,默认情况下,每个服务器都是同等“加权”的。 这假设每个服务器可以并且应该处理相同数量的负载(考虑到平衡算法的影响)。 但是,您也可以在声明期间为服务器设置替代权重:
# http context upstream backend_hosts { server host1.example.com weight=3; server host2.example.com; server host3.example.com; } . . .
在上面的示例中,host1.example.com
将接收到其他两台服务器三倍的流量。 默认情况下,每个服务器都分配一个权重。
使用缓冲区释放后端服务器
与许多用户有关的代理问题之一是向进程添加额外服务器对性能的影响。 在大多数情况下,这可以通过利用 Nginx 的缓冲和缓存功能在很大程度上得到缓解。
代理到另一台服务器时,两个不同连接的速度会影响客户端的体验:
- 从客户端到 Nginx 代理的连接。
- 从 Nginx 代理到后端服务器的连接。
Nginx 能够根据您希望优化的这些连接中的任何一个来调整其行为。
没有缓冲区,数据从代理服务器发送并立即开始传输到客户端。 如果假定客户端速度很快,可以关闭缓冲以便尽快将数据发送到客户端。 使用缓冲区,Nginx 代理将临时存储后端的响应,然后将此数据提供给客户端。 如果客户端很慢,这允许 Nginx 服务器更快地关闭与后端的连接。 然后它可以处理以任何可能的速度将数据分发给客户端。
Nginx 默认采用缓冲设计,因为客户端往往具有截然不同的连接速度。 我们可以使用以下指令调整缓冲行为。 这些可以在 http、服务器或位置上下文中设置。 重要的是要记住,大小指令是根据请求配置 ',因此当有许多客户端请求时,将它们增加到超出您的需要可能会影响您的性能:
- proxy_buffering:此指令控制是否启用此上下文和子上下文的缓冲。 默认情况下,这是“开”。
- proxy_buffers:该指令控制代理响应的缓冲区的数量(第一个参数)和大小(第二个参数)。 默认配置 8 个缓冲区,大小等于一个内存页(
4k
或8k
)。 增加缓冲区的数量可以让您缓冲更多信息。 - proxy_buffer_size:来自后端服务器的响应的初始部分(包含标头)与响应的其余部分分开缓冲。 该指令设置这部分响应的缓冲区大小。 默认情况下,这将与
proxy_buffers
的大小相同,但由于它用于标题信息,因此通常可以将其设置为较低的值。 - proxy_busy_buffers_size:该指令设置可以标记为“客户端就绪”并因此忙碌的缓冲区的最大大小。 虽然客户端一次只能从一个缓冲区读取数据,但缓冲区被放置在队列中以成串发送给客户端。 该指令控制允许处于此状态的缓冲区空间的大小。
- proxy_max_temp_file_size:这是磁盘上临时文件的每个请求的最大大小。 这些是在上游响应太大而无法放入缓冲区时创建的。
- proxy_temp_file_write_size:这是当代理服务器的响应对于配置的缓冲区来说太大时,Nginx 将一次写入临时文件的数据量。
- proxy_temp_path:当来自上游服务器的响应无法放入配置的缓冲区时,这是 Nginx 应该存储任何临时文件的磁盘区域的路径。
如您所见,Nginx 提供了很多不同的指令来调整缓冲行为。 大多数情况下,您不必担心其中的大部分,但调整其中一些值会很有用。 可能最有用的调整是 proxy_buffers
和 proxy_buffer_size
指令。
增加每个上游请求的可用代理缓冲区数量,同时减少可能存储标头的缓冲区的示例如下所示:
# server context proxy_buffering on; proxy_buffer_size 1k; proxy_buffers 24 4k; proxy_busy_buffers_size 8k; proxy_max_temp_file_size 2048m; proxy_temp_file_write_size 32k; location / { proxy_pass http://example.com; }
相反,如果您有快速的客户端并希望立即向其提供数据,则可以完全关闭缓冲。 如果上游比客户端快,Nginx 实际上仍会使用缓冲区,但它会立即尝试将数据刷新到客户端,而不是等待缓冲区入池。 如果客户端速度很慢,这可能会导致上游连接保持打开状态,直到客户端能够赶上。 当缓冲“关闭”时,仅使用由 proxy_buffer_size
指令定义的缓冲区:
# server context proxy_buffering off; proxy_buffer_size 4k; location / { proxy_pass http://example.com; }
高可用性(可选)
Nginx 代理可以通过添加一组冗余的负载均衡器来变得更加健壮,从而创建一个高可用性的基础设施。
高可用性 (HA) 设置是没有单点故障的基础架构,您的负载平衡器是此配置的一部分。 通过拥有多个负载均衡器,您可以防止在您的负载均衡器不可用或您需要将其关闭以进行维护时出现潜在的停机时间。
这是一个基本的高可用性设置图:
在此示例中,您在一个静态 IP 地址后面有多个负载平衡器(一个主动和一个或多个被动),可以从一台服务器重新映射到另一台服务器。 客户端请求从静态 IP 路由到活动负载均衡器,然后路由到您的后端服务器。 要了解更多信息,请阅读 如何使用浮动 IP 的这一部分。
配置代理缓存以减少响应时间
虽然缓冲可以帮助释放后端服务器来处理更多请求,但 Nginx 还提供了一种缓存来自后端服务器的内容的方法,从而完全无需为许多请求连接到上游。
配置代理缓存
要设置缓存以用于代理内容,我们可以使用 proxy_cache_path
指令。 这将创建一个可以保存从代理服务器返回的数据的区域。 proxy_cache_path
指令必须在 http 上下文中设置。
在下面的示例中,我们将配置这个和一些相关的指令来设置我们的缓存系统。
# http context proxy_cache_path /var/lib/nginx/cache levels=1:2 keys_zone=backcache:8m max_size=50m; proxy_cache_key "$scheme$request_method$host$request_uri$is_args$args"; proxy_cache_valid 200 302 10m; proxy_cache_valid 404 1m;
使用 proxy_cache_path
指令,我们已经在文件系统上定义了一个目录,我们希望在其中存储我们的缓存。 在本例中,我们选择了 /var/lib/nginx/cache
目录。 如果此目录不存在,您可以通过键入以下内容以正确的权限和所有权创建它:
sudo mkdir -p /var/lib/nginx/cache sudo chown www-data /var/lib/nginx/cache sudo chmod 700 /var/lib/nginx/cache
levels=
参数指定缓存的组织方式。 Nginx 将通过散列一个键的值来创建一个缓存键(如下配置)。 我们在上面选择的级别规定将创建一个带有两个字符子目录(取自散列值末尾的下两个字符)的单个字符目录(这将是散列值的最后一个字符)。 你通常不必关心这个细节,但它可以帮助 Nginx 快速找到相关的值。
keys_zone=
参数定义了这个缓存区的名称,我们称之为 backcache
。 这也是我们定义要存储多少元数据的地方。 在本例中,我们存储了 8 MB 的密钥。 对于每兆字节,Nginx 可以存储大约 8000 个条目。 max_size
参数设置实际缓存数据的最大大小。
我们在上面使用的另一个指令是 proxy_cache_key
。 这用于设置将用于存储缓存值的键。 这个相同的键用于检查是否可以从缓存中提供请求。 我们将其设置为方案(http 或 https)、HTTP 请求方法以及请求的主机和 URI 的组合。
proxy_cache_valid
指令可以指定多次。 它允许我们根据状态码配置值存储多长时间。 在我们的示例中,我们将成功和重定向存储 10 分钟,并为每分钟 404 响应过期缓存。
现在,我们已经配置了缓存区域,但是我们仍然需要告诉 Nginx 什么时候使用缓存。
在我们代理到后端的位置,我们可以配置此缓存的使用:
# server context location /proxy-me { proxy_cache backcache; proxy_cache_bypass $http_cache_control; add_header X-Proxy-Cache $upstream_cache_status; proxy_pass http://backend; } . . .
使用 proxy_cache
指令,我们可以指定 backcache
缓存区域应用于此上下文。 Nginx 在传递到后端之前会在这里检查一个有效的条目。
proxy_cache_bypass
指令设置为 $http_cache_control
变量。 这将包含一个关于客户端是否明确请求资源的新的、非缓存版本的指示符。 设置此指令允许 Nginx 正确处理这些类型的客户端请求。 无需进一步配置。
我们还添加了一个名为 X-Proxy-Cache
的额外标题。 我们将此标头设置为 $upstream_cache_status
变量的值。 基本上,这设置了一个标头,允许我们查看请求是否导致缓存命中、缓存未命中或缓存是否被显式绕过。 这对于调试特别有价值,但对客户端来说也是有用的信息。
关于缓存结果的说明
缓存可以极大地提高代理的性能。 但是,在配置缓存时,一定要牢记一些注意事项。
首先,任何与用户相关的数据都应该 而不是 被缓存。 这可能导致一个用户的数据被呈现给另一个用户。 如果您的网站是完全静态的,这可能不是问题。
如果您的站点有一些动态元素,则必须在后端服务器中考虑这一点。 您如何处理这取决于正在处理后端处理的应用程序或服务器。 对于私有内容,您应该根据数据的性质将 Cache-Control
标头设置为“no-cache”、“no-store”或“private”:
- no-cache:表示在没有先检查后端数据没有改变的情况下,不应再次提供响应。 如果数据是动态且重要的,则可以使用此方法。 每个请求都会检查一个 ETag 散列元数据标头,如果后端返回相同的散列值,则可以提供先前的值。
- no-store:表示在任何时候都不应缓存接收到的数据。 这是私有数据最安全的选择,因为这意味着每次都必须从服务器检索数据。
- private:这表示没有共享缓存空间应该缓存此数据。 这对于指示用户的浏览器可以缓存数据很有用,但代理服务器不应认为此数据对后续请求有效。
- public:这表示响应是公共数据,可以在连接中的任何点缓存。
可以控制此行为的相关标头是 max-age
标头,它指示应缓存任何资源的秒数。
根据内容的敏感性正确设置这些标头将帮助您利用缓存,同时保持您的私人数据安全和动态数据新鲜。
如果您的后端也使用 Nginx,您可以使用 expires
指令设置其中的一些,这将为 Cache-Control
设置 max-age
:
location / { expires 60m; } location /check-me { expires -1; }
在上面的示例中,第一个块允许将内容缓存一个小时。 第二个块将 Cache-Control
标头设置为“no-cache”。 要设置其他值,您可以使用 add_header
指令,如下所示:
location /private { expires -1; add_header Cache-Control "no-store"; }
结论
Nginx 首先是一个反向代理,它也恰好具有作为 Web 服务器工作的能力。 由于这种设计决策,将请求代理到其他服务器是相当直接的。 Nginx 非常灵活,如果需要,可以对代理配置进行更复杂的控制。