
我做了这么多年 Nginx 相关的分享和排障要说哪个指令被问得最多、坑最深proxy_pass绝对排第一。无论是刚入门配个前后端分离还是老手调负载均衡几乎每天都在跟它打交道。很多人觉得它简单不就是把请求转发给后端嘛但真到线上出了问题——路径多了个或少了个斜杠、页面 502、代理循环——才发现这里面门道不少。这篇内容我就把自己实际使用proxy_pass的经验整理一遍从最基础的“反向代理到底在干什么”讲起再拆解路径转发规则、负载均衡和 keepalive 配置最后把常见的坑和排查手段都列出来。适合正在学 Nginx 的开发者也适合被线上代理问题折腾过的运维朋友参考。1. proxy_pass 是什么先搞懂“反向代理”这件事1.1 从快递柜说起正向代理和反向代理的区别理解proxy_pass之前得先分清两个容易混淆的概念正向代理和反向代理。我用一个生活场景来解释你一下就明白了。正向代理像什么呢像你出国访问某个网站但你的网络访问不了于是你找了一台能访问的服务器请它帮你把内容拿回来你再通过它访问。在这个过程里客户端清楚知道代理服务器的存在也明确配置了要去使用它而目标服务器并不知道真正的访问者是你。也就是说正向代理是“替客户端”去访问目标服务器它代表的是客户端。反向代理就不一样了。它更像是公司的前台。你不需要知道某个部门具体在哪个工位只需把快递送到前台前台根据你写的收件人把快递转到正确的工位。整个过程对客户端来说它只认识前台这一个入口并不知道背后有几台真实服务器在工作。反向代理是“替服务器”接收请求然后分发给后端它代表的是服务端。proxy_pass指令就是负责完成“前台转发快递”这个动作的核心。当 Nginx 收到一个请求后如果匹配的location配置里有proxy_pass它就会把请求原封不动地转发给配置里指定的后端地址然后把后端返回的内容再交给客户端。站在客户端看响应还是来自 Nginx但它实际拿到的是后端服务器的处理结果。1.2 proxy_pass 的语法和最基本的转发模型proxy_pass的官方语法非常简单proxy_pass URL;这里的 URL 可以是一个http://开头的后端地址也可以是https://地址还可以是grpc://这类协议既可以写 IP也可以写域名还可以带上端口。最常见的写法是下面这样server { listen 80; server_name example.com; location /api/ { proxy_pass http://192.168.1.10:8080; } }这段配置的意思是当客户端请求http://example.com/api/xxx时Nginx 会把这个请求转发给http://192.168.1.10:8080去处理然后接收响应并返回给客户端。在你刚开始尝试配置的时候先记住一个简单的模型没有proxy_passNginx 只是静态文件服务器有了proxy_passNginx 才真正变成了应用网关。后端不需要直接暴露公网 IP统一由 Nginx 入口来承接流量不管是安全还是日后扩容都会方便很多。2. 核心细节proxy_pass 的路径转发规则不能搞错2.1 带不带斜杠结果完全不一样如果要在proxy_pass的使用里找一个最容易踩的坑我首推“URI 部分要不要写”。很多人配完之后发现后端收到的路径不对或者页面 404八成是这里出了问题。proxy_pass的参数里如果写了 URI 路径部分也就是http://域名/端口后面的那一串那么 Nginx 会把location匹配到的前缀部分“替换掉”。我直接举两个对比配置这个规则的重要性会看得很清楚第一种不带 URIlocation /api/ { proxy_pass http://192.168.1.10:8080; }这种写法下Nginx 转发时会把完整的原始请求 URI 原样传给后端。客户端请求/api/user/list后端收到的就是GET /api/user/list。后端的接口如果本来就设计成带/api前缀的那这种方式最省事不容易出错。第二种带 URIlocation /api/ { proxy_pass http://192.168.1.10:8080/; }注意看proxy_pass后面多了一个/。这个细节就是分水岭。此时 Nginx 的行为是把location匹配到的/api/前缀替换成/然后再把剩下的路径拼上去。客户端请求/api/user/list后端收到的实际路径是/user/list也就是/api/被剥离了。很多前后端分离项目前端只调用/api开头的接口后端服务本身却并不带这个前缀那么你就需要用第二种写法把前缀剥掉。如果后端的接口又确实需要/api那你就老老实实用第一种不加斜杠的写法。我把这两种情况整理成一个对照表你在配置的时候直接对号入座客户端请求路径location 配置proxy_pass 配置后端实际收到的路径/api/user/listlocation /api/http://backend:8080/api/user/list/api/user/listlocation /api/http://backend:8080//user/list/api/user/listlocation /apihttp://backend:8080/api/user/list前缀被替换为空原 uri 保留/api/user/listlocation /apihttp://backend:8080//user/list前缀 /api 被替换为 /这条规则的底层逻辑不复杂你只需要记住proxy_pass里带不带 URI决定了匹配到的 location 前缀会不会被“吞掉”。但实践中因为忽略它而翻车的例子真的数不过来。2.2 匹配优先级和 proxy_pass 结合时的注意点关于路径匹配还有一个容易忽略的问题location的匹配优先级。location /api/这种是前缀匹配如果项目里同时存在精确匹配location /api/login和普通前缀匹配location /api/Nginx 会优先走精确匹配那一条。所以你在排查代理路径问题时第一件事不是去看proxy_pass而是先确认请求到底进了哪个location。判断请求匹配到了哪个location最笨也最有效的办法就是在每个 location 里加一个响应头来做标记location /api/ { add_header X-Debug-Location api-prefix; proxy_pass http://192.168.1.10:8080; }然后请求后用 curl 看一眼响应头就能确定实际生效的 location 是哪一个。curl -I http://example.com/api/user/list看到X-Debug-Location: api-prefix就说明请求确实走到了这个块里接下来再去排查proxy_pass的路径规则才有意义。我每次做复杂路由配置的时候都会先用这个办法确认现场省去了很多预期之外的“迷之行为”。2.3 和 rewrite 一起用时uri 已经被改写了还有一个和路径相关的场景要单独拎出来说proxy_pass配合rewrite。rewrite会在proxy_pass转发之前改变请求的 URI而proxy_pass使用的是改变后的 URI 来做替换和拼接。看个实际的例子location /old/ { rewrite ^/old/(.*)$ /new/$1 break; proxy_pass http://backend:8080; }客户端的请求是/old/user/inforewrite 把 URI 改写成了/new/user/info由于后面带了break标志不会再继续匹配其他 location然后proxy_pass转发的就是改写后的 URI后端收到的是/new/user/info。如果这里proxy_pass写成http://backend:8080/那么 Nginx 会用 location 前缀/old/去替换成/但此时 URI 已经被 rewrite 成/new/user/info前缀替换的操作会影响它吗会的规则仍然是“匹配到的 location 前缀被替换为 proxy_pass 的 URI”但由于 URI 已经被改写成/new/开头不再以/old/开头这个替换实际上不会生效最终转发出去的就是完整的/new/user/info。这种组合拳在复杂路由重构时非常有用但也是逻辑最容易绕晕的地方。我的经验是在涉及 rewrite 的 location 里尽量在 rewrite 语句后用break而不是last避免再次进入 location 匹配循环否则很容易出现意想不到的 404 或者循环重定向。3. 反向代理实战配置从单机转发到负载均衡3.1 最基础的反向代理配置还应该带上这些请求头直接用最简配置做转发后端的请求能通但往往会有一些小问题——比如后端的日志里拿不到客户端真实 IP看到的全是 Nginx 服务器的内网 IP比如后端使用了 HTTPS 但不知道客户端原始协议是什么。所以在实际项目中我会建议在location里配置proxy_set_header这是和proxy_pass搭档频率最高的几个指令location /api/ { proxy_pass http://192.168.1.10: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; }这几个请求头各自负责的事情Host $host把客户端请求里的 Host 原样传给后端。如果不设置Nginx 默认会把proxy_pass里写的后端地址作为 Host很多后端服务做了域名校验的话就会直接 403。X-Real-IP $remote_addr把直连 Nginx 的客户端 IP 告诉后端。X-Forwarded-For因为在多层代理下后端拿到的$remote_addr可能只是上一级代理的 IP所以要把整个链路里的 IP 追加传递。X-Forwarded-Proto标记客户端原始请求是 HTTP 还是 HTTPS后端做跳转或者回调时经常用到。这些配置不写功能上大多也能通但一旦涉及用户 IP 识别、安全风控、回调地址拼接坑就来了。宁可一开始就配上也别等出了问题再补。3.2 用 upstream 做负载均衡proxy_pass 指向服务组单台后端扛不住流量的时候就要上负载均衡了。做法是在http块里定义一个upstream服务组然后把多个后端地址放进去最后proxy_pass直接指向这个组upstream backend_cluster { server 192.168.1.10:8080 weight3; server 192.168.1.11:8080 weight1; server 192.168.1.12:8080 backup; } server { listen 80; location /api/ { proxy_pass http://backend_cluster; proxy_set_header Host $host; } }这个配置里的含义正常情况下 Nginx 会把请求分发到 10 和 11 两台机器权重比是 3:1也就是每 4 个请求大约有 3 个落到 10 上、1 个落到 11 上。12 是备份节点平时不接收流量只有当 10 和 11 都不可用的时候Nginx 才会把请求转发给备份节点。upstream里常用的还有least_conn按当前连接数分配、ip_hash按客户端 IP 哈希分配写在前面的server列表之前即可upstream backend_cluster { least_conn; server 192.168.1.10:8080; server 192.168.1.11:8080; }ip_hash适合需要会话保持的场景同一个客户端 IP 会始终访问同一台后端least_conn适合后端请求处理时间差异较大的场景。默认的轮询方式则适用于绝大多数无状态服务。3.3 keepalive 连接复用参数别忽略如果你在upstream里只配了 server 列表就完事那我提醒一句把keepalive也加上。它控制的是 Nginx 和后端服务器之间保持的空闲长连接数量。upstream backend_cluster { server 192.168.1.10:8080; server 192.168.1.11:8080; keepalive 32; } server { location /api/ { proxy_pass http://backend_cluster; proxy_http_version 1.1; proxy_set_header Connection ; } }如果没有keepaliveNginx 每转发一个请求就要和后端新建一次 TCP 连接请求量一大握手开销瞬间拉满表现就是后端连接数暴涨、响应变慢。加上之后Nginx 会维护一批和后端之间已经建立的长连接空闲时复用性能提升非常明显。这里有两处配套设置不能少proxy_http_version 1.1是因为 HTTP/1.0 默认不支持 keep-alive必须显式声明 1.1 才行proxy_set_header Connection 是清掉客户端请求里的 Connection 头避免它干扰 Nginx 与后端之间的连接管理。这三个参数要一起出现才算完整。3.4 HTTPS 和 WebSocket 转发场景proxy_pass不只是转发 HTTP 明文流量HTTPS 场景同样常见。对外提供 HTTPS 服务时Nginx 负责处理证书和 TLS 握手然后把解密后的请求以明文 HTTP 转发给内网后端这是最常见的架构。server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/certs/example.com.pem; ssl_certificate_key /etc/nginx/certs/example.com.key; location / { proxy_pass http://192.168.1.10:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } }后端不需要处理和证书相关的事情证书统一由 Nginx 管理省事很多。WebSocket 转发则稍有不同因为 WebSocket 协议升级需要特殊请求头支持只在location里写一个proxy_pass是不够的。标准配置是location /ws/ { proxy_pass http://backend:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }Upgrade和Connection upgrade两个请求头是 WebSocket 协议升级的关键proxy_read_timeout要调大因为 WebSocket 连接是长连接默认的 60 秒读超时会导致连接被 Nginx 掐断。4. 常见问题与排查技巧实录踩坑后总结出来的速查表4.1 502 Bad Gateway 不完全等于后端挂了很多人一看到 502 就冲到后端去查日志结果后端一切正常。这里要先明确502 的含义是 Nginx 无法从上游服务器获得有效响应但原因并不只有后端进程挂掉这一种。常见的有Nginx 连不上后端端口防火墙拦了、后端监听地址写成了 127.0.0.1、后端处理超时、upstream 里的节点全部不可用、以及代理地址本身不可达。我建议的排查顺序是# 1. 先确认 Nginx 视角能否连通后端 curl -v http://192.168.1.10:8080/health # 2. 再看 Nginx 错误日志 tail -f /var/log/nginx/error.log错误日志里如果出现connect() failed (111: Connection refused)说明端口不通要么后端没起来要么监听地址不对如果出现upstream timed out而后端日志里其实也在处理那就要调proxy_read_timeout或proxy_send_timeout。502 的排障重点从来不是只看状态码本身而是要顺藤摸瓜找到具体是哪一环断掉了。4.2 404 和路径丢失先看斜杠再看 rewrite后端返回 404第一反应不要往后端代码里去查先确认后端收到的路径是不是你预期的路径。我举一个实际案例有个同事配置了这样的转发location /api/ { proxy_pass http://backend:8080/api/; }客户端请求/api/user按替换规则/api/被替换成/api/后端收到的还是/api/user这没问题。但一旦他写成location /api/ { proxy_pass http://backend:8080; }后端收到的还是/api/user依然没问题。最经典的错误是location /api/ { proxy_pass http://backend:8080; # 期望剥掉前缀 }没有斜杠的写法不会剥前缀后端如果只认/user那自然就是 404。遇到 404第一反应就是检查proxy_pass里是否有 URI、以及自己是不是真的想要这个 URI。如果路径里还叠了 rewrite就先在 location 里加add_header X-Debug-Uri $uri;看看转发前 URI 到底变成了什么。4.3 403 和代理循环注意访问控制与其他转发链路403 的情况稍微复杂一些可能原因包括后端目录没有索引文件且禁用了 autoindex、Nginx 配置了 deny 规则、或者后端做了 Referer/来源校验。先看 Nginx 错误日志确认是哪一层返回的 403如果错误日志里没有记录而响应头有Server: nginx标记基本可以断定是 Nginx 自己的访问控制拦的再去查allow和deny规则。代理循环的现象是请求一直在跳转最终浏览器报“重定向次数过多”。这种情况通常是因为请求又回到了 Nginx 自己然后location又匹配到同一条proxy_pass形成一个环。比如把proxy_pass指向了本机的另一个 server 块那个 server 块又通过某种方式把请求指了回来。排查时在location里开启 debug 日志或用curl -L观察跳转链路很快就能定位是哪个环节把流量又绕回到原地。4.4 域名解析缓存为什么改 DNS 不生效还有一种比较隐蔽的情况proxy_pass里写的是域名比如proxy_pass http://backend.example.com;然后你在 DNS 服务商那里把域名解析改了希望流量切到新服务器结果 Nginx 始终还是往旧地址转发。原因在于 Nginx 在启动或 reload 时解析一次上游域名之后在运行期间默认不会重新解析。这就意味着单纯改 DNS 并不会让 Nginx 立刻切换目标。如果你希望每次转发都实时解析域名可以使用变量来强制 Nginx 在请求时动态解析location /api/ { resolver 8.8.8.8 valid30s; set $backend_upstream http://backend.example.com:8080; proxy_pass $backend_upstream; }proxy_pass使用变量后Nginx 会在请求处理时通过resolver指定的 DNS 重新解析域名不再依赖启动时的缓存。代价是会带来一点额外的解析延迟而且要注意resolver的配置要能覆盖到这个 location否则会报找不到 resolver 的错误。4.5 常见问题速查表为了方便你直接对照排查我把上面提到的典型问题和解决方向整理成表现象可能原因排查/解决方向502 Bad Gateway后端进程没起、端口不通、防火墙拦截、超时用 curl 测后端连通性检查 Nginx error.log 里的具体 connect/refused/timeout 信息404 Not Foundproxy_pass 带/不带 URI 和预期不符、rewrite 改变了路径确认替换规则在 location 里临时加add_header X-Debug-Uri $uri;观察实际转发 URI403 ForbiddenNginx allow/deny 规则、目录无索引文件、后端来源校验失败看响应头 Server查 Nginx error.log 是 access denied 还是 directory index 类型重定向次数过多代理循环、后端跳转地址拼接错误用 curl -L 观察跳转链确认 X-Forwarded-Proto 和 Host 是否传到后端改了 DNS 不生效Nginx 缓存了启动时的解析结果使用变量 resolver 配置让请求实时解析WebSocket 连接被断开缺少 Upgrade/Connection 头、read timeout 太短补上proxy_set_header Upgrade $http_upgrade;等配置并增加 timeout4.6 再给两条省心的排查习惯除了上面这些具体问题我还建议你在所有代理类配置里养成两个小习惯。第一个习惯是给 location 增加基础观测点。不要只配一个proxy_pass就完事至少加上access_log和响应头标记这样请求到了 Nginx 这一层后有没有进入预期的 location、转发的目标是什么都能快速确认。加响应头的方法前面讲过用add_header X-Backend-Server $upstream_addr;甚至能看到这次请求实际被转发到了哪台后端在负载均衡场景下排查单节点问题时尤其好用。第二个习惯是做任何修改后用nginx -t先做语法检查再 reload。看起来是废话但不少人确实直接nginx -s reload结果配置文件有错的时候才发现没提前验证。语法检查又不需要重启服务养成习惯成本极低收益却很大。还有一种情况是 reload 时提示配置文件有问题此时 Nginx 会继续用旧配置运行你如果没注意到会一直以为新配置已经生效这比直接报错更坑。5. 一些实际操作中的体会和后端配合事项proxy_pass虽然只是 Nginx 众多指令中的一个但它更像是一座桥梁桥的一端是客户端另一端是后端服务。在做架构设计的时候不要只把它当成一个转发开关还应该考虑到整个链路是否顺畅。比如后端的接口路径设计。如果团队从一开始就约定所有接口带统一前缀比如/api网关层用不带 URI 的proxy_pass原样转发整个逻辑会非常清爽如果历史原因导致前后缀不统一那就只能用替换规则。路径规则没有绝对的好坏但要和团队约定清楚并在配置里写注释说明这段转发的意图。Nginx 配置本身不支持特别复杂的逻辑但可读性直接关系到后续维护的人会不会骂娘。再比如后端服务的监听地址。很多后端开发会把服务监听在127.0.0.1:8080这在本地开发没问题但一旦要通过 Nginx 做代理转发Nginx 从别的机器或者容器网络访问这个端口就会失败。部署联调之前先确认后端监听的地址是0.0.0.0而不是127.0.0.1能省下大把排查时间。还有一点关于超时参数的体会。proxy_connect_timeout的默认值是 60 秒proxy_read_timeout也是 60 秒。对大多数接口来说够用但如果有大文件上传、流式下载、或者 WebSocket 长连接一定要针对具体 location 单独调整而不是全局统一改否则容易让其他接口的异常响应也拖很久才暴露。我在实际项目里发现很多代理层的疑难杂症到最后都不是proxy_pass本身的问题而是路径语义、超时边界、DNS 解析、或者后端监听地址这些周边因素。把这些因素提前理清楚配置proxy_pass就会变成一件非常顺滑的事情。希望这份经验能让你少走一些我走过的弯路。