反向代理与负载均衡

反向代理与负载均衡

写在前面

Nginx 最早就是做反向代理出身,这是它最经典的能力。本篇会讲清楚 proxy_pass 的 URL 重写规则、upstream 块、各种负载均衡策略的适用场景和坑,最后拿公司 API 网关的完整配置做拆解。

一、反向代理与正向代理

简单区分:

[Client] → [Proxy] → [Server]    正向代理
[Client] → [Proxy] → [Servers]   反向代理(代理多个 server)

二、proxy_pass 基础

location /api/ {
    proxy_pass http://backend;
}

请求 /api/users 会被转发到 http://backend/users(去掉 /api/ 前缀)。

2.1 URL 重写规则

proxy_pass 是否带 URI 决定了如何重写:

location /api/ {
    proxy_pass http://backend;        # 末尾无 /,保留完整 URI
    # /api/users → http://backend/api/users
}

location /api/ {
    proxy_pass http://backend/;       # 末尾有 /,去掉 location 前缀
    # /api/users → http://backend/users
}

location /api/ {
    proxy_pass http://backend/api;    # 替换为指定路径
    # /api/users → http://backend/api/users
}

这条规则最容易出错——加不加末尾的 / 完全改变行为。

2.2 实战坑:正则 location 的 proxy_pass

# NG - 正则 location 不能用 proxy_pass URL 重写
location ~ ^/api/(.*)$ {
    proxy_pass http://backend/$1;  # 必须用 $1 捕获组
}

正则 location 的 proxy_pass 末尾必须明确指定 URI,否则报错。

三、upstream 块

3.1 基本定义

upstream backend {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
    server 10.0.0.3:8080;
}

server {
    location / {
        proxy_pass http://backend;
    }
}

3.2 完整 upstream 配置

upstream backend {
    # 负载均衡策略
    ip_hash;  # 或者 least_conn, random

    # 节点定义
    server 10.0.0.1:8080 weight=5 max_fails=3 fail_timeout=30s;
    server 10.0.0.2:8080 weight=3 max_fails=3 fail_timeout=30s;
    server 10.0.0.3:8080 weight=2 max_fails=3 fail_timeout=30s;

    # 备份节点 - 其他全挂了才用
    server 10.0.0.4:8080 backup;

    # 标记为不可用(临时下线)
    # server 10.0.0.5:8080 down;

    # 健康检查参数(配合 nginx_upstream_check_module)
    check interval=3000 rise=2 fall=3 timeout=1000 type=http;
    check_http_send "GET /health HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;

    # 长连接
    keepalive 32;        # 每个 worker 保持 32 个连接到 upstream
    keepalive_timeout 60s;
    keepalive_requests 1000;
}

3.3 server 指令的关键参数

server address [parameters];
参数 含义
weight=N 权重,默认 1
max_fails=N 失败多少次标记为不可用,默认 1
fail_timeout=T 失败超时时间,默认 10s
backup 备份节点,其他全挂了才用
down 永久下线
resolve 域名解析(需要 resolver 指令)
slow_start=T 节点恢复后,流量逐渐恢复的时间(Nginx Plus 商业版)

四、负载均衡策略

4.1 轮询(round-robin)—— 默认

upstream backend {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

请求依次分给每个 server,最公平。

4.2 加权轮询(weighted round-robin)

upstream backend {
    server 10.0.0.1:8080 weight=5;
    server 10.0.0.2:8080 weight=3;
    server 10.0.0.3:8080 weight=2;
}

按比例分配。机器配置不同就用权重——比如 4 核机器 weight=4,8 核机器 weight=8。

4.3 ip_hash

upstream backend {
    ip_hash;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

同一个客户端 IP 永远路由到同一台 server。

适用场景:

坑:

# 真实 IP 提取
real_ip_header X-Forwarded-For;
set_real_ip_from 10.0.0.0/8;  # 信任内网代理
set_real_ip_from 192.168.0.0/16;

4.4 least_conn(最少连接)

upstream backend {
    least_conn;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

把请求给当前连接数最少的 server。

适用:长连接服务(WebSocket、长轮询)、后端响应时间差异大。

4.5 url_hash(需要 nginx-upstream-fair 或第三方模块)

upstream backend {
    hash $request_uri;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

同一个 URL 总是路由到同一台 server——专门为缓存场景设计。

适用:CDN 缓存后端、视频网站(同一个视频总是命中同一台机器的缓存)。

4.6 random

upstream backend {
    random;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

随机选,Nginx 1.15.7+ 默认算法。带 two 参数就是两次随机选最小连接。

4.7 选择策略

场景 推荐策略
一般无状态服务 加权轮询
需要 session 保持 ip_hash(注意代理)
WebSocket / 长连接 least_conn
缓存敏感 url_hash / consistent_hash
服务器性能差异大 加权 + least_conn

五、proxy_pass 进阶配置

5.1 传递客户端信息

location / {
    proxy_pass http://backend;

    # 传递 Host 头
    proxy_set_header Host $host;

    # 传递真实 IP
    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 User-Agent $http_user_agent;
    proxy_set_header Referer $http_referer;
}

后端应用就能拿到客户端的真实信息。

5.2 超时设置

location / {
    proxy_pass http://backend;

    # 连接超时
    proxy_connect_timeout 5s;        # 连接 upstream 的超时,默认 60s,推荐 5s

    # 发送超时
    proxy_send_timeout 30s;          # 发送请求体的超时

    # 接收超时
    proxy_read_timeout 30s;          # 等待 upstream 响应的超时

    # 长连接超时(Nginx 1.15.3+)
    proxy_socket_keepalive on;
}

最常踩的坑:proxy_read_timeout 默认 60s。如果后端响应慢,客户端会先断开,但 Nginx 还在等 upstream,浪费资源。

5.3 buffer 调优

location / {
    proxy_pass http://backend;

    # 响应缓冲
    proxy_buffering on;
    proxy_buffer_size 16k;
    proxy_buffers 8 32k;
    proxy_busy_buffers_size 64k;

    # 大响应缓冲到临时文件
    proxy_max_temp_file_size 1024m;

    # 请求体缓冲
    proxy_request_buffering on;
}

proxy_buffering on:Nginx 先把上游响应全部收下来,再发给客户端——可以处理慢客户端。

proxy_buffering off:Nginx 收到一点就发给客户端一点——适合长连接流式响应(SSE、大文件下载)。

5.4 重试

location / {
    proxy_pass http://backend;
    proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
    proxy_next_upstream_tries 3;
    proxy_next_upstream_timeout 10s;
}

当后端返回 5xx、连接出错、超时时,自动重试下一个 upstream。

⚠️ 不要对 POST 请求重试——可能产生重复提交。我们一般只对 GET 重试。

5.5 WebSocket 代理

location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 86400s;  # 长连接超时
}

关键三个:

六、公司 API 网关实战

6.1 完整配置

# /etc/nginx/sites-enabled/api-gateway.conf

upstream user_service {
    least_conn;
    server 10.0.1.1:3000 weight=3 max_fails=2 fail_timeout=30s;
    server 10.0.1.2:3000 weight=3 max_fails=2 fail_timeout=30s;
    server 10.0.1.3:3000 weight=2 max_fails=2 fail_timeout=30s;
    keepalive 64;
}

upstream order_service {
    ip_hash;  # 订单服务需要 session
    server 10.0.2.1:4000 max_fails=2 fail_timeout=30s;
    server 10.0.2.2:4000 max_fails=2 fail_timeout=30s;
    keepalive 64;
}

upstream product_service {
    server 10.0.3.1:5000;
    server 10.0.3.2:5000;
    keepalive 64;
}

upstream static_assets {
    server 10.0.4.1:80;
    server 10.0.4.2:80;
    keepalive 32;
}

# ====== 全局配置 ======
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

# ====== API 网关 server ======
server {
    listen 80;
    server_name api.company.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.company.com;

    ssl_certificate /etc/nginx/ssl/api.company.com.crt;
    ssl_certificate_key /etc/nginx/ssl/api.company.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    # 通用 header
    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 X-Request-ID $request_id;  # 链路追踪 ID

    # 通用超时
    proxy_connect_timeout 3s;
    proxy_send_timeout 30s;
    proxy_read_timeout 30s;

    # 重试(只对 GET 重试)
    proxy_next_upstream error timeout http_502 http_503 http_504;
    proxy_next_upstream_tries 2;
    proxy_next_upstream_timeout 10s;

    # 通用缓冲
    proxy_buffering on;
    proxy_buffer_size 16k;
    proxy_buffers 8 32k;

    # ====== 用户服务 ======
    location /api/users/ {
        proxy_pass http://user_service/;
    }

    # ====== 订单服务 ======
    location /api/orders/ {
        proxy_pass http://order_service/;
        # 订单服务的 session 在内存里,需要 ip_hash
    }

    # ====== 商品服务 ======
    location /api/products/ {
        proxy_pass http://product_service/;
    }

    # ====== WebSocket ======
    location /ws/ {
        proxy_pass http://user_service;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 86400s;
    }

    # ====== 健康检查 ======
    location = /health {
        access_log off;
        return 200 "ok\n";
    }

    # ====== 限流 ======
    location /api/ {
        # 全局限流 - 每秒 1000 请求
        limit_req zone=api_limit burst=200 nodelay;

        # 兜底
        return 404;
    }
}

# ====== 静态资源 server ======
server {
    listen 80;
    server_name cdn.company.com;

    location / {
        proxy_pass http://static_assets;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # 缓存静态资源
        proxy_cache static_cache;
        proxy_cache_valid 200 304 7d;
        proxy_cache_valid any 1m;
        add_header X-Cache-Status $upstream_cache_status;
    }
}

6.2 关键设计点

多 upstream 分层

不同服务有不同的:

不要把所有后端都扔到一个 upstream。

限流 + 重试 + 超时 三件套

# 限流
limit_req zone=api_limit burst=200 nodelay;

# 重试
proxy_next_upstream error timeout http_502 http_503 http_504;

# 超时
proxy_connect_timeout 3s;
proxy_read_timeout 30s;

三者配合,后端短暂故障不会让前端感知到。

链路追踪 ID

proxy_set_header X-Request-ID $request_id;

Nginx 自动生成 UUID,通过 X-Request-ID 头传给后端。后端日志、应用日志都记录这个 ID,排查问题非常方便。

HTTPS 统一入口

所有 HTTP 请求 301 跳 HTTPS,避免代码里处理两种协议。

七、upstream 健康检查

7.1 被动检查(默认)

upstream backend {
    server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
}

请求失败 3 次,标记为不可用;30 秒内不再发请求给这台机器。

缺点:第一次请求会失败(因为还不知道机器挂了)。

7.2 主动检查(需要模块)

upstream backend {
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;

    check interval=3000 rise=2 fall=3 timeout=1000 type=http;
    check_http_send "GET /health HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
}

需要编译时带 nginx_upstream_check_module(淘宝开源)。

生产推荐用主动检查——机器真挂的时候不会有失败请求。

7.3 用 K8s 服务发现代替

如果跑在 K8s 上,直接用 Service:

# K8s Service
apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector:
    app: backend
  ports:
    - port: 80
      targetPort: 8080

Nginx Ingress 自动发现,健康检查、自动扩缩容都交给 K8s。

八、本篇小结

主题 关键点
proxy_pass URL 规则 末尾 / 决定是否去掉 location 前缀
upstream 块 多 upstream 分离,不同服务不同策略
负载均衡 加权轮询是默认,特殊场景用 ip_hash / url_hash
超时设置 proxy_read_timeout 关键,默认 60s 太长
重试 只对 GET 重试,POST 重试会重复提交
健康检查 K8s 优先,Nginx 主动检查需要第三方模块

下一篇讲 HTTPS 配置全攻略——证书自动续期、TLS 1.3 优化、HSTS、零故障切换的真实过程。

参考资料