Nginx 反向代理进阶实战:WebSocket 转发、真实 IP 透传与灰度发布


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。推荐方案:

  1. Nginx 用 set_real_ip_from + real_ip_header X-Forwarded-For 配合,只信任来自前置代理(如阿里云 SLB、Cloudflare)的连接;
  2. 在 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 的上游响应时间,大部分问题都能在两份日志里找到答案。


手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

VPS磁盘扩容实战:控制台扩容后growpart扩展分区与文件系统resize全流程

MySQL 数据库备份实战:mysqldump 逻辑备份、binlog 增量与 xtrabackup 恢复演练

评 论
更换验证码