Nginx-06 反向代理与负载均衡
反向代理是 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)
三个必须知道的后果:
- 不设
Host的话后端看到的是 upstream 名字,多域名后端会路由错。 - 不设
proxy_http_version 1.1+Connection ""的话,到上游是短连接,高 QPS 下会产生大量 TIME_WAIT。 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)。
三个重要缺陷:
- IPv4 只用前三段哈希(
a.b.c.*会落到同一台),所以同一个 NAT 出口的所有用户都到一台机器上——流量倾斜严重。 - 加减机器时哈希结果大面积变化,大部分用户的会话都会丢。
- 不能和
weight之外的功能很好配合,标记down的机器必须保留down标记而不能直接删掉,否则哈希环变了。
现代架构应该把 session 放 Redis,而不是靠 ip_hash。真需要粘性时,用 hash $cookie_xxx 比 ip_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.1 和 proxy_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
accepts和handled不相等 → 有连接被丢弃(通常是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;
}
}
stream 与 http 的差异:
| 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 动静分离与静态资源优化
xingliuhua