Nginx 反向代理进阶实战:WebSocket 转发、真实 IP 透传与灰度发布
很多人会用 proxy_pass 把请求转发给后端,但一遇到 WebSocket 长连接、后端拿不到客户端真实 IP、需要灰度发布时,配置就开始"玄学"了。本文不讲基础语法,直接围绕几个高频进阶场景,给出可直接上线的配置、原理说明和排错方法,帮你把反向代理从"能用"做到"稳"。
一、proxy_pass 的 URI 陷阱:带不带路径,转发结果完全不同
proxy_pass 后面是否带 URI(路径),决定了 location 匹配到的部分会不会被拼接转发,这是反向代理最容易踩的坑之一。
规则只有一条:proxy_pass 不带 URI 时,把完整请求原样转发;带 URI 时,用该 URI 替换掉 location 匹配到的部分。
看两个例子:
# 场景 1:不带 URI,/api/user 会原样转发到 http://127.0.0.1:8080/api/user
location /api/ {
proxy_pass http://127.0.0.1:8080;
}
# 场景 2:带 URI,/api/user 会被改写为 http://127.0.0.1:8080/user
location /api/ {
proxy_pass http://127.0.0.1:8080/;
}
排错方法:如果后端路由里一直出现多出的 /api 前缀(或相反),先检查 proxy_pass 末尾有没有那个斜杠。注意,proxy_pass 带变量时(如 $request_uri),不会发生替换,行为又不一样,生产环境建议避免动态拼接。
二、WebSocket 与 SSE 长连接转发:搞定 Upgrade 头与超时
WebSocket 握手靠的是 Upgrade: websocket 和 Connection: Upgrade 两个请求头。Nginx 默认不会把这些头透传给后端,必须显式配置。另外 WebSocket 是长连接,默认的 proxy_read_timeout 60 秒会导致空闲连接被掐断。
# http 块中定义升级映射
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# server 块中
location /ws/ {
proxy_pass http://ws_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# 心跳/空闲超时,聊天、推送类应用建议 300s 以上
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
SSE(Server-Sent Events)是单边长连接,不需要 Upgrade 头,但同样要加大 proxy_read_timeout,否则推送中断后前端会反复重连,表现为"事件流偶尔断开"。判断方法:curl -N http://域名/sse 观察是否在 60 秒左右断开,若断开就是超时设置问题。
三、上游 keepalive:让每个请求少一次 TCP 握手
默认情况下 Nginx 与后端每转发一个请求就新建一次 TCP 连接,高并发时握手开销不小。通过 upstream 的 keepalive 指令复用长连接,配合 HTTP/1.1 可以显著降低延迟和后端负载。
# http 块
upstream app_backend {
server 127.0.0.1:8080 max_fails=3 fail_timeout=10s;
server 127.0.0.1:8081 max_fails=3 fail_timeout=10s;
keepalive 32; # 每台 worker 保留的空闲连接数
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1; # keepalive 要求 HTTP/1.1
proxy_set_header Connection ""; # 清空 Connection,让后端感知长连接
}
}
注意:keepalive 必须配合 proxy_http_version 1.1 和清空的 Connection 头使用,否则连接无法复用。验证方式:ss -s 观察 ESTAB 连接数,或 ss -tnp | grep :8080 看同一源连接是否被反复复用。
四、真实客户端 IP 透传:X-Forwarded-For 与 proxy_protocol
经过反向代理后,后端看到的 IP 永远是 Nginx 的 IP。日志分析、风控、fail2ban 都会因此失效,必须把真实 IP 透传过去。
location / {
proxy_pass http://app_backend;
# 追加客户端 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;
}
安全提醒:X-Forwarded-For 是客户端可伪造的。如果后端直接信任它做风控,攻击者可以伪造任意 IP。推荐方案:
- Nginx 用
set_real_ip_from+real_ip_header X-Forwarded-For配合,只信任来自前置代理(如阿里云 SLB、Cloudflare)的连接; - 在 Nginx 这一层覆盖(而不是追加)X-Forwarded-For,后端再信任它就是安全的。
# 只信任前置代理网段,重写真实 IP(放在 http 块)
set_real_ip_from 100.64.0.0/10;
set_real_ip_from 203.0.113.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
对于四层入口(LVS、HAProxy)转发 TCP 的场景,用 proxy_protocol 协议透传原始连接信息,比 X-Forwarded-For 更可靠:
server {
listen 80 proxy_protocol;
set_real_ip_from 10.0.0.0/8; # 前置代理网段
real_ip_header proxy_protocol;
}
五、灰度发布:按权重与按 Cookie 分流
发布新版本时,想先让 10% 流量走新后端,或者让携带特定 Cookie 的测试用户走新版本,用 Nginx 就能实现,无需网关。
按权重灰度:
upstream app_gray {
server 127.0.0.1:8080 weight=90; # 老版本 90%
server 127.0.0.1:8090 weight=10; # 新版本 10%
}
按 Cookie 精准分流(测试组固定走新版本):
upstream app_old { server 127.0.0.1:8080; }
upstream app_new { server 127.0.0.1:8090; }
server {
listen 80;
server_name app.example.com;
# 带 gray=1 的 Cookie 走新版本
set $backend app_old;
if ($http_cookie ~* "gray=1") {
set $backend app_new;
}
location / {
proxy_pass http://$backend;
# 下面三个头照常带上
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
注意:Nginx 的 if 指令只在 location 内配合 set/return 使用是安全的,不要在 if 里写 proxy_pass 之外的复杂指令。灰度验证通过后,把 app_new 的 weight 逐步调大即可平滑放量。
六、四层反向代理:stream 端口转发与 SNI 分流
HTTP 之外的场景(MySQL、Redis、自建协议)同样可以用 Nginx 转发,不需要再装 iptables 或 frp。在 http 块之外新增 stream 块:
# 配置文件顶层(与 http 平级)
stream {
upstream mysql_backend {
server 10.0.1.10:3306;
}
server {
listen 3306;
proxy_pass mysql_backend;
proxy_timeout 60s;
proxy_connect_timeout 5s;
}
}
多域名 HTTPS 场景下,可以用 ssl_preread 按 SNI(客户端请求的域名)分流到不同后端,一台 Nginx 同时承载多个域名的 TLS 终结:
stream {
map $ssl_preread_server_name $backend {
a.example.com 10.0.1.10:443;
b.example.com 10.0.1.11:443;
default 10.0.1.12:443;
}
server {
listen 443;
proxy_pass $backend;
ssl_preread on;
}
}
排错:stream 层报错看 /var/log/nginx/stream.log(需在 stream 块里配置 error_log);连接建立不了先 telnet 目标IP 端口 或 nc -vz 验证本机到后端连通性。
七、反向代理排错指南:502/504 与常见配置错误
反向代理的报错大多集中在这几类,按下面的顺序排查最快:
1. 502 Bad Gateway:上游不可达
后端进程没起来、端口写错、防火墙拦截。先在本机验证:
curl -I http://127.0.0.1:8080 # 后端直接访问是否正常
ss -tlnp | grep 8080 # 端口是否在监听
nc -vz 127.0.0.1 8080 # 连通性测试
systemctl status 后端服务 # 服务状态
若本机正常但通过 Nginx 还是 502,检查 Nginx 错误日志:
tail -f /var/log/nginx/error.log
# 常见:connect() failed (111: Connection refused) —— 端口没监听或防火墙
# 常见:connect() failed (110: Connection timed out) —— 网络不通或后端过载
2. 504 Gateway Timeout:上游响应太慢
默认 proxy_read_timeout 60s,后端处理超过该时间就会 504。按业务调整:
proxy_connect_timeout 5s; # 建立连接超时
proxy_send_timeout 30s; # 发送请求超时
proxy_read_timeout 300s; # 读取响应超时,导出报表类接口调大
3. upstream sent too big header while reading response header
后端返回的响应头(比如 Set-Cookie 过多)超过了 Nginx 缓冲区。在 location 或 http 块调大:
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
4. SSL 证书链报错:upstream SSL certificate verify failed
Nginx 默认会校验证书链,自签名证书或证书链不全会报错。测试环境可临时关闭,生产环境应补全证书:
location / {
proxy_pass https://backend.internal;
proxy_ssl_server_name on; # 按上游域名发送 SNI
proxy_ssl_verify off; # 生产建议改为 on 并配 CA
}
5. proxy_redirect:后端返回的跳转地址不对
后端返回 301/302 时 Location 头写的是内网地址,客户端跳过去就错了。用 proxy_redirect 改写:
location / {
proxy_pass http://127.0.0.1:8080;
proxy_redirect http://127.0.0.1:8080/ http://$host/;
# 更省事的写法:把后端所有 Location 域名统一替换成客户端域名
proxy_redirect http://backend.internal/ /;
}
八、总结与建议
反向代理进阶的核心就三件事:头信息怎么传(Upgrade、X-Forwarded-For、Host)、连接怎么复用(keepalive)、流量怎么分流(权重/Cookie 灰度、SNI)。最后给几条上线前的检查清单:
- 修改配置后先
nginx -t校验语法,再nginx -s reload平滑重载; - 用
curl -H "Upgrade: websocket" -H "Connection: Upgrade" -i https://域名/ws验证长连接转发; - 用
curl -sI https://域名 | grep -i location检查重定向是否被 proxy_redirect 正确改写; - 后端日志确认收到真实客户端 IP,再上线风控/统计类功能。
配置不是一劳永逸的,线上出问题先看 /var/log/nginx/error.log 和 access.log 的上游响应时间,大部分问题都能在两份日志里找到答案。


