反向代理: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:
| location | proxy_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 -lntp、journalctl -u <服务名> |
| 504 Gateway Timeout | 上游在超时内没返回首个响应 | 慢 SQL、下游三方接口慢、超时确实偏小 | 先定位慢在哪,再决定是否调超时 |
| 413 Request Entity Too Large | 请求体超过 client_max_body_size | 上传大文件、批量导入 | 按业务调大,并在应用层同步限制 |
| 指令 | 含义 | 默认 | 建议值 |
|---|---|---|---|
proxy_connect_timeout | 与上游建立 TCP 连接的时间 | 60s | 3~5s,本机反代连不上就是没起来,等 60 秒毫无意义 |
proxy_send_timeout | 两次向上游写操作的间隔 | 60s | 60s;只有上传大文件才需要放大 |
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_timeout | 504 变成「更慢的 504」 | 先定位慢点,再按需放宽单个接口 |
WebSocket 没传 Upgrade | 连接 101 失败或 60 秒后断开 | Upgrade + Connection "upgrade" + 长超时 |
client_max_body_size 保持默认 1m | 上传报 413 | 在 server 或 location 调大 |
忘了 X-Forwarded-Proto | 应用生成 http 链接,重定向循环 | 四件套请求头都加上,并在应用侧信任代理 |
小结:反向代理的正确姿势是「路径拼接先定清楚、请求头四件套都转发、超时按现象分类处理」;proxy_pass 带 URI 会剥掉 location 前缀、不带就原样透传,永远用调试接口验证而不是靠猜;WebSocket 与 SSE 用独立 location 并放大读超时;504 的第一反应是查 $upstream_response_time 而不是改配置数字。