反向代理:proxy_pass 与请求头

反向代理是 Nginx 最常用的用法:浏览器只跟 Nginx 说话,真正的业务进程躲在 127.0.0.1 后面。功能上它只有两件事——把请求转给上游、把响应送回去;但真正出事故的地方几乎都在细节里:proxy_pass 结尾那个斜杠决定了路径去掉了还是留着,少写一个请求头后端就永远看到 127.0.0.1,超时和缓冲没调对就表现为 502、504 和上传失败。本章把这些细节一次讲清。

要解决的问题

  • 请求转过去之后,路径是 /api/user/1 还是 /user/1
  • 后端拿不到真实客户端 IP 与真实协议(以为是 http),日志、限流、跳转全错。
  • 应用返回慢或没起来时,到底该改超时还是去修应用。
  • WebSocket / SSE 这类长连接一过 60 秒就断。

最小可用反向代理配置

# /etc/nginx/conf.d/api.example.com.conf
upstream app_backend {
    server 127.0.0.1:8080;
    keepalive 32;                       # 与上游保持长连接,必须配合清空 Connection
}

server {
    listen 443 ssl;
    server_name api.example.com;
    client_max_body_size 50m;           # 默认 1m,上传接口必须显式放大

    location / {
        proxy_pass http://app_backend;  # 这里没有 URI 部分,路径原样透传
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        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;
        proxy_set_header Connection        "";   # 清空,才能复用 upstream 长连接

        proxy_connect_timeout 5s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;

        proxy_buffering on;                 # 默认开;SSE/流式响应要改 off
        proxy_buffers 8 16k;

        proxy_next_upstream error timeout http_502 http_503 http_504;
    }
}

proxy_pass 结尾斜杠:四种组合

规则只有一句:proxy_pass 带 URI(结尾有 //v1/ 等路径)时,location 前缀会被替换掉;不带 URI 时,完整请求路径原样透传。 假设请求是 /api/user/1

locationproxy_pass上游实际收到说明
location /api/http://127.0.0.1:8080/api/user/1不带 URI,原样透传,最不容易踩坑
location /api/http://127.0.0.1:8080//user/1单个 / 把 location 前缀整体替换掉
location /api/http://127.0.0.1:8080/v1//v1/user/1前缀被替换为 /v1/,常用于后端带版本号
location ~ ^/api/(.*)$http://127.0.0.1:8080/api/$1报错正则 location 里直接写 URI 会 [emerg] cannot have URI part,必须含变量才合法

记住一条经验:要么全都不带斜杠让它透传,要么两边都带 / 让前缀被剥掉;混着写时,前端请求 /api/user(无尾斜杠)也匹配 /api/,替换结果会少一段,这类 bug 只有看后端日志才发现。

必须转发的请求头

请求头常用变量不转发的后果
Host$host后端只看到 app_backend,多站点路由、签名校验、生成绝对 URL 全错
X-Real-IP$remote_addr应用日志里全是 127.0.0.1,风控与审计失效
X-Forwarded-For$proxy_add_x_forwarded_for拿不到真实客户端 IP,应用层限流失效(该变量会追加而非覆盖,代理链可追溯)
X-Forwarded-Proto$scheme应用以为自己在 http 上,生成 http 链接或重定向死循环
Connection""默认会传 Connection: close,上游长连接永远建不起来

排查「后端拿不到真实 IP/协议」的顺序:

# 1) 直接问后端它收到了什么(写一个只回显请求头的调试接口)
curl -s http://127.0.0.1:8080/debug/headers | grep -i -E 'x-real-ip|x-forwarded|host'
# 2) 经过 Nginx 再问一次,对比差异
curl -s https://api.example.com/debug/headers | grep -i -E 'x-real-ip|x-forwarded|host'
# 3) 确认 Nginx 侧确实配了这些头
nginx -T | grep -n 'proxy_set_header'

两者一致说明 Nginx 侧没问题,此时要去应用框架里「信任代理」:Express 需要 app.set('trust proxy', 'loopback'),Django 需要 USE_X_FORWARDED_HOST/SECURE_PROXY_SSL_HEADER,Spring Boot 需要 server.forward-headers-strategy=framework。Nginx 发了头,应用不认,同样白搭。

WebSocket 与长连接

location /ws/ {
    proxy_pass http://app_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;   # 透传 Upgrade: websocket
    proxy_set_header Connection "upgrade";       # 这里必须是 upgrade,不能是 ""
    proxy_read_timeout 3600s;                    # 空闲 60 秒就被断的元凶就是它
    proxy_buffering off;                         # 长连接/SSE 建议关闭缓冲
}

WebSocket 与普通反代是两套写法:普通请求要把 Connection 清空以复用长连接,WebSocket 必须把它改成 upgrade。同一个 server 里用不同的 location 分开,别用一条配置硬凑。

超时与缓冲:502/504 到底是谁的问题

现象含义常见原因处理方向
502 Bad Gateway连不上上游,或上游返回了无法解析的响应应用没起来、端口写错、进程刚崩溃ss -lntpjournalctl -u <服务名>
504 Gateway Timeout上游在超时内没返回首个响应慢 SQL、下游三方接口慢、超时确实偏小先定位慢在哪,再决定是否调超时
413 Request Entity Too Large请求体超过 client_max_body_size上传大文件、批量导入按业务调大,并在应用层同步限制
指令含义默认建议值
proxy_connect_timeout与上游建立 TCP 连接的时间60s3~5s,本机反代连不上就是没起来,等 60 秒毫无意义
proxy_send_timeout两次向上游写操作的间隔60s60s;只有上传大文件才需要放大
proxy_read_timeout两次从上游读操作的间隔60s常规 60s;WebSocket/SSE 单独放大到小时级

调大超时不是 504 的修复。把 proxy_read_timeout 从 60s 改成 600s,只把「用户 60 秒后看到错误」变成「用户 10 分钟后看到同样的错误」,慢查询和慢依赖一个都没解决。正确顺序是:先看 $upstream_response_time 定位慢在应用还是慢在依赖,能优化就优化,优化不了再给这个接口单独放宽超时。

proxy_buffering on 时 Nginx 尽快读完上游响应再按客户端速度慢发,上游连接可以立刻释放,是默认且推荐的设置;只有 SSE、长轮询、流式下载才关掉。响应头特别大时会出现 an upstream response is buffered to a temporary file 警告,调大 proxy_buffer_size(默认 4k/8k)即可。

上游也是 HTTPS

location / {
    proxy_pass https://backend.example.com;
    proxy_ssl_server_name on;      # 发送 SNI,否则多域名上游可能拿到默认证书
    proxy_ssl_verify on;           # 开启后必须给可信 CA,否则校验必然失败
    proxy_ssl_trusted_certificate /etc/nginx/certs/ca.pem;
}

验证方法

sudo nginx -t && sudo systemctl reload nginx                    # 配置改动固定两步(等价 nginx -s reload)
curl -I https://api.example.com/                                 # 状态码与 Server 头
curl -s -o /dev/null -w 'code=%{http_code} total=%{time_total}\n' https://api.example.com/
curl -s https://api.example.com/debug/headers | grep -i x-forwarded-proto
curl -i http://127.0.0.1:8080/healthz                            # 绕过 Nginx 直测上游
sudo tail -f /var/log/nginx/error.log                            # 502/504 现场

常见坑

现象做法
proxy_pass 结尾斜杠想当然后端路径多一段或少一段,404/405按上表确认组合,用 /debug/headers 验证
忘了 X-Forwarded-Proto应用生成 http 链接,重定向循环四件套请求头都加上,并在应用侧信任代理
只调大 proxy_read_timeout504 变成「更慢的 504」先定位慢点,再按需放宽单个接口
WebSocket 没传 Upgrade连接 101 失败或 60 秒后断开Upgrade + Connection "upgrade" + 长超时
client_max_body_size 保持默认 1m上传报 413serverlocation 调大
忘了 X-Forwarded-Proto应用生成 http 链接,重定向循环四件套请求头都加上,并在应用侧信任代理

小结:反向代理的正确姿势是「路径拼接先定清楚、请求头四件套都转发、超时按现象分类处理」;proxy_pass 带 URI 会剥掉 location 前缀、不带就原样透传,永远用调试接口验证而不是靠猜;WebSocket 与 SSE 用独立 location 并放大读超时;504 的第一反应是查 $upstream_response_time 而不是改配置数字。

笔记加载中…