Nginx proxy_pass反向代理与upstream负载均衡配置详解

本文从实际配置场景出发,深入解析Nginx proxy_pass指令与upstream模块的结合使用,涵盖语法规则、后端组定义、负载均衡算法、常见陷阱及调优参数,帮助运维与开发者快速搭建高可用反向代理层。

Nginx proxy_pass反向代理与upstream负载均衡配置详解
封面图:ZuCDN · ZuCDN 原创

从一次504错误说起

线上流量突增,某PHP应用的后端服务器开始出现间歇性超时。运维同学在Nginx错误日志里看到大量upstream timed out记录。检查配置后发现proxy_pass指向的单台服务器已成为瓶颈,而未使用upstream做后端组。这让我意识到:多数人会用proxy_pass http://backend,但真正理解upstream块proxy_pass配合规则的人并不多。本文不讲理论翻译,只讲你调试时一定会遇到的那些坑。

upstream块:必须放在http层

在Nginx配置中,upstream块只能出现在http上下文内,不能放在serverlocation里。一个典型示例:

http {
    upstream myapp {
        server 192.168.1.10:8080 weight=3;
        server 192.168.1.11:8080;
        server 192.168.1.12:8080 backup;
    }

    server {
        listen 80;
        location / {
            proxy_pass http://myapp;
        }
    }
}

这里proxy_pass http://myapp中的myapp必须与upstream定义的名称完全一致。若upstream名称包含下划线或特殊字符,建议用引号包裹测试,但官方推荐使用字母数字和下划线。

server指令参数详解

  • weight:权重,默认1,越大分配请求越多
  • max_fails:允许失败次数,默认1,超过后Nginx将标记节点不可用
  • fail_timeout:节点被标记为不可用后的冷却时间,默认10秒
  • backup:标记为备用节点,仅在主节点全部不可用时启用
  • down:永久标记为不可用,用于维护时临时移除而不修改配置

注意max_failsfail_timeout搭配使用才能生效。很多新手只配了max_fails=3却忽略fail_timeout,导致节点频发切换。

proxy_pass的URI陷阱

proxy_pass是否带URI(即/后面的路径)直接影响转发行为。看三个常见写法:

不带URI:原样转发

location /api/ {
    proxy_pass http://backend;
}
# 请求 /api/user -> 转发到 /api/user

此时upstream里的路径完全由客户端请求决定。如果后端服务需要剥离前缀,就必须用以下写法。

带URI:替换location匹配部分

location /api/ {
    proxy_pass http://backend/newapi/;
}
# 请求 /api/user -> 转发到 /newapi/user

注意:proxy_pass的URI部分会直接替换掉location匹配到的路径。如果location使用正则表达式,那么带URI的proxy_pass会报错(除非使用rewrite配合)。官方文档明确:如果proxy_pass包含URI,且location使用正则,则不会自动替换,需要手动rewrite。

根location的特殊性

location / {
    proxy_pass http://backend/;
}
# 请求 / -> 转发到 /  (因为location匹配的是/,被替换为空再拼接/)
# 实际测试:location / 时带URI的proxy_pass会将/请求转发到 / 根路径,但看似没区别

一个常见错误是:所有请求都加上/导致后端路径多一层斜杠。例如proxy_pass http://myapp/如果后端本身期望/api,却收到//api(双斜杠),某些框架会报400。所以建议根据后端实际情况测试。

负载均衡算法:不止轮询

upstream默认使用加权轮询(weighted round-robin)。多数场景够用,但以下算法可定向配置:

指令行为典型场景
least_conn转发给当前活跃连接数最少的节点长连接服务如WebSocket
ip_hash对客户端IP进行哈希,固定转发到同一节点需要session持久化的应用(无共享会话时)
hash $cookie_xxx自定义哈希键,如cookie、uri等基于用户标识的粘性会话
random two least_conn随机选取两个节点,再选连接数少的高并发下避免热点,但又保持相对均匀

配置方式:在upstream块内使用上述指令。注意ip_hashbackup节点配合时,backup节点只有在主节点全部不可用时才会接收请求,而哈希规则仅对主节点生效。

ip_hash的局限性

如果客户端经过CDN或代理,所有请求的IP可能相同,导致流量全部打到一个后端节点。此时应改用hash $http_x_forwarded_for,或者使用sticky模块(Nginx Plus或第三方模块)。开源版Nginx没有内置sticky,可通过cookie模块代替:

upstream backend {
    hash $cookie_route consistent;
    server 10.0.0.1;
    server 10.0.0.2;
}

由应用在下发cookie时设定route值,配合consistent(一致性哈希)在节点变更时影响最小。

proxy_pass与HTTPS后端

后端服务可能是HTTPS,此时upstream中需指定端口443:

upstream secure {
    server 192.168.1.10:443;
}
server {
    location / {
        proxy_pass https://secure;
        proxy_ssl_verify off;  # 如果后端证书是自签的,需要关闭验证
    }
}

注意:proxy_pass的协议必须与后端一致,如果后端是HTTP则写http://,是HTTPS则写https://。否则Nginx直接报502错误。另外,反向代理到HTTPS后端时需配置proxy_ssl_nameproxy_ssl_server_name以避免SNI问题。

常见故障场景与验证方法

配置完upstream后,务必执行以下验证:

  1. 语法检查nginx -t 输出成功方可重载
  2. 请求分发验证:在每台后端返回不同header,如add_header X-Backend $hostname;,再用curl -I查看
  3. 超时回滚:模拟一台后端宕机,观察请求是否自动切换到健康节点
  4. 日志监控:访问日志中增加$upstream_addr$upstream_status$upstream_response_time字段

举个例子,在日志格式中加入:

log_format upstreamlog '$remote_addr - $remote_user [$time_local] '
                      '"$request" $status $body_bytes_sent '
                      '"$http_referer" "$http_user_agent" '
                      'upstream: $upstream_addr status=$upstream_status '
                      'response_time=$upstream_response_time';
access_log /var/log/nginx/access.log upstreamlog;

从日志可以清晰看到每个请求的是哪个后端IP、响应状态码以及处理耗时。

调优参数:防止雪崩

高并发下,upstream层有三个参数容易成为瓶颈:

  • proxy_connect_timeout:与后端建立连接的超时时间,默认60秒,内网建议5秒
  • proxy_read_timeout:从后端读取响应的超时,默认60秒。如果后端处理慢,可以适当增大到120秒,但需配合应用日志排查
  • proxy_send_timeout:向后端发送请求的超时,默认60秒
  • keepalive:upstream块内单独配置,用于复用后端连接。如keepalive 32;,需配合proxy_http_version 1.1;proxy_set_header Connection ""

一个高效的反向代理配置示例:

http {
    upstream app {
        server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
        server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
        keepalive 64;
    }
    server {
        listen 80;
        location / {
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_pass http://app;
            proxy_connect_timeout 5s;
            proxy_read_timeout 30s;
        }
    }
}

注意:keepalive参数只会维持Nginx到后端的连接池,并不影响客户端到Nginx的连接。很多误解认为它优化用户长连接,实际上它降低的是Nginx与后端之间的TCP握手开销。

回滚与灰度发布实战

在生产环境中修改upstream配置可能引发连锁反应。一个安全做法:

  1. 先新增一台服务器到upstream,权重设为0(server IP weight=0;),但Nginx不支持weight=0,需要临时标记为down
  2. server IP down;加入配置,重载后新增节点不接收流量
  3. 移除down再重载,节点开始接收流量。如果出现问题,立刻加回down并重载
  4. 对于核弹级回滚,建议保留旧版本的upstream块,通过include指令动态切换:include /etc/nginx/upstream_current.conf;,修改只需替换此文件再重载

回滚时要注意nginx -s reload是优雅重载,不会中断现有连接。但如果新旧配置中upstream名称相同但后端列表变化,Nginx会立刻使用新列表处理新连接,旧连接仍由旧worker处理。这在实际中几乎没有副作用,但若节点摘除太快,正在处理的长请求可能被中断。

总结

proxy_pass + upstream是Nginx反向代理的核心组合,配置本身并不复杂,但URI替换规则、负载均衡算法选择、超时与健康检查参数往往成为隐患。建议每次修改后执行单元测试,并用日志验证分发逻辑。记住:配置文件中的每一行都有原因,不带理解地抄配置才是线上故障的根源。

延伸阅读