Nginx 反向代理那些容易踩的坑
Nginx 反向代理看着是个一行配置就能搞定的事:location /api/ { proxy_pass http://backend:8000; }。但真上线后你会发现一堆诡异现象——后端拿不到真实 IP、刷新 SPA 页面 404、Safari 上偶发连接断开、HTTPS 跳转成 HTTP 死循环……
这些坑我一个个都踩过。这篇把最常遇到的几个整理出来,每个都附上经过验证的配置片段。把这几处理顺,线上能少一半莫名其妙的故障。
一、一个标准的 /api/ 反代配置
先上一段我目前在用的、相对完整的反代配置,下面逐条解释为什么这么写:
# /etc/nginx/conf.d/default.conf
server {
listen 80;
server_name example.com;
# 后端 API 反代
location /api/ {
proxy_pass http://backend:8000;
# 传递真实请求信息(关键,见第二节)
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_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
二、proxy_set_header 的四个必填项
反代之后,后端拿到的请求来源变成了 Nginx 自己(IP 是 Nginx 所在机器的内网 IP,比如 172.18.0.1),原始用户的信息全丢了。这会导致:
- 日志里的 IP 全是 Nginx 的,无法做风控、地理统计
- 依赖
X-Forwarded-Proto判断 HTTPS 的代码(如 OAuth 回调)会出错 - 基于 Host 的逻辑(如生成绝对 URL)拿到的是内网地址
所以这四个 header 一个都不能少:
| Header | 取值 | 作用 |
|---|---|---|
Host | $host | 原始域名,后端生成 URL 用 |
X-Real-IP | $remote_addr | 用户真实 IP(单值) |
X-Forwarded-For | $proxy_add_x_forwarded_for | 代理链路,风控常用 |
X-Forwarded-Proto | $scheme | 原始协议 http/https |
注意$proxy_add_x_forwarded_for会把已有的 XFF 值追加上去,而不是覆盖。如果你的 Nginx 直接面向公网,要警惕客户端伪造 XFF,必要时只信任$remote_addr。
三、proxy_http_version 1.1 与长连接
Nginx 默认用 HTTP/1.0 跟后端通信,这意味着每个请求都会新建一次 TCP 连接,用完就关。对高 QPS 的接口,这是巨大的浪费——光握手就够喝一壶。
开启长连接只需两行:
proxy_http_version 1.1;
proxy_set_header Connection "";
第一行升级协议到 1.1,第二行清掉 Connection: close 这个默认头,让 Nginx 和后端之间复用连接。配合后端的 keep-alive,吞吐能提升 20%~40%。
upstream 块里加 keepalive 32;,维护一个到后端的连接池,配合上面的两行,效果最佳。
四、SPA history 模式与 try_files
Vue/React 用 history 路由时,用户刷新 /users/123 这种深层路由,浏览器会向服务器请求这个路径。但服务器上根本没有 /users/123/index.html,于是返回 404。
解决办法是用 try_files 把找不到的路径都回退到 index.html,让前端路由接管:
# 前端静态资源 + SPA history 回退
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# 重要:/index.html 不能被缓存,否则发布新版后用户拿到旧入口
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
try_files $uri $uri/ /index.html 的含义是:先找文件、再找目录、最后回退到 index.html。这是 SPA 上线必加的一行,没有它用户一刷新就 404。
五、Safari keepalive 复用断连的奇怪 bug
这是个非常隐蔽的坑,折磨过我整个下午。现象是:iOS Safari 上偶发性出现请求挂起、空白页或 ERR_SPDY_PROTOCOL_ERROR,Chrome 上完全正常。
排查后发现是 keepalive_timeout 设得过长(默认 65s 甚至更高),Safari 对长连接复用有比较严格的超时判断,连接被服务端保持太久后,Safari 误判为已断开,再复用时直接报错。
keepalive_timeout 适当调低,或者在出问题的场景干脆对 Safari 关掉 keep-alive。我现在的折中配置是 keepalive_timeout 30s;,兼顾性能和兼容。
# nginx.conf 的 http 块
keepalive_timeout 30s;
keepalive_requests 1000; # 单个连接最多复用 1000 次后关闭
send_timeout 60s;
另一个相关的小坑:send_timeout 是两次写操作之间的间隔超时,不是总时长。慢客户端长连接可能撑很久不被断,必要时配合 client_body_timeout、client_header_timeout 一起调。
六、静态资源缓存:hash 文件 immutable
前端构建产物带 hash(如 app.7f3a9c.js),内容变了 hash 就变,可以放心地永久缓存。而 index.html 是入口,绝不能缓存——否则发版后用户拿到的还是旧入口,引用的还是旧 hash 文件。
策略很清晰:
# 带 hash 的静态资源:永久缓存(内容变了 hash 就变,安全)
location ~* \.(js|css|png|jpg|jpeg|gif|webp|svg|ico|woff2?)$ {
root /usr/share/nginx/html;
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
access_log off;
}
# index.html:绝不缓存,保证发版即时生效
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires off;
}
关键字 immutable 告诉浏览器:这个资源永远不会变,连 304 校验请求都不用发。带 hash 的资源用上它,回访用户的加载速度会有质的提升。同时别忘了关掉这类静态资源的 access_log,省一堆日志噪音。
一个小坑:用 add_header 在子 location 里会覆盖父级的所有 add_header,而不是追加。所以如果你在 server 块设了 HSTS、CSP 等公共头,子 location 里要重新声明一遍,否则会丢失。
✓ 小结
Nginx 反代的坑基本都集中在"信息丢失"和"连接管理"两件事上。四个 proxy_set_header 让后端能拿到真实的用户信息;HTTP/1.1 + Connection "" 让连接能复用;try_files 让 SPA 不再 404;hash 资源 immutable + index.html 不缓存让发版顺畅。每一条都是上线前必须检查的清单。把这些理顺之后,Nginx 这一层基本能稳定运行很久不需要再碰。