Nginx:关于 proxy_pass 的一切
首先,关于 https 的说明
一个简单的例子
削减还是不削减
$uri 和 $request_uri
捕获正则表达式
使用 try_files 和 WebApp 作为后备
即使并非所有上游主机都可用,也让 nginx 启动
更好的 proxy_pass 日志格式
结论
随着微服务™的出现,入口路由和服务间路由的需求日益增长。目前我默认使用nginx来实现这一点——虽然没有合理的理由或经验支持这个决定,但仅仅因为它似乎是目前使用最广泛的工具。
然而,这个经常需要用到的proxy_pass
指令却让我抓狂,因为它的行为对我来说太不直观了。所以我决定记录一下它的工作原理、它能做什么,以及如何规避它的一些怪癖。
首先,关于 https 的说明
默认情况下,proxy_pass
如果端点是 https 协议,则不验证其证书(这怎么可能是默认行为呢?!)。这在内部可能很有用,但通常情况下,您需要非常明确地执行此操作。如果您使用公共路由端点(我以前就这样做过),请确保将其设置proxy_ssl_verify
为on
。您还可以使用客户端证书等对上游服务器进行身份验证proxy_pass
,请务必查看https://docs.nginx.com/nginx/admin-guide/security-controls/securing-http-traffic-upstream/上的可用选项。
一个简单的例子
通常情况下,当一个 nginx 实例处理多项任务,并将部分请求委托给其他服务器时,会使用A 模式proxy_pass
。例如,Kubernetes 集群中的入口 (Ingress) 会将请求分散到负责特定位置的不同微服务中。或者,您可以使用 nginx 直接为前端交付静态文件,而一些服务器端渲染的内容或 API 则由 Web 应用(例如 ASP.NET Core 或 Flask)交付。
假设我们有一个在http://localhost:5000上运行的 WebApp,并希望它在http://localhost:8080/webapp/上可用,下面是我们在一个最小的 nginx.conf 中如何做到这一点:
daemon off;
events {
}
http {
server {
listen 8080;
location /webapp/ {
proxy_pass http://127.0.0.1:5000/api/;
}
}
}
您可以将其保存到文件中,例如 nginx.conf,然后使用以下命令运行它
nginx -c $(pwd)/nginx.conf
。
现在,您可以访问http://localhost:8080/webapp/,所有请求都会转发到http://localhost:5000/api/。
请注意,/webapp/ 前缀是如何被 nginx “切掉”的。这就是 location 的工作原理:它们切掉规范中指定的部分location
,然后将剩余部分传递给“上游”。“上游”指的是 nginx 后面的任何东西。
削减还是不削减
除了在proxy_pass
上游定义中使用变量的情况(我们将在下文中学习),位置和上游定义非常简单地联系在一起。这就是为什么你需要注意斜线,因为如果使用不当,可能会发生一些奇怪的事情。
下表显示了 Web 应用如何接收请求,具体取决于你如何编写location
和proxy_pass
声明。假设所有请求都发送到http://localhost:8080:
地点 | proxy_pass | 要求 | 上游已接收 |
---|---|---|---|
/webapp/ | http://localhost:5000/api/ | /webapp/foo?bar=baz | /api/foo?bar=baz |
/webapp/ | http://localhost:5000/api | /webapp/foo?bar=baz | /apifoo?bar=baz |
/webapp | http://localhost:5000/api/ | /webapp/foo?bar=baz | /api//foo?bar=baz |
/webapp | http://localhost:5000/api | /webapp/foo?bar=baz | /api/foo?bar=baz |
/webapp | http://localhost:5000/api | /webappfoo?bar=baz | /apifoo?bar=baz |
换句话说:通常情况下,你总是需要一个尾部斜杠,而不是混合使用带斜杠和不带斜杠的情况,并且只有在需要连接某个路径组件时才不需要尾部斜杠(我猜这种情况很少见)。注意查询参数是如何保存的!
$uri 和 $request_uri
您有两种方法来避免被location
切断:首先,您可以简单地重复定义中的位置proxy_pass
,这很容易:
location /webapp/ {
proxy_pass http://127.0.0.1:5000/api/webapp/;
}
这样,您的上游 WebApp 将在上面的示例中接收 /api/webapp/foo?bar=baz。
重复位置的另一种方法是使用 $uri 或 $request_uri。区别在于 $request_uri 会保留查询参数,而 $uri 则会丢弃它们:
地点 | proxy_pass | 要求 | 上游已接收 |
---|---|---|---|
/webapp/ | http://localhost:5000/api$request_uri | /webapp/foo?bar=baz | /api/webapp/foo?bar=baz |
/webapp/ | http://localhost:5000/api$uri | /webapp/foo?bar=baz | /api/webapp/foo |
请注意proxy_pass
,在定义中,“api”与 $request_uri 或 $uri 之间没有斜杠。这是因为完整的 URI 总是会以斜杠开头,如果你写成“api/$uri”,就会导致出现双斜杠。
捕获正则表达式
虽然这并非独有proxy_pass
,但我发现使用正则表达式将请求的部分内容转发到上游 Web 应用或重新格式化它通常很方便。例如:您的公共 URI 应该是http://localhost:8080/api/cart/items/123 ,而您的上游 API 会以http://localhost:5000/cart_api?items=123的形式处理它。在这种情况下,或者在更复杂的情况下,您可以使用正则表达式捕获请求 URI 的部分内容并将其转换为所需的格式。
location ~ ^/api/cart/([a-z]*)/(.*)$ {
proxy_pass http://127.0.0.1:5000/cart_api?$1=$2;
}
使用 try_files 和 WebApp 作为后备
我遇到的一个用例是,我希望 nginx 处理文件夹中的所有静态文件,如果该文件不可用,则将请求转发到后端。例如,我有一个通过 Flask 交付的 Vue 单页应用程序 (SPA),由于主 HTML 需要一些服务器端调整,所以我希望用 nginx 而不是 Flask 来处理静态文件。(这是gunicorn 官方文档推荐的做法。)
除了 /app/wwwroot/ 上提供的 index.html 之外,您的 SPA 可能已包含所有内容,而http://localhost:5000/将提供经过服务器调整的 index.html。
您可以按照以下步骤操作:
location /spa/ {
root /app/wwwroot/;
try_files $uri @backend;
}
location @backend {
proxy_pass http://127.0.0.1:5000;
}
请注意,由于某些原因,您无法proxy_pass
在 @backend 指令中指定任何路径。Nginx 会提示您:
nginx: [emerg] "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block in /home/daniel/projects/nginx_blog/nginx.conf:28
这就是为什么您的后端应该接收任何请求并返回其 index.html,或者至少返回由前端路由器处理的路由。
即使并非所有上游主机都可用,也让 nginx 启动
我目前使用 127.0.0.1 而不是 localhost 的原因之一是 nginx 对主机名解析非常挑剔。由于某些无法解释的原因,nginx 会proxy_pass
在启动时尝试解析指令中定义的所有主机,如果这些主机无法访问,则会导致启动失败。然而,尤其是在微服务环境中,要求所有上游服务在入口、负载均衡器或某些中间路由器启动时都可用是非常不现实的。
您可以通过在指令中使用变量来规避 nginx 启动时所有主机都可用的要求proxy_pass
。然而,出于某些难以理解的原因,如果您这样做,则需要专门的resolver
指令来解析这些路径。对于 Kubernetes,您可以在此处使用 kube-dns.kube-system。对于其他环境,您可以使用内部 DNS,或者对于公共路由的上游服务,您甚至可以使用公共 DNS,例如 1.1.1.1 或 8.8.8.8。
此外,使用变量会proxy_pass
彻底改变 URI 向上游传递的方式。当仅仅改变
proxy_pass https://localhost:5000/api/;
到
set $upstream https://localhost:5000;
proxy_pass $upstream/api/;
……你可能以为结果应该完全一样,但你可能会感到惊讶。前者会将/api/foo?bar=baz
我们示例中的请求发送到你的上游服务器/webapp/foo?bar=baz
。然而,后者会将请求发送到你的上游服务器/api/
。没有 foo,没有 bar,也没有 baz。:-(
我们需要通过将请求分为两部分来解决这个问题:第一部分是位置前缀后的路径,第二部分是查询参数。第一部分可以使用我们上面学到的正则表达式捕获,第二部分(查询参数)可以使用内置变量$is_args
和进行转发$args
。如果我们把它们放在一起,最终会得到如下配置:
daemon off;
events {
}
http {
server {
access_log /dev/stdout;
error_log /dev/stdout;
listen 8080;
# My home router in this case:
resolver 192.168.178.1;
location ~ ^/webapp/(.*)$ {
# Use a variable so that localhost:5000 might be down while nginx starts:
set $upstream http://localhost:5000;
# Put together the upstream request path using the captured component after the location path, and the query parameters:
proxy_pass $upstream/api/$1$is_args$args;
}
}
}
虽然 localhost 不是一个很好的例子,但它也适用于你服务的任意 DNS 名称。我发现这在生产环境中非常有价值,因为在处理生产问题时,如果 nginx 因为一个可能不太重要的服务而拒绝启动,可能会相当麻烦。然而,这会使 location 指令变得更加复杂。它从一个简单的location /webapp/
指令proxy_pass http://localhost/api/
变成了如此庞大的指令。不过,我认为这是值得的。
更好的 proxy_pass 日志格式
为了调试问题,或者只是为了在将来调查问题时手头有足够的信息,您可以最大限度地利用有关正在发生的事情的location
信息proxy_pass
。
我发现这个很方便log_format
,我添加了一个自定义变量 $upstream,就像我们上面定义的一样。如果你在所有使用 的位置都把变量命名为 $upstream proxy_pass
,那么你可以使用它log_format
,这样你的日志中通常就能包含很多你所需要的信息:
log_format upstream_logging '[$time_local] $remote_addr - $remote_user - $server_name to: $upstream: $request upstream_response_time $upstream_response_time msec $msec request_time $request_time';
这是一个完整的例子:
daemon off;
events {
}
http {
log_format upstream_logging '[$time_local] $remote_addr - $remote_user - $server_name to: "$upstream": "$request" upstream_response_time $upstream_response_time msec $msec request_time $request_time';
server {
listen 8080;
location /webapp/ {
access_log /dev/stdout upstream_logging;
set $upstream http://127.0.0.1:5000/api/;
proxy_pass $upstream;
}
}
}
但是,我还没有找到记录转发到 $upstream 的实际 URI 的方法,这是调试proxy_pass
问题时需要了解的最重要的事情之一。
结论
我希望您在本文中找到有用的信息,以便在开发和生产 nginx 配置中充分利用。
文章来源:https://dev.to/danielkun/nginx-everything-about-proxypass-2ona