本文是「Nginx 避坑」系列第二篇,本系列共四篇
后续篇目:HTTPS、安全与跨域 → 进阶与生产实践
这些 Nginx 反向代理的坑,我替你踩完了(反向代理与负载均衡)
https://oa22.cn/bbs.asp?id=38197
你的Nginx正在裸奔:配了HTTPS却从没真正安全过
https://oa22.cn/bbs.asp?id=38196
写了几年Nginx配置,这些错误我见过N多次了
https://oa22.cn/bbs.asp?id=38195
同事改了一行Nginx配置,大促当天系统崩—这5个教训,每一个都是真金白银换来的
https://oa22.cn/bbs.asp?id=38194
上周有个同事找我排查一个 502,搞了快两个小时,最后发现是 proxy_pass 末尾多了个斜杠。就这一个字符,两个小时。
类似的故事我见过太多了。反向代理是 Nginx 用得最多的场景,也是坑最集中的地方——不是因为 Nginx 难,而是因为这些坑看起来太不起眼了,很多人用了好几年都没意识到自己踩着。
今天把我见过的、踩过的、帮人排查过的反向代理坑全部整理出来。对照着检查一遍你现在的配置,大概率能挖出几个隐患。
先说那个折磨过最多人的坑
proxy_pass 末尾加不加斜杠,差别大了去了
说这是 Nginx 史上坑了最多人的配置,应该没有争议。
症状很典型:后端服务直接访问完全没问题,但套了一层 Nginx 代理之后,某些路径就 404 了。后端日志、应用代码挨个查,啥问题都没有。最后才发现,是 Nginx 把路径传错了。
根本原因是这个:
# 写法一:proxy_pass 末尾没有斜杠
location /api {
proxy_pass http://backend;
}
# 请求 /api/user → 后端收到 /api/user ✅ 路径原样传过去
# 写法二:proxy_pass 末尾加了斜杠
location /api {
proxy_pass http://backend/;
}
# 请求 /api/user → 后端收到 //user ❌ 路径直接乱了
# 写法三:location 和 proxy_pass 末尾都有斜杠
location /api/ {
proxy_pass http://backend/;
}
# 请求 /api/user → 后端收到 /user ✅ /api/ 被替换成了 /
规律其实不复杂:proxy_pass 里带了 URI 部分(哪怕只是一个斜杠),Nginx 就会把 location 匹配到的那段路径"剪切"掉,用 proxy_pass 里的 URI 替换。不带就原样传。
记不住规律也没关系,用 rewrite 显式控制,不猜:
location /api {
rewrite ^/api(/.*)$ $1 break;
proxy_pass http://backend;
}
从此再也不用靠记忆,看配置就知道后端会收到什么。
三个"看起来没问题,其实一直在坑你"的配置
一、Host 头悄悄被换掉了
后端用了 Spring Boot 或者 Django,本来应该跳转到 https://example.com,结果用户收到的重定向链接变成了 http://127.0.0.1:8080。
这不是后端的 bug,是 Nginx 默认把 Host 头替换成了 proxy_pass 里的地址。
加三行就解决了:
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;
proxy_set_header X-Forwarded-Proto $scheme;
}
顺带说一句:$host 和 $http_host 大部分时候结果一样,但 $host 更稳,能处理请求里没有 Host 头的边界情况,优先用 $host。
二、超时时间设成了一刀切
两种极端都很常见:
极端一:保留默认的 60 秒,后端有个导出报表的接口要跑 3 分钟,结果用户收到 504,但后端日志里这个请求是正常完成的。
location /api/export {
proxy_pass http://backend;
proxy_read_timeout 300s; # 这类慢接口单独放大
proxy_send_timeout 300s;
proxy_connect_timeout 10s; # 连接超时不用放大,快速失败更好
}
极端二:嫌麻烦,全局直接改成 300 秒甚至更大——然后某天后端宕机了,Nginx 傻等 5 分钟,连接积压满了,把自己也拖垮了。
正确姿势:全局保持合理默认值,已知的慢接口单独开小灶。不要一刀切。
三、X-Forwarded-For 被用户伪造了
后端根据客户端 IP 做限速、做安全审计,但有人发现只要在请求头里塞一个假的 X-Forwarded-For,后端就会把这个假 IP 当真的用。
原因很简单:Nginx 默认会把客户端传来的 X-Forwarded-For 原样拼上去,没有任何校验。
解决也简单,分两种情况:
# 情况一:Nginx 直接面向公网(没有 CDN 或上游代理)
proxy_set_header X-Forwarded-For $remote_addr;
# 直接用 $remote_addr,客户端传来的头完全忽略
# 情况二:CDN → Nginx → 后端,Nginx 是中间层
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 追加真实 IP,不覆盖已有链路
负载均衡:算法选错,代价很大
你用的是哪种算法?
默认是轮询,大多数场景没问题。但这两种情况会让你后悔:
场景一:后端接口响应时间差异明显,有的 10ms,有的 500ms。轮询不管三七二十一均分请求,慢接口那台机器越积越多,压垮。
upstream backend {
least_conn; # 改成最少连接数,谁空闲谁接活
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
场景二:应用有 Session,没做共享,用轮询导致同一个用户的请求被分到不同机器,登录状态丢失。
upstream backend {
ip_hash; # 同一个 IP 固定打同一台机器
server 192.168.1.10:8080;
server 192.168.1.11:8080;
}
但是,ip_hash 有个隐藏问题:如果用户都走同一个出口 IP(公司 NAT、CDN 节点),那所有请求会打到同一台机器,负载完全不均衡。这时候最好的解法是让后端做 Session 共享(Redis),然后用普通轮询,根本上解决问题。
backup 机器 + ip_hash,这个组合会让你查半天
upstream backend {
ip_hash;
server 192.168.1.10:8080;
server 192.168.1.11:8080;
server 192.168.1.12:8080 backup; # ⚠️ 这行不会生效
}
backup 和 ip_hash(以及 hash 指令)不兼容。配了也没用,甚至可能报错。踩过这个坑的人都说:为什么 backup 机器从来不接流量……
健康检查:开源版只有"被动发现"
开源版 Nginx 没有主动健康检查——它不会主动去 ping 后端,只会在请求失败之后才知道那台机器挂了。
实际影响是:在发现某台机器挂掉之前,已经有真实用户的请求失败了。
upstream backend {
server 192.168.1.10:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.11:8080 max_fails=3 fail_timeout=30s;
}
这是能做到的最好配置了:30 秒内失败 3 次,才把这台标记为不可用,等 30 秒后再试。
想要主动健康检查,选项有三个:Nginx Plus(付费)、OpenResty + lua-resty-healthcheck、或者直接用 K8s 让 kube-proxy 来做健康检查。
一个被忽视最多的配置:Nginx 到后端的长连接
没开 keepalive,TIME_WAIT 会把你搞崩
默认情况下,Nginx 对后端使用的是短连接:每来一个请求,新建一条 TCP 连接,用完就关。高并发场景下,这些关掉的连接会进入 TIME_WAIT 状态,等待 2MSL(大概 60 秒)才释放。
量一大,本机端口就耗尽了,后端开始拒绝连接,业务报错。
开启长连接,三行配置:
upstream backend {
server 192.168.1.10:8080;
keepalive 32; # 每个 worker 维持 32 个到后端的空闲长连接
}
location / {
proxy_pass http://backend;
proxy_http_version 1.1; # HTTP/1.1 才支持 keepalive
proxy_set_header Connection ""; # 清掉 Connection: close,否则后端会主动关
}
补充一点:keepalive 32 不是限制最大连接数,而是空闲连接池的大小。实际并发可以超过这个数,超出的连接用完就关,32 以内的会保留复用。
如果后端 IP 会变,这个坑可能要命
微服务环境、K8s 容器重启、弹性伸缩——这些都会导致后端 IP 变化。但 Nginx 只在启动时解析 DNS,之后不再解析。
后端换了 IP,Nginx 还在往老地址打流量,结果就是 502。
# 解决方案:用变量 + resolver,强制每次重新解析 DNS
resolver 8.8.8.8 valid=30s;
resolver_timeout 5s;
location / {
set $backend "backend.internal:8080";
proxy_pass http://$backend;
}
proxy_pass 里用了变量,Nginx 就不会缓存 DNS 结果了。
一份完整的 upstream 配置,别漏参数
upstream backend {
server 192.168.1.10:8080 weight=1 max_fails=3 fail_timeout=30s max_conns=1000;
server 192.168.1.11:8080 weight=1 max_fails=3 fail_timeout=30s max_conns=1000;
keepalive 32;
keepalive_requests 1000; # 单个长连接最多处理 1000 个请求
keepalive_timeout 60s; # 空闲长连接超过 60s 关闭
}
max_conns 这个参数特别容易被忽略:它限制了 Nginx 到单台后端的最大并发连接数,防止某台机器被压垮。需要 Nginx 1.11.5 以上,高并发场景下非常值钱。
最后说一句
这些坑里,有的是一个字符的问题(斜杠),有的是一行配置的遗漏(keepalive),有的是算法选错了方向(ip_hash + NAT)。没有一个是高深的技术问题,但每一个都能在生产上造成真实事故。
对照检查一遍你现在的配置,比什么都值。
下一篇聊 HTTPS、安全加固和跨域——有个能让你陷入重定向死循环的坑,还有一个 add_header 的继承陷阱,估计很多人从来没意识到自己中招了。
如果这篇对你有帮助,转发给还在踩这些坑的同事吧。
阅读原文:原文链接
该文章在 2026/3/27 16:36:52 编辑过