Nginx 的 location 和 proxy_pass这两条指令称得上反向代理配置里最容易翻车的地方。我见过太多同事配置完一测后端收到路径莫名其妙多了一段或者怎么匹配都命中不了预期的 location最后只能一脸懵地重启调试。说白了多数问题就出在没搞懂 location 的匹配顺序以及没吃透 proxy_pass 带不带 URI 到底有什么区别。这篇文章就把这两兄弟一次说透。我会从 location 的匹配规则讲起再拆解 proxy_pass 的路径替换逻辑最后给几组可以直接抄的实战配置和排查实录。不管你是刚接触 Nginx 的新手还是已经维护过生产环境的运维按这篇文章的思路去理解基本能避开绝大多数坑。1. location 匹配规则先解决“请求落到哪个块”的问题很多人配置 location 时是按直觉写的觉得“我写了个 /api那 /api 开头的不就该命中它吗”。实际不是Nginx 有一套自己的匹配顺序理解这个顺序是理解 proxy_pass 的前提。1.1 四种修饰符以及它们到底匹配什么location 的语法是location [修饰符] /路径 { ... }修饰符决定了这条 location 的匹配方式。我用一个表格把四种修饰符和“无修饰符”的情况一次列清楚修饰符写法示例匹配规则使用场景location /api精确匹配URI 和路径完全一致才命中放健康检查、精确接口尽量少用^~location ^~ /static/前缀匹配命中后直接停止不再查正则静态资源目录防止被正则干扰~location ~ \.(png|jpg)$正则匹配区分大小写按配置顺序匹配按文件后缀、特定模式做处理~*location ~* \.(png|jpg)$正则匹配不区分大小写需要忽略大小写的静态资源场景无修饰符location /api/普通前缀匹配取最长匹配结果常规路径分发最常用精确匹配最严格比如location /api只有请求 URI 是/api时命中/api/user都不算。它适合用来做精准拦截像location /health给负载均衡器探活。^~属于“霸道”修饰符一旦最长前缀匹配到就直接定下来完全不理会后面有没有正则。这个很关键因为正则的优先级其实非常高如果你不想让某个前缀路径被正则“抢走”就得加^~。正则~和~*是按配置文件里出现的顺序来匹配的匹配到第一个就停所以正则的书写顺序有讲究。一般来说把更具体、更容易命中的正则放在前面否则前面的正则把请求抢走了后面的规则永远没有执行机会。无修饰符的普通前缀匹配最容易理解但也最容易踩坑因为它的匹配结果只是“保底”最后可能被正则覆盖掉。1.2 完整匹配顺序别被“最长前缀优先”骗了网上很多文章只说了“最长前缀优先”这个说法不完整。Nginx 官方的匹配顺序实际上是下面这四步先对所有精确匹配location /xxx做检查命中就直接用流程结束。再对所有^~前缀匹配做最长匹配如果命中就直接用流程结束不再看正则。然后按配置文件里的顺序依次尝试所有的正则 location~和~*找到第一个命中的就用。如果正则全都没命中才落到普通前缀匹配的结果取最长的那一个。这里最大的坑在于普通前缀匹配即使已经找到了最长结果也不会立即生效因为后面还要给正则让路。只有正则全部没命中普通前缀匹配的结果才会“上位”。举个例子配置文件里有这么几段location /images/ { # 普通前缀匹配 } location ^~ /static/ { # ^~ 前缀匹配 } location ~ \.(png|jpg|gif)$ { # 正则匹配 }请求/images/logo.png时普通前缀匹配先记住了/images/这个最长结果但接着正则\.(png|jpg|gif)$命中了所以最后生效的是正则那一条。如果你本意是让/images/目录下的文件都走普通前缀那条规则就得改成location ^~ /images/用^~挡掉正则。再比如请求/static/css/app.css^~ /static/直接命中后面的正则连尝试的机会都没有。搞清楚这个顺序后再去看网上那些“为什么我的 rewrite 没生效”“为什么这个 location 没命中”的问题基本都能自己找到答案。2. proxy_pass 的路径替换逻辑带不带斜杠完全是两码事location 只是决定了“谁来处理请求”真正把请求转给后端的是 proxy_pass。而 proxy_pass 后面到底写不写路径、怎么写决定了后端收到的 URI 长什么样。这一节的坑比 location 匹配规则还要多。2.1 一眼看懂带 URI 与不带 URI 的区别proxy_pass 的完整写法是proxy_pass http://后端地址;也可以写成proxy_pass http://后端地址/带路径;。关键区别就是proxy_pass 后面有没有带 URI路径部分。不带 URI 的写法location /api/ { proxy_pass http://127.0.0.1:8080; }请求/api/user?namexx转发到后端时URI 保持原样就是/api/user?namexx。Nginx 只负责换 IP 和端口路径一个字节都不改。带 URI 的写法location /api/ { proxy_pass http://127.0.0.1:8080/; }同样是请求/api/user?namexx转发到后端的 URI 会变成/user?namexx。也就是说location 匹配到的/api/这个前缀被 proxy_pass 里的/替换掉了参数部分不受影响。这个区别我用一张表总结一下方便你对照location 配置proxy_pass 配置请求 URI后端收到的 URIlocation /api/http://backend;/api/user/api/userlocation /api/http://backend/;/api/user/userlocation /http://backend;/user/userlocation /http://backend/;/user/user注意最后两行当 location 是/时带不带斜杠几乎没区别因为/替换/等于没变。所以很多人习惯在location /里写proxy_pass http://backend;也不会出问题。但一旦 location 带上了具体前缀带不带 URI 的效果就天差地别了。顺带提一句query string 在反向代理时默认会原样传递不需要额外处理。Nginx 内部有两个变量容易混淆$request_uri是客户端发来的完整原始 URI含参数$uri是经过 rewrite、内部跳转等处理后的规范化路径不含参数。调试的时候分清楚这两个变量能少走很多弯路。2.2 前缀裁剪的工作原理与正则 location 的坑带 URI 的 proxy_pass 本质上是一个“前缀替换”操作把请求 URI 中与 location 前缀匹配的部分替换成 proxy_pass 的 URI。举个例子location /api/ { proxy_pass http://backend/rest/; }请求/api/user后端收到的 URI 是/rest/user。这里就是把/api/替换成了/rest/。如果想让后端完全丢掉前缀就写成location /api/ { proxy_pass http://backend/; }请求/api/user变成/user。这也是前后端分离部署时最常用的写法。但是到了正则 location 里事情就没这么简单了。正则不是“前缀”没法简单替换所以 Nginx 对正则 location 里的 proxy_pass 有额外限制如果正则 location 里的 proxy_pass 带了 URI并且想用正则的匹配结果拼接必须用捕获组变量。常见的写法是这样location ~ ^/api/(.*)$ { proxy_pass http://backend/$1; }你想想^/api/(.*)$里的(.*)捕获了/api/后面的部分再用$1拼到 proxy_pass 的 URI 后面。比如请求/api/user$1是user后端收到/user。这种写法在正则 location 里是明确可靠的。我之前见过有人这么写location ~ ^/api/ { proxy_pass http://backend/; }这种写法在部分场景下结果可能和你预想的不同因为你没法准确描述“正则匹配到的部分”到底该替换成什么。正则匹配是一个模式判断和普通前缀的“截断”逻辑完全不一样很容易产生歧义。我的建议是生产环境别写这种不明确的组合要么正则 location 里 proxy_pass 不带 URI 原样透传要么老老实实用捕获组做拼接。另一个容易忽略的点location /api这种精确匹配属于“不带 URI 前缀”的 location它后面的 proxy_pass 如果带 URI行为又会不同。不过实际生产中我很少在精确匹配里再去做路径替换通常就是直接透传或者返回固定响应。3. 实战配置三组高频场景直接抄理论讲完了上实战。我整理了三组我实际部署里用过的配置覆盖了前后端分离、接口前缀裁剪、多服务分发和 WebSocket 转发这几种最常见的场景。3.1 前后端分离部署静态资源与 API 分离现在前后端分离太常见了前端打包出来是一堆静态文件后端是一组 API 服务。用 Nginx 做统一入口时我一般这样配server { listen 80; server_name example.com; # 前端静态资源 root /data/www/dist; index index.html; location / { try_files $uri $uri/ /index.html; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ { expires 7d; add_header Cache-Control public, max-age604800; try_files $uri 404; } # API 转发 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; } }这段配置有几个细节值得说第一try_files $uri $uri/ /index.html;是 SPA 项目Vue、React的标准写法。当请求路径不是实际存在的文件或目录时Nginx 会兜底返回/index.html这样前端路由就不会 404。我见过很多新手配完 SPA 后刷新子路由就 404多半是这个配置漏了。第二location /api/里我写的是proxy_pass http://127.0.0.1:8080/;注意末尾那个斜杠。这样后端收到的路径就没有/api前缀了。如果你的后端路由本身是带着/api的就把斜杠去掉写成不带 URI 的形式让路径原样透传。这里的判断标准只有一个后端接口实际接收什么路径你就让 Nginx 转成什么路径。第三proxy_set_header这几个请求头很重要。Host $host是把客户端访问的域名传给后端很多后端框架根据 Host 生成链接X-Real-IP和X-Forwarded-For是让后端能拿到真实客户端 IP否则应用里看到的全是 Nginx 的 IP。3.2 去掉接口前缀/api/xxx 转成 /xxx很多团队习惯前端统一请求/api/xxx但后端服务本身的路由是/xxx这时候就要把/api/前缀切掉。实现方式有两种一种是直接利用 proxy_pass 带 URI另一种是用 rewrite。第一种直接用带斜杠的 proxy_pass最简洁location /api/ { proxy_pass http://127.0.0.1:8080/; }请求/api/user/list→ 后端/user/list。注意location /api/的末尾有斜杠proxy_pass的末尾也有斜杠两个斜杠都别省。第二种用 rewrite 加不带 URI 的 proxy_passlocation /api/ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://127.0.0.1:8080; }rewrite ^/api/(.*)$ /$1 break;先把/api/前缀截掉然后proxy_pass不带 URI原样透传改写后的路径。这里break是关键它的作用是让改写后的 URI 在当前 location 内生效不重新走一遍 location 匹配。这两种方式我倾向于用第一种。原因很简单减少一层 rewrite 的解析开销配置更好读。只有在需要更复杂的路径改写比如加个时间戳、改多段路径时我才会用 rewrite 方案。这里还想提醒一个常见的连环坑如果两种方式一起用容易把路径搞重复。比如有人写了rewrite ^/api/(.*)$ /api/$1;proxy_pass 又带了/结果后端收到的是/api/api/xxx。排查这种问题时直接在 Nginx 里加一个临时 header 看实际路径比我干想来得快。3.3 多服务按路径分发与 WebSocket 转发有时候一个域名要同时对应多个后端服务比如按业务模块拆微服务这时候就需要按路径分发upstream order_service { server 192.168.1.10:8080 weight5; server 192.168.1.11:8080 weight5; keepalive 32; } upstream user_service { server 192.168.1.20:8081; } server { listen 80; server_name example.com; location /order/ { proxy_pass http://order_service/; proxy_set_header Host $host; proxy_http_version 1.1; proxy_set_header Connection ; } location /user/ { proxy_pass http://user_service/; proxy_set_header Host $host; } # WebSocket 转发 location /ws/ { proxy_pass http://192.168.1.30:9501/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 60s; } }Upstream 这里我加了一个小优化keepalive 32加上proxy_http_version 1.1; proxy_set_header Connection ;可以让 Nginx 到上游的 HTTP 连接复用减少握手开销。在高并发场景下这个配置能显著降低请求延迟和系统资源消耗。不过要注意keepalive 配置必须配合清空 Connection 头的proxy_set_header Connection ;否则连接复用不起来。WebSocket 转发的核心就是Upgrade和Connection两个头。$http_upgrade是客户端请求的 Upgrade 值当客户端发起 WebSocket 握手时Nginx 会原样把它带给后端并显式声明Connection upgrade这样 TCP 连接才能升级成 WebSocket。proxy_read_timeout 60s;是防止连接长时间没消息被 Nginx 断开实际值可以根据业务心跳包频率调整。4. 故障排查实录配置出问题怎么定位配置写完了测试的时候问题就来了。我在排查 Nginx 转发问题时总结了一套流程能在几分钟内定位大部分故障原因。4.1 经典报错404、502、路径多了一段把最常遇到的几个现象列成一张速查表方便你按图索骥现象可能原因处理方法后端收到 404proxy_pass 带 URI 把路径裁剪了和后端路由对不上去掉 proxy_pass 的 URI或调整后端路由后端收到 502后端服务没启动、端口没监听、连接被拒检查后端进程netstat确认端口监听情况路径多了一段/api/apirewrite 和 proxy_pass 都改了路径只保留一种路径改写方案刷新页面 404SPA 项目没有配置try_files兜底加上try_files $uri $uri/ /index.html;重定向后地址不对后端返回的 Location 头被 Nginx 改写用proxy_redirect修正静态资源不缓存没配置 expires 或 Cache-Control在静态资源 location 里加缓存头反代后域名不对没设置proxy_set_header Host $host增加 Host 转发配置404 和 502 是最常见的两类。404 不需要慌先想想是 Nginx 找不到静态文件还是后端返回的 404。如果是静态文件检查 root 路径和 try_files如果是后端看路径转换对不对。502 则大概率是后端服务本身没起来或者 Nginx 和上游之间的网络不通先去curl http://127.0.0.1:8080/自测一下通常就能定位。4.2 定位 location 命中的几个方法我调试 location 匹配时最常用的方法是临时往配置里塞一个响应头看看实际命中的到底是哪条规则。像这样location /api/ { add_header X-Debug-Location api-location; proxy_pass http://127.0.0.1:8080/; }然后用 curl 请求看响应头curl -I http://localhost/api/user如果响应里出现X-Debug-Location: api-location说明确实命中了/api/这条规则。这个方法在匹配规则复杂时特别管用比你盯着配置猜半天效率高多了。如果临时环境不方便改配置也可以看 Nginx 的 access log。我一般会在 log_format 里加上$upstream_addr $upstream_response_time这样能直接看出请求转到了哪个上游地址、耗时多少。error log 则重点关注以下几类信息connect() failed (111: Connection refused) while connecting to upstream→ 后端端口没监听no resolver defined to resolve→ proxy_pass 里用了域名/变量但没配 resolverupstream timed out→ 连接或响应超时需要调大超时时间改配置前一定养成好习惯先跑一遍nginx -t检查语法。我用这个命令拦住过太多低级错误什么少分号、变量拼错、正则括号不匹配都能在 reload 之前暴露出来。语法没问题再nginx -s reload平滑生效不影响线上服务。5. 这些细节也容易被忽略最后补几个不算高频、但遇到了就让人头疼的细节问题。5.1 变量传后端服务时的隐性坑有时候后端地址是动态的比如根据不同条件转发到不同的服务这时候会在 location 里用变量set $backend http://192.168.1.10:8080; location /api/ { proxy_pass http://$backend; }这里有个容易被忽略的点proxy_pass 里使用变量后如果写的是域名Nginx 不会在启动时解析而是依赖运行时 resolver 去解析。所以如果你在变量里写的是域名就必须要配 resolver 指令否则启动虽然不报错请求时会提示找不到域名。另一个坑是proxy_pass http://$backend/;这种写法带 URI 又带变量在正则 location 里是直接报错的。Nginx 在 reload 时会提示proxy_pass URL cannot have URI part in location given by regular expression。所以在正则 location 里用变量做动态转发只能写不带 URI 的形式。同时要注意变量模式下$1之类捕获组的拼接往往失效别把 rewrite 的捕获想当然往 proxy_pass 里塞。5.2 上游超时、HTTPS 和响应头处理调优层面有几个参数虽然不是 location 和 proxy_pass 的直接主题但既然是配置 proxy_pass就得经常打交道。超时时间三个经常会调proxy_connect_timeout是连接上游的超时默认 60 秒proxy_read_timeout是等待响应的超时默认也是 60 秒proxy_send_timeout是发送请求给上游的超时。如果业务接口本身要跑很久不调大这两个超时很容易出现 “upstream timed out”。反过来如果你想快速失败并重试也可以把 connect 超时调小比如 3 秒。如果你的上游是 HTTPS 的地址比如proxy_pass https://backend;通常要补上这几个配置proxy_ssl_server_name on; proxy_ssl_name $host; proxy_ssl_verify off;proxy_ssl_server_name用于 SNI让后端知道客户端请求的是哪个域名否则后端证书校验可能过不去。proxy_ssl_name是用于证书校验的域名默认是上游 IP手动设成$host才能真正匹配证书。如果内网环境没有正规证书proxy_ssl_verify off能跳过证书校验但生产环境不要轻易关。响应头方面如果后端响应的 Header 里有敏感信息不想暴露给客户端可以用proxy_hide_header X-Powered-By;。反之如果想强制加一个响应头用add_header。注意add_header在location /和location /api/里是分开继承的不在同一个层级会出现加了等于没加的情况。我自己调试 Nginx 配置时最大的体悟是把 location 和 proxy_pass 当成一个“路径转换公式”而不是“一段神奇的转发命令”。先算清楚客户端请求进来时的 URI 经过 location 匹配剩什么再算 proxy_pass 带不带 URI 会替换什么最后再确认后端实际接收什么路径三步走完基本不会出错。再配合临时 add_header 和 access log 验证绝大多数配置问题都能在几分钟内水落石出。
