Nginx 反向代理配置:从原理到负载均衡
反向代理的本质
反向代理是个容易被误解的概念。正向代理替客户端访问服务端,反向代理替服务端接收客户端请求。一字之差,但方向完全相反。从部署看,反向代理更像一台"前台接待",它不处理业务,只负责把请求转发给后端真正的服务,再把结果返回给客户端。
引入反向代理解决的核心问题是:后端服务的暴露面。没有反向代理时,每个后端服务都得开放端口给外部,攻击面巨大。有了反向代理,外部只看到 80/443,后端服务全藏在内网。附加价值是负载均衡、缓存、SSL 卸载这些功能可以统一在代理层完成,后端服务不用各自实现一遍。
设计代价是引入了一跳网络延迟和单点风险。如果代理挂了,所有后端服务都不可用——所以生产环境代理本身也要做高可用。这个成本必须接受,没有廉价方案。
代理与负载均衡的差异
很多人把 proxy_pass 和 upstream 混为一谈。它们不是一回事。
proxy_pass 指向单个后端地址,只是转发,不涉及负载均衡。哪怕指向一个域名,如果 DNS 解析出多个 IP,Nginx 也只在启动时选一个。
upstream 定义一组后端,Nginx 会在组内分配请求。这才是负载均衡。
坑在于:很多人写 proxy_pass http://backend; 以为就负载均衡了,但如果没定义对应的 upstream backend {} 坯,Nginx 会把 backend 当域名做 DNS 解析。解析失败就起不来。
基础代理配置
最基础的反向代理配置,把 /api/ 的请求转发到后端 8080:
# 实测于 Nginx 1.24.0, CentOS 7
server {
listen 80;
server_name api.example.com;
location /api/ {
proxy_pass http://127.0.0.1:8080/;
# 透传真实客户端 IP,后端日志才有意义
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
}
}
proxy_pass 末尾的斜杠是个经典坑。http://127.0.0.1:8080/ 带斜杠会把 /api/ 替换成 /,后端收到的路径是 /users;如果不带斜杠写成 http://127.0.0.1:8080,后端收到的是 /api/users。前后端路径不一致的时候,99% 是这里的问题。
负载均衡配置
引入 upstream 后,可以做多后端负载均衡:
upstream backend {
# 加权轮询(默认),weight 越高分配越多
server 192.168.1.10:8080 weight=3;
server 192.168.1.11:8080 weight=1;
# 备用服务器,只在主服务器都挂了才启用
server 192.168.1.12:8080 backup;
# 容错:10 秒内失败 3 次就标记不可用,停 30 秒再试
# 这个参数写在 server 行内
}
server {
listen 80;
location /api/ {
proxy_pass http://backend/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Nginx 默认的负载均衡算法是加权轮询(Weighted Round Robin)。还有两种可选:
- least_conn:转发给当前连接数最少的后端,适合请求处理时间差异大的场景
- ip_hash:按客户端 IP 哈希固定后端,保证会话粘性
ip_hash 看起来很美,但坑在于:客户端经过 NAT 出口,所有请求看起来都是同一个 IP,会集中打到一台后端。生产环境如果走 CDN,ip_hash 基本失效。
实测记录:
两个后端,weight 1:1,用 wrk -t4 -c100 -d30s 压测。
- 理论预期:负载约 50:50 分配
- 实际结果:49.7% : 50.3%,基本符合预期
- 切到 ip_hash 后变成 87% : 13%——大多数 IP 都被哈希到同一个后端,验证了 NAT 场景下 ip_hash 失效
偏差分析:ip_hash 的哈希空间只有 2 个桶(2 个后端),分布极度不均匀。后端数量越多,ip_hash 的均匀性越好;只有 2 个后端时几乎没意义。
会话保持:用 cookie 而不是 ip_hash
替代 ip_hash 的方案是 sticky cookie。Nginx 商业版原生支持,开源版可以用 nginx-sticky-module-ng 第三方模块。原理是给客户端种一个 cookie,下次请求带这个 cookie 就路由到同一后端。
如果不想引入第三方模块,最实用的方案是在应用层做会话共享(Redis 存 session),后端无状态,随便路由到哪台都行。这才是真正的可扩展架构。我维护的服务都走这条路——代理层只管转发,无状态化交给应用层处理。
SSL 卸载
反向代理做 SSL 卸载,后端就不需要处理 HTTPS,降低 CPU 占用:
server {
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
location /api/ {
proxy_pass http://backend/;
# 后端需要知道原始协议是 https
proxy_set_header X-Forwarded-Proto $scheme;
}
}
后端框架通常通过 X-Forwarded-Proto 判断原始协议。Django 和 Flask 都支持,但要确保配置了 SECURE_PROXY_SSL_HEADER 或者 ProxyFix 中间件,否则后端会以为请求是 http 进来的,重定向时生成错误的 URL。
遗留问题
- Nginx 后端健康检查目前只能靠被动探测(请求失败才标记 down)。主动健康检查(
health_check)是商业版功能,开源版要装nginx_upstream_check_module,这个模块跟 Nginx 版本绑定很死,升级麻烦。替代方案是外部脚本定期 curl 探测,通过nginx -s reload动态剔除节点,但 reload 会导致短时连接中断。 proxy_pass的 DNS 解析只在启动时做一次,如果后端用域名且 IP 会变(比如云服务商),需要用resolver指令加变量方式实现动态解析。具体写法可参考官方文档的resolver章节。
关联笔记
关联笔记:Nginx 限流配置:防刷防爬的实战方案 中关于 limit_req 和 limit_conn 的配置讨论,可与此文配合使用