Nginx 反向代理与负载均衡实战:upstream、真实 IP 与四层七层选型

把后端从一台扩到多台只是加两个 IP 的事,真正麻烦的是加完之后冒出来的一堆问题:后端日志里全是负载均衡的 IP、出了 502 不知道该看哪张日志、两个后端”用户登录状态随机丢失”。这篇把反向代理和负载均衡这一层该配的东西整理一遍。

一、正代理和反代理,一句话分清

区别在服务的对象:正向代理代理客户端(客户端主动配置代理,代表是翻墙工具和企业上网代理),反向代理代理服务端(客户端以为在直连,实际请求被 Nginx 转发给后端)。运维日常干的都是后者。

对后端的价值就三条:统一入口(后端扩缩容用户无感知)、分摊压力(多节点负载)、安全缓冲(后端不直接暴露公网,可在入口层做限流、WAF、HTTPS 卸载)。

二、proxy_pass 的参数才是关键

最简配置一行就够,但那只是开始:

1
2
3
location / {
proxy_pass http://172.18.1.7:8080;
}

1. 请求头透传(不配就是给自己挖坑)

1
2
3
4
proxy_set_header Host $http_host;                # 保留用户访问的域名
proxy_set_header X-Real-IP $remote_addr; # 客户端 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # 标记 http/https

不配 Host 的后果很典型:后端一台服务器挂了多个虚拟主机,Nginx 转发过去都用默认站点匹配,用户访问 A 网站结果出来了 B 网站。不配 X-Forwarded-For 的后果是后端业务日志里所有请求都来自代理 IP,”哪些用户在访问”这个问题直接没法回答。

2. 超时与缓冲

1
2
3
4
5
6
7
proxy_connect_timeout 30s;    # 与后端建立连接的超时
proxy_send_timeout 60s; # 向后端发送请求的超时
proxy_read_timeout 60s; # 等待后端响应(最常调整的一个)

proxy_buffering on; # 边收边传
proxy_buffer_size 32k; # 响应头的缓冲区
proxy_buffers 4 128k; # 响应体缓冲区

经验值:一般接口 60s 够用;导出类接口要单独放宽;proxy_read_timeout 调大之前先想清楚——它只是把”超时失败”变成”用户等更久”,后端真慢还是要治后端。

3. WebSocket 和 HTTPS 的代理

1
2
3
4
5
6
7
8
# WebSocket:必须带 Upgrade 头,否则握手失败
location / {
proxy_pass http://127.0.0.1:9501;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; # 长连接不能 60s 断
}

这套参数我统一放进了 /etc/nginx/proxy_params,业务配置里一行 include proxy_params; 复用,改参数只改一处。

三、upstream:负载均衡核心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
upstream web_pool {
server 172.18.1.7:80 weight=5 max_fails=3 fail_timeout=30s;
server 172.18.1.8:80 weight=3 max_fails=3 fail_timeout=30s;
server 172.18.1.9:80 down; # 手动下线,维护时用
server 172.18.1.10:80 backup; # 备用机:其他全挂才启用
keepalive 32; # 与后端的长连接池
}

server {
location / {
proxy_pass http://web_pool;
proxy_http_version 1.1; # keepalive 必须配 1.1
proxy_set_header Connection "";
}
}

几个参数的生产含义:

参数 作用 建议
weight 权重,按比例分发 新机器先给小权重观察
max_fails / fail_timeout 失败 N 次后摘除,多久后重试 默认 1 次太敏感,生产 2~3 次
backup 备用节点,全挂才启用 容灾必备,成本允许就配
down 手动下线 维护时改配置,不删节点
keepalive 与后端的长连接池 每次请求都重连后端时 QPS 上不去,优先调这个

调度算法上,轮询和权重能覆盖大多数场景;ip_hash 是会话保持的兜底方案(有副作用,见下文);least_conn 在请求耗时差异大时更均衡。

健康检查

Nginx 自带的只有被动检查(max_fails)——后端挂了,要等真实请求失败才会摘除。想要主动探测得用第三方模块 nginx_upstream_check_module,编译进 Nginx:

1
2
3
4
5
6
7
8
9
10
upstream web {
server 172.18.1.7:80 max_fails=2 fail_timeout=10s;
server 172.18.1.8:80 max_fails=2 fail_timeout=10s;
check interval=3000 rise=2 fall=3 timeout=1000 type=tcp;
# 每 3 秒探测一次,2 次成功标记 up,3 次失败标记 down
}

location /upstream_check {
check_status; # 页面直接看各后端健康状态
}

预算够的话更省心的方案是云上负载均衡(SLB/CLB)或 LVS+Keepalived,健康检查是内置的;用 Nginx 就得接受”编译第三方模块”这一步。

四、真实 IP:多级代理下怎么拿到用户地址

只要经过一层代理,后端看到的 $remote_addr 就变成了代理的 IP。分两种情况:

后端是 Nginx:用 realip 模块把 XFF 头里的真实 IP 还原出来。

1
2
3
4
5
6
7
8
9
server {
listen 80;
server_name www.example.com;

set_real_ip_from 172.18.1.5; # 上一级代理的 IP,可以写多行
set_real_ip_from 172.18.1.6;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
}

之后日志里的 $remote_addr 就是真实客户端 IP 了。

后端是业务程序:程序框架一般有约定,从 X-Forwarded-For 取第一个地址。要提醒开发注意:XFF 是客户端可以伪造的头字段,只有前面每一层代理都追加过才是可信的——所以要配合 set_real_ip_from 限定”只信任已知代理写入的部分”。

五、四层还是七层

  • 七层(Nginx http/upstream):能解析 HTTP,按域名、URL、header 分流,能做会话保持、rewrite、缓存——但性能比四层低;
  • 四层(Nginx stream 或 LVS):只转发 TCP 包,不解析内容,性能最高,适合数据库、SSH 这类非 HTTP 服务,或者大流量入口。

Nginx 做四层需要单独配置在 http 块外面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# /etc/nginx/conf.c/lb.conf
stream {
upstream web01_ssh {
server 172.18.1.7:22;
}
upstream db01 {
server 172.18.1.51:3306;
}

server {
listen 5555;
proxy_pass web01_ssh; # 访问 5555 即后端 22
}
server {
listen 6666;
proxy_pass db01; # 访问 6666 即后端 3306
}
}

典型用途就是数据库代理:业务连 10.0.8.4:6666,实际打到 MySQL 的 3306,后端数据库不用对外暴露真实端口。

六、502/504/499:入口层的三个高频故障

状态码 含义 排查方向
502 Nginx 连不上后端 后端进程没了/端口不通/防火墙
504 后端响应超时 后端慢(慢 SQL、死锁、GC)
499 客户端等不及先断了 后端处理慢,用户侧超时

502 的标准排查路径:

1
2
3
4
5
6
curl -I http://www.example.com        # 先确认现象
tail -50 /var/log/nginx/error.log # 关键日志
# connect() failed (111: Connection refused) → 后端没起
# upstream timed out → 后端响应慢
ss -lntp | grep 8080 # 后端端口在不在
systemctl status tomcat # 后端服务状态

另外有个配置值得加上,能让”单台后端偶发 500”不至于把整个请求打回用户:

1
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;

意思是这台后端返回 5xx 时,把请求转给下一台重试——请求成功率立竿见影,但要注意:非幂等操作(下单、支付)不能开,重试可能造成重复提交,这个必须跟开发对齐后再加。

七、会话保持:优先改应用,别硬锁 IP

ip_hash 是最省事的方案,但有两个坑:同一出口 IP 的用户(公司网络、代理)全压到同一台后端;单节点故障时,那部分用户的登录态全丢。

更稳的方案是把会话外置,让任何一台后端都能识别登录态:

  • PHP 应用:session.save_handler = redis,session 存 Redis(后面 LNMP 那篇展开);
  • Java 应用:Tomcat 集群的 Redis 会话管理器。

选型上的建议:短期救急用 ip_hash,长期一定走统一会话存储——扩容、发版、缩容都不会再被它绊住。

八、小结

反向代理和负载均衡这一层的配置量不大,但每个参数背后都是一次生产教训:Host 不透传会串站、XFF 不透传日志全废、keepalive 不配性能上不去、proxy_next_upstream 乱开会产生重复下单。我的习惯是把 proxy_params 固化成一份模板,新站点直接 include,参数变更走一次压测验证——入口层稳定了,后面的排障压力会小很多。