Nginx 反向代理与负载均衡实战:upstream 配置与调优要点

反向代理是 Nginx 在生产环境里最常见的用法:对外只暴露一个入口,请求按规则转发到后端多台应用服务器,顺带做负载均衡、故障隔离和限流。本文把 upstream 的核心配置一次讲清,重点说明几个容易踩坑的口径问题,读完可以直接照着改自己的配置。

适用环境:Nginx 1.24+(开源版),Linux 发行版不限;后端为任意 HTTP 服务。文中后端节点用 app-01、app-02 表示,示例 IP 使用 192.0.2.0/24 文档地址段。

前置条件

  • Nginx 已安装并可正常启动,配置文件位于 /etc/nginx/nginx.conf;
  • 后端应用在 app-01(192.0.2.11)与 app-02(192.0.2.12)的 8080 端口提供 HTTP 服务;
  • 已准备好测试域名 api.example.com,DNS 解析到 Nginx 所在主机;
  • 操作账号具备 sudo 权限,修改配置前先备份原文件。

第一步:定义 upstream 与五种调度方式

upstream 块定义一组后端服务器,proxy_pass 把请求转发过去。调度方式通过在 upstream 内加指令选择,默认是轮询。

upstream backend_api {
    server 192.0.2.11:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 192.0.2.12:8080 weight=1 max_fails=3 fail_timeout=30s;
    server 192.0.2.13:8080 backup;
}

五种调度方式的适用场景如下:

  • 轮询(默认):按顺序依次分发,配合 weight 可按权重比例分配。适合后端机器配置一致、无状态的服务。
  • ip_hash:按客户端 IP 哈希,同一 IP 固定打到同一台后端。只适合后端存了本地会话且没做会话共享的过渡场景;NAT 出口后大量用户共用一个 IP 时会导致倾斜。
  • least_conn:把请求发给当前活跃连接数最少的节点。适合请求处理时长差异大的场景,比如有慢接口混跑。
  • hash $request_uri consistent:按 URI 哈希并启用一致性哈希,节点增减时只影响少量请求。适合缓存型后端(如图片、静态资源源站),能提高缓存命中率。
  • random two least_conn:随机抽两个节点,选连接数较少的那个。大规模集群下效果接近 least_conn 但开销更低,一般业务用不到。

server 行上的 max_fails 与 fail_timeout 是被动健康检查的一部分,后文细说。backup 表示备用节点,仅当其他节点都不可用时才启用。

第二步:开启与后端的长连接

默认情况下 Nginx 与后端之间每个请求新建一条 TCP 连接(HTTP/1.0 且带 Connection: close),高并发下会产生大量 TIME_WAIT 并打满端口。开启长连接需要三处配合,缺一不可:

upstream backend_api {
    server 192.0.2.11:8080;
    server 192.0.2.12:8080;
    keepalive 64;
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://backend_api;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

这里的坑是:keepalive 指令只定义连接池大小;如果不把 proxy_http_version 改为 1.1 并清空 Connection 头,Nginx 仍会按 HTTP/1.0 与后端建连,长连接不会生效。验证方法是在后端机器上用 ss -tn state established 观察连接数是否稳定复用,而不是随请求量线性增长。

第三步:超时与重试参数的口径

代理超时有三个,务必分清它们量的是哪段时间:

location / {
    proxy_pass http://backend_api;
    proxy_connect_timeout 3s;
    proxy_send_timeout 30s;
    proxy_read_timeout 30s;
    proxy_next_upstream error timeout http_502 http_504;
    proxy_next_upstream_tries 2;
    proxy_next_upstream_timeout 10s;
}
  • proxy_connect_timeout:与后端建立 TCP 连接的超时,一般设 2–5 秒,超过说明网络或后端监听有问题,不要设大。
  • proxy_send_timeout / proxy_read_timeout:发送请求、等待响应的超时,注意它们量的是「两次读/写操作之间的间隔」,不是整个请求的总时长。长连接下载大文件时不要误判;慢接口要单独在 location 里放宽。
  • proxy_next_upstream:什么情况换一台后端重试。默认只对 error 和 timeout 重试。对 POST 等非幂等请求要谨慎:请求可能已被前一台后端处理,盲目重试会造成重复下单之类的问题,业务侧应配合幂等设计,或显式加 non_idempotent 前确认风险。

第四步:传递真实客户端 IP

后端应用拿到的 $remote_addr 默认是 Nginx 的地址,真实客户端 IP 要靠头传递。标准做法是设置 X-Real-IP 与 X-Forwarded-For:

location / {
    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_add_x_forwarded_for 的行为是「把已有的 X-Forwarded-For 值拼上本次的 $remote_addr」,形成一条链:client, proxy1, proxy2。后端框架取真实 IP 时应取链上的第一个值,且只信任来自自家上一级代理的请求。如果链路前面还有一层 CDN 或 SLB,Nginx 本机拿到的 $remote_addr 是上层代理的 IP,这时要用 real_ip 模块还原:

set_real_ip_from 192.0.2.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

注意:set_real_ip_from 必须只写可信的上层代理网段,写成 0.0.0.0/0 等于允许任何人伪造 X-Forwarded-For,把 IP 白名单、限流、审计全部绕开。

第五步:健康检查的被动方式与局限

开源版 Nginx 没有主动健康检查(定时探测是商业版 Nginx Plus 或第三方模块的能力),只有被动检查:真实请求失败达到 max_fails 次,就在 fail_timeout 时间内把该节点标记为不可用,之后再放进来试探。局限要心里有数:

  • 没有真实流量经过时,不会发现后端已经挂了;
  • 「失败」的判定只看连接失败与超时等,返回 500 的应用层错误默认不算失败(可用 proxy_next_upstream 让它换台重试,但节点本身不会被摘除);
  • fail_timeout 内节点被完全跳过,恢复瞬间流量会集中打回来,敏感场景要配合限流。

对可用性要求高的场景,常见补法是外部脚本定时 curl 后端健康接口,异常时改配置并 reload,或直接把这层换成带主动检查的负载均衡器。

第六步:限流最小配置

limit_req 按速率限流,适合防刷接口;limit_conn 按并发连接数限流,适合保护下载、长连接类接口。两者都需要在 http 块先定义共享内存区:

http {
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    server {
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            limit_conn conn_limit 20;
            limit_req_status 429;
            proxy_pass http://backend_api;
        }
    }
}

rate=10r/s 表示每 IP 平均每秒 10 个请求;burst=20 允许突发排队 20 个,nodelay 让排队的请求立即处理而不是匀速放行(更符合实际体验)。超限默认返回 503,建议用 limit_req_status 改成语义更准确的 429。10m 的 zone 约可存 1.6 万个 IP 的状态,够中小站点使用。

第七步:日志格式与 502/504 排障

排障前先把关键变量记进日志,否则只能猜:

log_format upstream_log '$remote_addr - $request '
    'status=$status upstream_status=$upstream_status '
    'urt=$upstream_response_time req_time=$request_time '
    'upstream_addr=$upstream_addr';

access_log /var/log/nginx/api.access.log upstream_log;

502 与 504 的定位对照:

  • 502 Bad Gateway:Nginx 连上了后端,但后端提前断开、崩溃或返回了非法响应。看 $upstream_status 是否为空或为 502 本身,并去后端查进程存活与应用日志。常见于后端重启、OOM 被杀、或后端主动关掉长连接(后端 keepalive 超时比 Nginx 更短时会偶发)。
  • 504 Gateway Timeout:Nginx 在超时时间内没等到后端响应。看 urt 是否等于 proxy_read_timeout,等于说明后端处理慢或卡死;去后端的慢日志、线程栈里找。
  • 499:客户端在等响应时主动断开,通常是用户取消或前端超时更短,不是服务端故障。
  • 502 且 upstream_addr 为空:连后端都没选出来,往往是 upstream 全部节点被被动检查摘除,或 DNS 解析失败。

验证

nginx -t && nginx -s reload
curl -s -o /dev/null -w "%{http_code}\n" http://api.example.com/health
for i in $(seq 1 10); do curl -s http://api.example.com/whoami; done
tail -f /var/log/nginx/api.access.log

whoami 是后端返回自身主机名的测试接口,连续请求应轮流命中 app-01 与 app-02。停掉一台后端后再请求,应短暂出现重试日志,随后流量自动只打到存活节点,恢复后自动重新加入。

常见问题

  • 改了配置不生效:reload 前必须先 nginx -t 校验;另外确认请求命中的是哪个 server 块,用 curl 时带上正确的 Host(或直接访问域名),用 IP 访问会落到默认 server。
  • 偶发 502、后端日志却正常:多半是长连接竞态——后端先关掉了空闲连接,Nginx 复用这条连接发请求就报错。让 Nginx 侧的 keepalive_timeout 小于后端的连接保活时间,并在 proxy_next_upstream 中保留对这类错误的重试即可缓解。
  • 后端取到的 IP 全是同一个:检查后端框架是否读取了 X-Forwarded-For,以及链路中间是否还有一层代理没透传该头;同时确认应用监听的日志格式里打印的是不是 $http_x_forwarded_for。
  • 限流误伤严重:公司出口、校园网等 NAT 场景下大量用户共用一个 IP,按 IP 限流必然误伤。应改用更细的限流键(如 IP + 接口路径),或把限流下沉到带用户身份的网关层。

小结:upstream 调优的主线就是四件事——调度方式与业务形态匹配、长连接三件套配齐、超时与重试分清口径、真实 IP 链路可信。先把日志打全,再谈调优,502 和 504 的定位基本都在十分钟内能收口。

发表回复

后才能评论