Linux Nginx 怎么在 proxy_pass 中使用变量动态指定后端地址
前言proxy_pass最常见的写法是把后端地址写死在配置里proxy_pass http://10.0.0.5:8080;。但真实环境里后端地址经常需要变蓝绿发布要把一部分请求指向新集群多租户网关要根据请求头把请求分流到不同实例容器环境里后端 IP 每次重建都会变。这时候就会想到「能不能在 proxy_pass 后面放一个变量」。可以。Nginx 的proxy_pass允许把地址写成变量也可以配合map在请求阶段算出地址。但带变量之后有三处行为和写死地址时不同而这三点恰好最容易出故障请求 URI 怎么传给后端、域名形式的后端由谁解析、upstream块里的负载均衡策略还起不起作用。本文先讲清这三条规则再给一套可以落地的多环境动态后端配置最后附一份用curl实测验证的方法——这类规则只读文档容易理解偏亲手跑一遍最省事。示例环境RHEL 9 或 Ubuntu 22.04nginx 1.24用nginx -v确认。RHEL 系把片段放到/etc/nginx/conf.d/下的.conf文件Debian 系放到/etc/nginx/sites-enabled/也行内容放在http或server块内即可。一、带变量时 nginx 怎么处理请求 URI写死地址时nginx 在配置解析阶段就能确定「location 前缀」和「proxy_pass 里的 URI」之间的替换关系带变量时这个关系在解析阶段拿不到于是 nginx 改用另一套规则变量值里如果带了 URI就直接用它替换原始请求 URI变量值里没有 URI也就是http://主机:端口后面没有斜杠就把客户端请求的原始 URI 原样转发。四种写法对比假设客户端请求的是/api/users# A. 写死不带 URI后端收到 /api/users location /api/ { proxy_pass http://10.0.0.5:8080; } # B. 写死带 URI结尾斜杠location 前缀 /api/ 被替换掉后端收到 /users location /api/ { proxy_pass http://10.0.0.5:8080/; } # C. 变量变量值不带 URI后端收到 /api/users location /api/ { set $backend http://10.0.0.5:8080; proxy_pass $backend; } # D. 变量变量值带 URI结尾斜杠后端收到 / location /api/ { set $backend http://10.0.0.5:8080/; proxy_pass $backend; }结论想要 B 那种「把/api/前缀剥掉」的效果用变量是做不到的要么把路径自己算进变量要么用rewrite ... break先改 URI。最省心的做法是让变量只保存协议://主机:端口路径交给原始 URI 原样转发。D 这条规则建议以官方文档 proxy_pass 章节为准不同版本曾有过调整升级前按下文方法实测一次。想亲眼确认用一个会回显路径的后端最直接Python 3 标准库就够了不用额外装东西python3 - PYEOF import http.server, socketserver class Echo(http.server.BaseHTTPRequestHandler): def do_GET(self): body (upstream received path self.path \n).encode() self.send_response(200) self.send_header(Content-Type, text/plain) self.send_header(Content-Length, str(len(body))) self.end_headers() self.wfile.write(body) socketserver.TCPServer.allow_reuse_address True socketserver.TCPServer((127.0.0.1, 8080), Echo).serve_forever() PYEOFcurl -s http://127.0.0.1/api/users # 依次切换上面的 A~D 写法每次 nginx -t nginx -s reload 后再执行一次 # 对比输出里的 upstream received path 是什么二、变量里的域名由 resolver 解析如果变量值是http://api.svc.internal:8080这种域名必须显式配置resolver。Nginx 不会去读系统的/etc/resolv.conf这一点和很多反向代理不一样。server { listen 80; # 指向集群 DNS 服务器 IPvalid 控制解析结果的缓存时长 resolver 10.0.0.2 valid10s ipv6off; location /api/ { set $backend http://api.svc.internal:8080; proxy_pass $backend; } }三个要点resolver后面跟的是 DNS 服务器的 IP不是域名。写死 IP 的变量不需要它。这个指令可以写在http、server、location任意一层就近生效。症状很好认nginx -t能通过但请求返回 502error.log里有一行大意是no resolver defined to resolve 域名。看到 502 先tail /var/log/nginx/error.log别急着怀疑后端挂了。关于缓存nginx 会按valid指定的时长缓存解析结果不写valid时取 DNS 响应里的 TTL。所以 DNS 里改了记录可能要等缓存过期才生效。K8s 这类后端 IP 频繁变化的场景把valid设小一点比如 5 秒到 10 秒代价是 DNS 查询次数变多。三、实战按请求头把流量分到不同环境下面这套配置用map在请求处理阶段决定后端不用改配置、不用 reload 就能通过请求头切环境。适用于 nginx 1.24RHEL 9 与 Ubuntu 22.04 通用。# /etc/nginx/conf.d/dynamic_backend.conf # 根据请求头 X-Env 选择后端没有该头时走 default map $http_x_env $backend_addr { default http://127.0.0.1:8080; gray http://127.0.0.1:8081; canary http://127.0.0.1:8082; } server { listen 80 default_server; location /api/ { set $backend $backend_addr; proxy_pass $backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Connection ; proxy_connect_timeout 3s; proxy_read_timeout 30s; } }map的结果在请求处理阶段才求值所以同一个 worker 处理不同请求时会用不同的后端地址这正是「动态」的含义。验证步骤sudo nginx -t # 语法检查必须先通过 sudo nginx -s reload # 平滑生效 curl -s -H X-Env: gray http://127.0.0.1/api/users curl -s -H X-Env: canary http://127.0.0.1/api/users curl -s http://127.0.0.1/api/users # 走 default如果后端是域名而不是 IP在server块里补一行resolver见上一节。保留 upstream 的负载均衡如果后端是一组机器希望变量方式仍然享受upstream里的weight、max_fails、backup这些策略可以让变量只保存 upstream 组名upstream app_pool { server 10.0.0.11:8080 max_fails3 fail_timeout10s; server 10.0.0.12:8080 max_fails3 fail_timeout10s; keepalive 32; } server { listen 80; location /api/ { set $pool app_pool; proxy_pass http://$pool; proxy_http_version 1.1; proxy_set_header Connection ; } }nginx 会先在已定义的 upstream 组里查找这个名字找不到才走resolver去解析域名。这样既保留了动态切换也保留了组内的被动失败判定与负载均衡。上游客连接复用是否真正生效可以用ss -tn state established | grep 8080观察与后端之间的连接数变化自行确认不同版本行为有差异以官方文档为准。用模板生成配置文件如果后端地址来自编排系统注入的环境变量不要想着在 nginx 配置里直接读环境变量用模板加envsubst生成配置更可靠这也是 nginx 官方容器镜像的做法export BACKEND_HOST10.0.0.11:8080 envsubst ${BACKEND_HOST} /etc/nginx/templates/app.conf.template \ /etc/nginx/conf.d/app.conf sudo nginx -t sudo nginx -s reload模板文件里正常写proxy_pass http://${BACKEND_HOST};即可envsubst只替换你允许的那几个变量不会误伤$host、$remote_addr这类 nginx 自带变量。常见坑点❌set $backend http://10.0.0.5:8080/;再proxy_pass $backend;——变量值里的结尾斜杠被当作 URI后端只会收到/业务侧大面积 404。✅ 变量只存协议://主机:端口不加结尾斜杠让原始 URI 原样转发。❌ 变量值是域名却没配resolvernginx -t通过上线后全站 502。✅ 加resolver 10.0.0.2 valid10s ipv6off;并用tail -f /var/log/nginx/error.log确认不再出现解析类报错。❌ 变量值里忘了协议头只写10.0.0.5:8080。✅ 写成http://10.0.0.5:8080。缺协议时重载会直接失败报invalid URL prefix in ...。❌ 用if ($http_x_env gray) { proxy_pass ...; }在if里改代理目标。✅if在 location 内会生成隐式嵌套写在里面的proxy_pass容易丢掉外层继承的配置。用map算变量代理目标只出现在proxy_pass一行。❌map忘了写default请求头没带时变量为空请求 502。✅map块第一行固定写default http://127.0.0.1:8080;。❌ 变量方式下还指望proxy_pass http://某组名/;自带路径剥离验证时发现路径没变。✅ 用rewrite ^/api/(.*)$ /$1 break;显式改写 URI再交给proxy_pass转发。❌ 以为带变量的proxy_pass会自动享受upstream的负载均衡于是变量里填的是某一台机器的域名。✅ 变量填 upstream 组名见上一节或者明确接受「这个变量就是单点」。❌ 为了「保险」在变量后面拼$request_uri遇到经rewrite改过 URI 的场景转发出去的还是客户端最初的路径和后端期望的不一致。✅$request_uri保存的是客户端最初的原始 URI不受rewrite影响要转发改写后的 URI 请用$uri需要带查询串时用$uri$is_args$args并注意$uri是规范化并解码过的值。总结关注点地址写死proxy_pass用变量地址解析时机启动/重载时每个请求域名后端启动时解析一次失败则重载失败按resolver缓存解析失败时 502URI 处理location 前缀可被替换变量带 URI 就整体替换不带则原样转发upstream 负载均衡生效仅当变量值是 upstream 组名时生效切换方式改配置 reload改请求头或改map里的值典型故障配置写错导致重载失败502 error.log 里的解析错误动态后端换来的灵活性代价是丢掉了配置加载期的校验写死的地址在nginx -t时就报错变量里的地址要等真来了请求才知道对不对。所以配套的error.log监控和灰度验证不能省。选型上如果只是偶尔切一次环境改配置加 reload 更简单也更安全只有当「同一时刻要按请求分流到不同后端」时变量方式才真正划算。