目录

Nginx-06 反向代理与负载均衡

前置阅读:Nginx-05 location 匹配与 rewrite

反向代理是 Nginx 最主要的用途。这一篇覆盖从基本配置到失败重试、长连接复用、灰度发布的完整体系。

1. 反向代理基础

1.1 最小配置

location /api/ {
    proxy_pass http://127.0.0.1:8080;
}

这三行就能工作,但生产环境绝对不够,因为后端拿不到任何客户端的真实信息。

1.2 必须设置的请求头

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

    # 1. Host:不设的话后端收到的 Host 是 upstream 的名字(如 "backend")
    proxy_set_header Host $host;

    # 2. 真实客户端 IP
    proxy_set_header X-Real-IP $remote_addr;

    # 3. 代理链(会自动追加,格式:client, proxy1, proxy2)
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # 4. 原始协议:后端据此生成正确的绝对 URL,否则 HTTPS 站点会生成 http:// 链接
    proxy_set_header X-Forwarded-Proto $scheme;

    # 5. 原始 Host 和端口(后端做重定向时需要)
    proxy_set_header X-Forwarded-Host  $host;
    proxy_set_header X-Forwarded-Port  $server_port;

    # 6. 链路追踪 ID
    proxy_set_header X-Request-ID $request_id;
}

$host vs $http_host vs $server_name

变量 取值
$http_host 客户端 Host 头的原始值,可能含端口,可能为空
$host 优先用 Host 头(去掉端口、转小写),没有则用匹配到的 server_name
$server_name 配置里写的 server_name,与客户端请求无关

默认用 $host。需要保留端口时用 $http_host

1.3 Nginx 默认改了哪些头

Nginx 在转发前会做几件事,不知道的话容易困惑:

# Nginx 的内置默认值(等价于以下配置)
proxy_set_header Host       $proxy_host;   # ← 注意!默认是 upstream 名字,不是客户端的 Host
proxy_set_header Connection close;         # ← 默认关闭到上游的长连接

# 另外:
# - 默认用 HTTP/1.0 与上游通信(proxy_http_version 默认 1.0)
# - 值为空字符串的头会被丢弃
# - 带下划线的头会被丢弃(除非 underscores_in_headers on)

三个必须知道的后果:

  1. 不设 Host 的话后端看到的是 upstream 名字,多域名后端会路由错。
  2. 不设 proxy_http_version 1.1 + Connection "" 的话,到上游是短连接,高 QPS 下会产生大量 TIME_WAIT。
  3. X-User_Id 这种带下划线的头会被静默丢弃。这个坑非常隐蔽——用横杠 X-User-Id 就没事。
# 允许下划线头(不推荐,改成横杠更好)
underscores_in_headers on;

1.4 超时配置

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

    # 与上游建立 TCP 连接的超时,不能超过 75s
    # 内网服务设 1-3s 就够,太长会拖慢故障切换
    proxy_connect_timeout 3s;

    # 向上游发送请求的两次写操作之间的超时
    proxy_send_timeout 30s;

    # 从上游读取响应的两次读操作之间的超时(最常需要调的)
    proxy_read_timeout 30s;
}

proxy_read_timeout 是「两次读之间的间隔」而不是总时长。一个流式响应(SSE)只要持续有数据就不会超时。但一个后端要跑 60 秒的慢查询(期间一个字节都不返回),30s 的 proxy_read_timeout 就会把它掐掉,客户端收到 504。

不同场景的推荐值:

场景 connect send read
普通内网 API 1-3s 10s 10-30s
文件上传 3s 300s 60s
大文件下载 3s 10s 300s
WebSocket / SSE 3s 60s 3600s
报表导出等长任务 3s 10s 300s

1.5 缓冲

这块很关键,直接影响内存占用和首字节延迟。

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

    # 开启响应缓冲(默认 on)
    # on:Nginx 尽快读完上游响应存起来,然后慢慢发给客户端
    # off:收到多少发多少
    proxy_buffering on;

    # 存放响应头的缓冲区(也用于第一部分响应体)
    proxy_buffer_size 8k;

    # 存放响应体的缓冲区:数量 × 大小
    proxy_buffers 8 16k;             # 共 128k

    # 从上游读的同时往客户端写时,能用的缓冲区上限
    proxy_busy_buffers_size 32k;

    # 超出缓冲区的部分写临时文件
    proxy_max_temp_file_size 1024m;  # 设 0 则完全禁用临时文件
    proxy_temp_file_write_size 32k;

    # 请求体缓冲(客户端上传)
    proxy_request_buffering on;      # on:读完整个请求体再转发给上游
}

proxy_buffering on 的价值:后端进程(尤其是 PHP-FPM、同步框架)能快速把响应吐给 Nginx 然后立刻释放去处理下一个请求,不用陪着慢速客户端。这是保护后端的关键机制——慢速客户端的连接由 Nginx 扛着,后端不受影响

什么时候要关 buffering

# SSE / 流式响应 / 实时日志推送 —— 必须关
location /stream/ {
    proxy_pass http://backend;
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding off;   # 视情况
    add_header X-Accel-Buffering no; # 也可以让上游返回这个头来动态控制
}

上游可以返回 X-Accel-Buffering: no动态关闭 buffering,比在 Nginx 里写死更灵活。

proxy_request_buffering off 的场景:大文件上传。默认 on 会让 Nginx 先把整个文件收完(可能几 GB,写临时文件)再转给后端,延迟高、磁盘 I/O 大。关掉后是边收边转:

location /upload {
    proxy_pass http://backend;
    proxy_request_buffering off;
    client_max_body_size 5g;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

代价是失去了重试能力(请求体已经流给上游了,没法重发到另一台)。

2. upstream 与负载均衡

2.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;
    }
}

2.2 server 指令的参数

upstream backend {
    # weight:权重,默认 1。配置好的机器给更高权重
    server 10.0.0.1:8080 weight=3;

    # max_fails + fail_timeout:被动健康检查
    # 在 fail_timeout 时间内失败 max_fails 次,就把这台标记为不可用 fail_timeout 时长
    server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;

    # backup:只有所有主服务器都挂了才会用
    server 10.0.0.9:8080 backup;

    # down:手动标记下线(发布时用)
    server 10.0.0.3:8080 down;

    # max_conns:这台机器的最大并发连接数,超了就不再分配
    server 10.0.0.4:8080 max_conns=100;

    # slow_start:恢复后逐步增加权重(仅商业版 NGINX Plus)
    # server 10.0.0.5:8080 slow_start=30s;

    # resolve:让域名在运行时重新解析(需要 upstream 里配 resolver,1.27.3+ 开源版支持)
    # server api.internal.com:8080 resolve;

    # 也支持 unix socket
    # server unix:/var/run/app.sock;
}

2.3 六种负载均衡算法

① 轮询(round-robin,默认)

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

按顺序依次分配。最简单,适合所有后端配置相同、请求耗时均匀的场景。

② 加权轮询

upstream backend {
    server 10.0.0.1:8080 weight=5;   # 16 核机器
    server 10.0.0.2:8080 weight=2;   # 8 核机器
    server 10.0.0.3:8080 weight=1;   # 4 核机器
}

Nginx 用的是平滑加权轮询(smooth weighted round-robin),不是简单地「先给 A 五次再给 B 两次」,而是尽量把请求打散。比如 {a:5, b:1, c:1} 的分配序列是 a a b a c a a 而不是 a a a a a b c

③ least_conn —— 最少连接

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

把请求给当前活跃连接数最少的(考虑权重)。请求处理时长差异大的场景应该用这个——比如有的接口 5ms 有的 5s,轮询会导致慢请求堆在同一台机器上。

④ ip_hash —— 按客户端 IP 哈希

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

同一个客户端 IP 总是落到同一台后端,用于会话保持(sticky session)

三个重要缺陷:

  1. IPv4 只用前三段哈希a.b.c.* 会落到同一台),所以同一个 NAT 出口的所有用户都到一台机器上——流量倾斜严重
  2. 加减机器时哈希结果大面积变化,大部分用户的会话都会丢。
  3. 不能和 weight 之外的功能很好配合,标记 down 的机器必须保留 down 标记而不能直接删掉,否则哈希环变了。

现代架构应该把 session 放 Redis,而不是靠 ip_hash。真需要粘性时,用 hash $cookie_xxxip_hash 好。

⑤ hash —— 自定义 key 哈希

# 按 URI 哈希:同一个 URL 总是落到同一台,缓存命中率高
upstream cache_servers {
    hash $request_uri consistent;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

# 按 cookie 做会话保持(比 ip_hash 精确)
upstream backend {
    hash $cookie_sessionid consistent;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
}

# 按用户 ID 哈希
upstream backend {
    hash $arg_uid consistent;
    ...
}

consistent 参数开启一致性哈希(ketama 算法)。区别巨大:

  • 不带 consistent:取模哈希,加一台机器(N → N+1)会导致几乎所有 key 重新分布
  • consistent:加一台机器只影响 1/(N+1) 的 key。

做缓存分片时必须加 consistent,否则扩容一次缓存全失效,后端瞬间被打穿。

一致性哈希的原理见 一致性 hash

⑥ random —— 随机(1.15.1+)

upstream backend {
    random two least_conn;
    server 10.0.0.1:8080;
    server 10.0.0.2:8080;
    server 10.0.0.3:8080;
}

random two least_conn 是**“Power of Two Random Choices”** 算法:随机挑两台,再从这两台里选连接数少的那台。

这个算法在多个 Nginx 实例前置同一组后端时特别有价值:least_conn 是每个 Nginx 独立统计的,多个 Nginx 各自认为「A 最闲」就会一起打向 A,造成羊群效应。random two least_conn 用随机性打破了这种同步,效果接近全局最优但没有协调成本。

2.4 算法选择速查

场景 推荐算法
后端同质、请求耗时均匀 round-robin(默认)
后端配置不同 weight 加权
请求耗时差异大(有长任务) least_conn
多个 Nginx 前置同一组后端 random two least_conn
缓存分片、需要相同 key 落同一台 hash $key consistent
需要会话保持 hash $cookie_sid consistent(优于 ip_hash

3. 健康检查

3.1 被动健康检查(开源版)

开源版只有被动检查:通过真实请求的失败来判断后端死活

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

location / {
    proxy_pass http://backend;

    # 什么情况算「失败」——这个配置直接决定了健康检查的灵敏度
    proxy_next_upstream error timeout http_502 http_503 http_504;
    proxy_next_upstream_tries 2;
    proxy_next_upstream_timeout 10s;
}

工作机制:在 fail_timeout(30s)窗口内,如果对某台后端的请求失败达到 max_fails(3)次,就把它标记为不可用,持续 fail_timeout(30s)。30s 后放一个请求过去试探,成功则恢复,失败则再等 30s。

注意 fail_timeout 是双重含义的:既是统计失败次数的时间窗口,也是被踢出后的隔离时长。

proxy_next_upstream 的取值

含义
error 连接、发送、读取时发生错误
timeout 连接、发送、读取超时
invalid_header 上游返回了非法响应头
http_500 / 502 / 503 / 504 / 403 / 404 / 429 对应状态码
non_idempotent 允许重试非幂等请求(POST/PATCH/LOCK),默认不重试
off 完全禁用重试

默认值是 error timeout

3.2 重试的幂等性陷阱

这是生产事故高发点,也是很好的面试题。

默认情况下,Nginx 只对幂等方法(GET/HEAD/PUT/DELETE/OPTIONS/TRACE)重试,POST 不重试。这个默认值是对的。

但很多人为了「提高可用性」加上 non_idempotent

# ⚠️ 危险
proxy_next_upstream error timeout non_idempotent;

后果:一个 POST 创建订单的请求,Nginx 转发给 A,A 已经成功处理并写了库,但响应还没返回就超时了。Nginx 认为失败,重试到 B,B 又创建了一个订单。用户下单一次,系统里出现两笔订单

同理,http_500 也很危险:

# ⚠️ 也危险
proxy_next_upstream error timeout http_500;

500 通常意味着后端已经收到并执行了请求,只是执行过程出了错。这时重试可能造成重复副作用(重复扣款、重复发消息)。

安全的配置原则

# ✅ 推荐:只重试「明确没有被处理」的情况
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;         # 限制重试次数,避免雪崩
proxy_next_upstream_timeout 10s;     # 限制重试总耗时
  • error:连接都没建立/发送失败,后端没收到,安全。
  • timeout其实不完全安全(可能后端已处理),但连接超时(proxy_connect_timeout)的情况是安全的。严格场景可以只留 error
  • 502/503/504:通常是后端进程没起来或队列满,请求没被业务逻辑处理,相对安全。
  • 500:不安全,不要加。

根本解法是业务层做幂等(用唯一请求 ID 去重),而不是在 Nginx 层纠结。

还有一个隐藏的坑:proxy_next_upstream_tries 不设的话默认是 0(不限次数),理论上会把所有后端都试一遍。后端多的话一个请求可能拖很久。

3.3 主动健康检查

开源版没有主动健康检查,有三个方案:

方案一:Nginx Plus(商业版)

upstream backend {
    zone backend 64k;
    server 10.0.0.1:8080;
}
location / {
    proxy_pass http://backend;
    health_check interval=5s fails=3 passes=2 uri=/health match=ok;
}
match ok {
    status 200;
    body ~ "ok";
}

方案二:nginx_upstream_check_module(淘宝开源,需要打补丁编译)

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;
}

# 查看健康状态页
location /upstream_status {
    check_status;
    access_log off;
}

方案三:OpenResty + lua-resty-upstream-healthcheck

http {
    lua_shared_dict healthcheck 1m;
    init_worker_by_lua_block {
        local hc = require "resty.upstream.healthcheck"
        hc.spawn_checker{
            shm = "healthcheck", upstream = "backend",
            type = "http", http_req = "GET /health HTTP/1.0\r\nHost: x\r\n\r\n",
            interval = 2000, timeout = 1000,
            fall = 3, rise = 2, valid_statuses = {200},
        }
    }
}

实践建议:在 K8s 环境下不用纠结这个——Service 的 Endpoint 已经由 kubelet 的 readinessProbe 维护了,不健康的 Pod 会被自动摘掉。Nginx 只需要正确处理连接失败即可。

4. 上游长连接(keepalive)

这是性价比最高的一项优化,但很多人配错。

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

    # 每个 worker 保持的空闲长连接数(不是总连接数上限!)
    keepalive 64;

    # 单个长连接最多复用多少次请求后关闭(1.15.3+)
    keepalive_requests 1000;

    # 空闲长连接的存活时间(1.15.3+)
    keepalive_timeout 60s;
}

location / {
    proxy_pass http://backend;

    # ★ 这两行必须加,否则 keepalive 不生效!
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

proxy_http_version 1.1proxy_set_header Connection "" 缺一不可

  • Nginx 默认用 HTTP/1.0 与上游通信,HTTP/1.0 不支持持久连接。
  • Nginx 默认设置 Connection: close,明确告诉上游用完就关。

不加这两行的话,keepalive 64 完全是摆设。配了 keepalive 但没加这两行,是最常见的配置错误之一

4.1 keepalive 的数值怎么定

keepalive N 的含义是「每个 worker 进程维持的空闲长连接数」。所以实际的空闲连接总数 = worker_processes × keepalive × upstream 中的 server 数量(大致)。

估算方法:

所需 keepalive ≈ 峰值 QPS × 平均响应时间 / worker_processes

例如峰值 5000 QPS、平均响应 20ms、8 个 worker: 5000 × 0.02 / 8 ≈ 13,设 keepalive 32 有富余。

设太大的风险:上游服务器要维持大量连接,可能耗尽它的连接数或文件描述符。特别注意如果前面有 10 台 Nginx,上游看到的连接数是 10 × 8 × 64 = 5120。

没配 keepalive 的代价:每个请求都要 TCP 三次握手(内网 0.1-1ms,跨可用区可能 1-5ms),并且产生大量 TIME_WAIT。高 QPS 下 TIME_WAIT 会耗尽本地端口(net.ipv4.ip_local_port_range 默认约 28000 个),表现为 connect() failed (99: Cannot assign requested address)

4.2 验证 keepalive 生效

# 在 Nginx 机器上看到上游的连接状态
ss -tan | grep :8080 | awk '{print $1}' | sort | uniq -c
# 期望:ESTAB 数量稳定,TIME-WAIT 很少
#   64 ESTAB
#    2 TIME-WAIT

# 没生效的话是这样:
#   50 ESTAB
# 5000 TIME-WAIT     ← 大量 TIME_WAIT 说明每个请求都在新建连接

5. 灰度发布与流量切分

5.1 按权重灰度

upstream backend {
    server 10.0.0.1:8080 weight=95;   # 老版本
    server 10.0.0.9:8080 weight=5;    # 新版本,5% 流量
}

最简单,但同一个用户可能一会儿新版一会儿老版,体验不一致。

5.2 按请求头/Cookie 灰度(推荐)

upstream stable  { server 10.0.0.1:8080; server 10.0.0.2:8080; }
upstream canary  { server 10.0.0.9:8080; }

# 优先级:header > cookie > 默认
map $http_x_canary $pool_by_header {
    default  "";
    "1"      "canary";
    "true"   "canary";
}

map $cookie_canary $pool_by_cookie {
    default  "";
    "1"      "canary";
}

map "$pool_by_header$pool_by_cookie" $backend_pool {
    default   "stable";
    ~canary   "canary";
}

server {
    location / {
        proxy_pass http://$backend_pool;
        include snippets/proxy-headers.conf;
        add_header X-Backend-Pool $backend_pool always;   # 便于验证
    }
}

测试:

curl -H "X-Canary: 1" http://example.com/api/x -I | grep X-Backend-Pool
# X-Backend-Pool: canary

5.3 按用户 ID 尾号灰度(稳定分流)

# 取 uid 最后一位,0-4 走灰度(50%)
map $cookie_uid $uid_tail {
    default   "";
    ~(\d)$    $1;
}

map $uid_tail $backend_pool {
    default   "stable";
    "0"       "canary";
    "1"       "canary";
}

同一个用户永远在同一侧,体验一致,这是灰度发布的正确做法。

5.4 用 split_clients 做随机但稳定的分流

split_clients "${remote_addr}${http_user_agent}" $variant {
    5%      "canary";
    *       "stable";
}

location / {
    proxy_pass http://$variant;
}

split_clients 对输入做 MurmurHash2,按比例分桶。同样的输入永远得到同样的结果,所以同一个客户端稳定落在一侧。这是做 A/B 测试的标准工具。

$request_id 作为 key 就是纯随机(每个请求都不同);用 $cookie_uid 就是按用户稳定分流。

6. 常见故障排查

6.1 502 Bad Gateway

含义:Nginx 连上了后端,但后端返回了无效响应,或者根本连不上。

# 1. 看 error.log
tail -100 /var/log/nginx/error.log

# 典型报错及原因:
# connect() failed (111: Connection refused)
#   → 后端进程没起来,或端口/地址写错
# connect() failed (113: No route to host)
#   → 网络不通,检查安全组/防火墙/路由
# no live upstreams while connecting to upstream
#   → 所有后端都被被动健康检查标记为 down 了
# upstream prematurely closed connection while reading response header
#   → 后端进程崩了/被 OOM kill/主动关连接
# upstream sent too big header while reading response header
#   → 响应头太大,调大 proxy_buffer_size

# 2. 从 Nginx 机器上直连后端验证
curl -v http://10.0.0.1:8080/api/x

# 3. SELinux(CentOS 常见)
getenforce
setsebool -P httpd_can_network_connect 1

6.2 504 Gateway Timeout

含义:后端在 proxy_read_timeout 内没返回完整响应。

# 在日志里找出耗时分布
awk '{print $NF}' access.log | sort -n | tail -20

# 用 upstream_response_time 定位
# 如果 upstream_response_time 接近 proxy_read_timeout → 后端确实慢,去查后端
# 如果 upstream_connect_time 大 → 后端 accept 队列满或网络问题

处理顺序:先查后端为什么慢,而不是先调大 timeout。调大 timeout 只是把问题藏起来,同时会让慢请求占着连接,加剧雪崩。

6.3 长连接被误关

# 现象:明明配了 keepalive,还是有大量 TIME_WAIT
# 原因排查清单:
# 1. 是否漏了 proxy_http_version 1.1
# 2. 是否漏了 proxy_set_header Connection ""
# 3. 是否在某个 location 里写了别的 proxy_set_header,
#    导致数组式继承把 Connection "" 冲掉了  ← 这个最隐蔽
# 4. 上游服务自己的 keepalive 是否开启、超时是否比 Nginx 短

第 3 点举例:

http {
    proxy_http_version 1.1;
    proxy_set_header Connection "";      # 全局设置

    server {
        location /api/ {
            proxy_set_header X-Foo bar;   # ⚠️ 这一行让上面的 Connection "" 失效了
            proxy_pass http://backend;
        }
    }
}

修复:把所有 proxy_set_header 放在同一层级,或统一用 include 片段。

6.4 上游状态观察

# 开源版的基础状态页
location = /nginx_status {
    stub_status;
    allow 127.0.0.1;
    deny all;
    access_log off;
}
curl http://127.0.0.1/nginx_status
# Active connections: 291
# server accepts handled requests
#  16630948 16630948 31070465
# Reading: 6 Writing: 179 Waiting: 106
  • acceptshandled 不相等 → 有连接被丢弃(通常是 worker_connections 不够)
  • Waiting 大 → keepalive 空闲连接多,正常
  • Reading 持续很大 → 可能有慢速攻击(Slowloris)

7. 四层代理(stream)

http 块只能代理 HTTP。要代理 MySQL、Redis、SSH、gRPC 裸 TCP,用 stream 块(需要 --with-stream)。

# 注意:stream 和 http 平级,都在 main 块下
stream {
    log_format basic '$remote_addr [$time_local] $protocol $status '
                     '$bytes_sent $bytes_received $session_time '
                     '$upstream_addr';
    access_log /var/log/nginx/stream.log basic;

    upstream mysql_backend {
        least_conn;
        server 10.0.0.1:3306 max_fails=2 fail_timeout=10s;
        server 10.0.0.2:3306 backup;
    }

    server {
        listen 3306;
        proxy_pass mysql_backend;
        proxy_connect_timeout 3s;
        proxy_timeout 300s;         # 注意:不是 proxy_read_timeout
        proxy_socket_keepalive on;
    }

    # UDP 代理(如 DNS、syslog)
    upstream dns_backend {
        server 8.8.8.8:53;
        server 1.1.1.1:53;
    }
    server {
        listen 53 udp;
        proxy_pass dns_backend;
        proxy_responses 1;          # UDP 期望收到几个响应包
        proxy_timeout 3s;
    }

    # TLS 透传(SNI 路由,不解密)
    map $ssl_preread_server_name $tls_backend {
        a.example.com  backend_a;
        b.example.com  backend_b;
        default        backend_default;
    }
    server {
        listen 443;
        ssl_preread on;             # 只偷看 ClientHello 里的 SNI,不解密
        proxy_pass $tls_backend;
    }
}

streamhttp 的差异:

http stream
超时指令 proxy_read_timeout / proxy_send_timeout proxy_timeout(统一)
请求头操作 proxy_set_header 不支持(没有 HTTP 概念)
location 支持 不支持(一个 server 一个 proxy_pass)
真实 IP 传递 X-Forwarded-For 需要 PROXY protocol
变量 $request_uri $ssl_preread_*$session_time

四层代理传递真实 IP 要用 PROXY protocol

# Nginx 侧(作为代理,发送 PROXY protocol)
stream {
    server {
        listen 3306;
        proxy_pass backend;
        proxy_protocol on;
    }
}

# 后端 Nginx(接收 PROXY protocol)
server {
    listen 8080 proxy_protocol;
    set_real_ip_from 10.0.0.0/8;
    real_ip_header proxy_protocol;
}

ssl_preread on 那个配置很实用:不解密 TLS,只解析 ClientHello 里的 SNI 字段来决定往哪转发。适合多个不同证书的服务共用一个 443 端口,而 Nginx 不需要持有任何私钥。

8. gRPC 代理

server {
    listen 443 ssl;
    http2 on;                        # gRPC 强制要求 HTTP/2

    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    location / {
        grpc_pass grpc://grpc_backend;
        # TLS 后端用 grpcs://
        grpc_set_header X-Real-IP $remote_addr;
    }

    # 按 service 路由到不同后端
    location /user.UserService/ {
        grpc_pass grpc://user_service;
    }
    location /order.OrderService/ {
        grpc_pass grpc://order_service;
    }
}

upstream grpc_backend {
    server 10.0.0.1:50051;
    server 10.0.0.2:50051;
    keepalive 32;
}

gRPC 负载均衡的关键问题:gRPC 基于 HTTP/2 多路复用,一个 TCP 连接上跑很多 stream。Nginx 做四层代理时,一个客户端的所有请求都在同一个连接上,会全部落到同一台后端——负载完全不均。

grpc_pass(七层)就没这个问题,Nginx 会在 HTTP/2 stream 级别做负载均衡。这是 Nginx 代理 gRPC 必须用 grpc_pass 而不是 stream 的原因

9. 面试题

Q:proxy_pass 时为什么一定要设 proxy_set_header Host $host;

因为 Nginx 的默认值是 Host: $proxy_host,即 upstream 的名字(如 backend)。后端如果按 Host 做虚拟主机路由、生成绝对 URL 或做域名校验,就会拿到错误的值。

Q:Nginx 有哪些负载均衡算法?分别适用什么场景?

轮询(默认,后端同质)、加权轮询(后端配置不同)、least_conn(请求耗时差异大)、ip_hash(会话保持,但有流量倾斜和扩容失效问题)、hash $key [consistent](自定义 key,做缓存分片必须加 consistent)、random two least_conn(多 Nginx 实例前置同一组后端时避免羊群效应)。

Q:ip_hash 有什么问题?

(1) IPv4 只取前三段做哈希,同一 NAT 出口的所有用户落到同一台,流量严重倾斜;(2) 增减后端节点会导致哈希结果大面积变化,会话大量丢失(要用 down 标记而不能直接删 server);(3) 与 least_conn 等策略不兼容。更好的方案是 session 外置到 Redis,或用 hash $cookie_sessionid consistent

Q:一致性哈希解决了什么问题?

普通取模哈希在节点数从 N 变成 N+1 时,几乎所有 key 的归属都会改变。一致性哈希(hash ... consistent,ketama 算法)把节点映射到哈希环上并引入虚拟节点,增删一个节点只影响约 1/N 的 key。做缓存分片时不用它,扩容一次就会导致缓存整体失效、后端被击穿。

Q:开源版 Nginx 怎么做健康检查?

只有被动健康检查:max_fails + fail_timeout 配合 proxy_next_upstream 定义的失败条件。在 fail_timeout 窗口内失败达到 max_fails 次就隔离该节点 fail_timeout 时长,之后放试探请求。主动健康检查需要 NGINX Plus、nginx_upstream_check_module 补丁,或 OpenResty 的 lua-resty-upstream-healthcheck

Q:proxy_next_upstream 加上 non_idempotent 有什么风险?

会让 POST/PATCH/LOCK 这类非幂等请求也参与重试。如果后端已经成功处理但响应超时,Nginx 重试到另一台会导致业务重复执行(重复下单、重复扣款)。同理不应该加 http_500——500 说明后端已经执行过了。安全的组合是 error timeout http_502 http_503 http_504,并配 proxy_next_upstream_tries 限次。根本解法是业务层幂等。

Q:配了 keepalive 64 但上游还是大量 TIME_WAIT,为什么?

几乎一定是漏了 proxy_http_version 1.1;proxy_set_header Connection "";。Nginx 默认用 HTTP/1.0 且发送 Connection: close。另一种隐蔽原因是在子 location 里写了其他 proxy_set_header,因为数组式继承规则把父级的 Connection "" 整体冲掉了。

Q:proxy_buffering on 有什么好处?什么时候要关?

开启时 Nginx 会尽快把上游响应读完缓存起来,让后端进程尽早释放去处理下个请求,慢速客户端由 Nginx 扛着——这是保护后端的核心机制。需要关闭的场景是流式响应(SSE、实时日志、大文件边下边传),否则客户端会等到缓冲满才收到数据。也可以让上游返回 X-Accel-Buffering: no 来动态关闭。

Q:为什么代理 gRPC 要用 grpc_pass 而不是四层 stream

gRPC 基于 HTTP/2 多路复用,一个 TCP 连接承载大量 stream。四层代理只能按连接分发,一个客户端的所有调用都会落到同一台后端,负载完全不均。grpc_pass 工作在七层,能在 HTTP/2 stream 级别做负载均衡。


上一篇:Nginx-05 location 匹配与 rewrite | 下一篇:Nginx-07 动静分离与静态资源优化