这几年配 Nginx 反向代理的次数不算少,但每次新起一个服务还是会在同样几个地方卡住。这次干脆把踩过的坑集中记一遍,下回直接抄。
一、漏配 proxy_set_header,后端拿到的全是 127.0.0.1
最典型的症状:应用日志里所有请求的来源 IP 都是 127.0.0.1 或者内网网关地址,限流、风控、访问统计全部失效。
原因是 Nginx 转发请求时会重新构造 HTTP 请求头,默认只带 Host 和 Connection,客户端的真实信息不会自动传下去。必须显式加上:
location /api/ {
proxy_pass http://127.0.0.1:8080;
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;
}
几个变量的区别值得说清楚:
| 变量 | 含义 |
|---|---|
$remote_addr | 直连 Nginx 的那一跳的 IP |
$proxy_add_x_forwarded_for | 在已有 XFF 后面追加 $remote_addr,形成完整链路 |
$host | 请求行或 Host 头里的域名,不带端口 |
$scheme | http 或 https |
X-Forwarded-Proto 这一条经常被忽略。如果 HTTPS 在 Nginx 层终止,后端应用看到的是明文 HTTP,框架生成的绝对 URL、重定向地址就会退化成 http://,浏览器随即报混合内容错误。
注意:如果 Nginx 直接暴露在公网,客户端伪造的
X-Forwarded-For会被原样追加。后端解析真实 IP 时应当从右往左数,或者在 Nginx 层用proxy_set_header X-Forwarded-For $remote_addr;直接覆盖。
WebSocket 还要额外两行
握手阶段依赖 Upgrade 和 Connection 头,而这两个是逐跳头部,默认不会转发,连接会稳定地停在 101 之前:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_http_version 默认是 1.0,不改的话 Upgrade 机制根本不生效。长连接还要把 proxy_read_timeout 调大,否则默认 60 秒无数据就会被断开。
二、proxy_pass 末尾那个斜杠
这是我个人栽得最多的一处。proxy_pass 的地址带不带结尾斜杠,转发出去的路径完全不同。
假设请求是 /api/user/list:
| 配置 | 实际转发路径 |
|---|---|
proxy_pass http://backend; | /api/user/list |
proxy_pass http://backend/; | /user/list |
规律是:URI 部分为空时,原始路径原封不动转发;带了 URI(哪怕只是一个 /),Nginx 会把 location 匹配到的前缀替换成这个 URI。
所以后端如果自己带 /api 前缀就用不带斜杠的写法,如果后端路由是从根开始的就用带斜杠的写法。写反的表现是清一色 404,但看 Nginx 日志一切正常,很容易往后端方向排查半天。
还有一个坑:只要 proxy_pass 里用了变量,就必须自己拼完整路径,否则默认不传递 URI。
# 变量写法必须显式带上 $request_uri
set $backend "http://127.0.0.1:8080";
proxy_pass $backend$request_uri;
三、location 匹配优先级
Nginx 的 location 不是从上往下先匹配先赢,顺序是固定的:
=精确匹配,命中立刻结束^~前缀匹配,命中后不再尝试正则- 正则
~(区分大小写)和~*(不区分),按配置文件里的书写顺序,第一个命中就用 - 普通前缀匹配,取最长的那一条
踩过的具体例子:静态资源配了一条正则处理缓存,同时又配了 /static/ 的普通前缀想单独指向另一个目录,结果正则优先级更高,前缀那条永远不生效。
# 想让 /static/ 走这里,必须用 ^~ 阻断后面的正则
location ^~ /static/ {
alias /var/www/assets/;
expires 30d;
}
location ~* \.(js|css|png|jpg|gif|ico)$ {
expires 7d;
access_log off;
}
alias 和 root 别混用
root 是把 location 路径拼在后面,alias 是把 location 路径整段替换掉:
location /static/ {
root /var/www; # 实际路径 /var/www/static/xxx
}
location /static/ {
alias /var/www/assets/; # 实际路径 /var/www/assets/xxx
}
用 alias 时,如果 location 以斜杠结尾,alias 的路径也必须以斜杠结尾,少一个就会拼出 /var/www/assetsxxx 这种诡异路径。
四、改完记得先检查再重载
nginx -t # 语法检查,会打印出错的行号
nginx -s reload # 平滑重载,不断开已有连接
直接 restart 会中断正在处理的请求,日常改配置一律用 reload。另外 nginx -T(大写)能把所有 include 进来的配置合并打印出来,排查「这条配置到底有没有被加载」时非常好用。
小结一下这次的教训:反向代理出问题时,先用 curl -v 直连后端端口确认服务本身正常,再对比 Nginx 转发过去的请求头差在哪。大部分「后端好好的,一过 Nginx 就不对」的问题,答案都在被丢掉的那几个头里。