Nginx:关于 proxy_pass 的一切 首先,关于 https 的说明 一个简单的例子 是否要斜线 $uri 和 $request_uri 捕获正则表达式 使用 try_files 和 WebApp 作为后备方案 即使并非所有上游主机都可用,也让 nginx 启动 proxy_pass 的更好的日志记录格式 结论

2025-05-24

Nginx:关于 proxy_pass 的一切

首先,关于 https 的说明

一个简单的例子

削减还是不削减

$uri 和 $request_uri

捕获正则表达式

使用 try_files 和 WebApp 作为后备

即使并非所有上游主机都可用,也让 nginx 启动

更好的 proxy_pass 日志格​​式

结论

随着微服务™的出现,入口路由和服务间路由的需求日益增长。目前我默认使用nginx来实现这一点——虽然没有合理的理由或经验支持这个决定,但仅仅因为它似乎是目前使用最广泛的工具。

然而,这个经常需要用到的proxy_pass指令却让我抓狂,因为它的行为对我来说太不直观了。所以我决定记录一下它的工作原理、它能做什么,以及如何规避它的一些怪癖。

首先,关于 https 的说明

默认情况下,proxy_pass如果端点是 https 协议,则不验证其证书(这怎么可能是默认行为呢?!)。这在内部可能很有用,但通常情况下,您需要非常明确地执行此操作。如果您使用公共路由端点(我以前就这样做过),请确保将其设置proxy_ssl_verifyon。您还可以使用客户端证书等对上游服务器进行身份验证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 应用如何接收请求,具体取决于你如何编写locationproxy_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
PREV
只需 10 分钟即可启动您的免费数据库和 API 服务!🔥🔥🔥(带有 GitHub 存储库)
NEXT
文档的重要性