最近又在群里看到有人问Nginx报404,前端接口全部打不到后端,排查了半天发现是location和proxy_pass组合的时候路径拼接出了问题。这种问题我见过太多次了,尤其是刚接触Nginx反代的同学,经常在proxy_pass末尾的斜杠、location的匹配优先级上栽跟头。今天我把这两个配置项彻底拆开讲清楚,从匹配规则到拼接逻辑,再到高频故障排查,一次性说透。
这篇内容适合谁?不管是刚入门,还是在用Nginx做网关、反代、静态资源服务的同学,只要你想搞明白“一个请求进来到底去了哪、后端看到的URL是什么”,这篇文章都值得看完。我会结合实际案例和踩坑记录来写,尽量不堆教科书式的东西。
1. location 匹配规则,先把地基打牢
1.1 六种location写法,别傻傻分不清
location是Nginx里用来匹配请求URI的核心指令,写法其实不止一种。我先把常见写法列出来,后面再逐个拆。
| 写法 | 示例 | 匹配特征 |
|---|---|---|
| 精确匹配 | location = /login {} | 完全相等才命中 |
| 普通前缀匹配 | location /api {} | 以 /api 开头即命中 |
| 区分大小写正则 | location ~ \.php$ {} | 正则匹配,区分大小写 |
| 忽略大小写正则 | location ~* \.(jpg|png)$ {} | 正则匹配,忽略大小写 |
| 优先前缀匹配 | location ^~ /static/ {} | 命中后不再查正则 |
| 命名location | location @fallback {} | 不直接接收外部请求,用于内部跳转 |
这里最关键的是要理解:location /并不等于“根路径”,它是一个前缀匹配,所有以/开头的请求都会命中最少一次。很多人写location /是当兜底用的,但它其实是所有请求的“保底分支”,并不是只有访问根域才会走它。
1.2 匹配顺序和优先级,面试必考、线上必踩
Nginx的location匹配不是从上到下按顺序命中的,它有自己的一套优先级逻辑。我用自己的话概括一下:
- 先做所有“普通前缀匹配”(包括
=、^~、普通前缀),记录下最长的那一个前缀作为候选。 - 如果这个最长的前缀是
^~类型,直接使用,不再检查正则。 - 如果没有
^~,就按配置文件中正则出现的顺序,依次检查正则;一旦某个正则匹配成功,立刻使用,后面的正则不再看。 - 如果所有正则都不匹配,就使用第1步记录的最长前缀。
举一个实际例子。假设配置文件里有这么几段:
location = /login { return 200 "exact login"; } location ^~ /static/ { return 200 "static prefix"; } location ~* \.(png|jpg|css)$ { return 200 "regex static"; } location /api { return 200 "api prefix"; } location / { return 200 "fallback"; }请求路径来了,结果是:
| 请求URI | 命中的location | 原因 |
|---|---|---|
/login | = /login | 精确匹配最高优先级,直接结束 |
/static/img/a.png | ^~ /static/ | 虽然正则png也能匹配,但^~让正则检查直接跳过 |
/images/a.png | ~* \.(png|jpg|css)$ | 正则优先于普通前缀/ |
/api/user | /api | 最长前缀比/更长,且没有正则可以命中 |
/other | / | 没有任何更具体的匹配,兜底 |
这个优先级表建议刻在脑子里。线上最容易出的问题是:正则排在前面,把预期的前缀请求“截胡”了;或者^~没加,导致静态资源被正则匹配到了别的后端。
1.3 几个容易混淆的细节
location /api和location /api/是有区别的。/api能匹配/api本身,也能匹配/api/user;而/api/要求路径里必须带/,所以/api这个URI它匹配不到。如果你指望/api和/api/都能走同一个后端,建议写location /api,不要写location /api/。
再提醒一点:正则location之间的顺序是敏感的,Nginx按出现顺序匹配,先写先得。普通前缀之间则不存在顺序问题,它永远选最长匹配。所以当你在一个配置文件里同时写多个正则location,务必把更精确的规则放前面。
提示:
location的匹配目标是不含域名、不含查询参数的URI部分。比如请求http://example.com/api/user?id=1,匹配用的是/api/user,?id=1不参与匹配。
2. proxy_pass 拼接逻辑,最关键也最容易翻车
2.1 带不带URI,结果天差地别
proxy_pass是反向代理的核心指令,但它的一个隐藏行为坑了无数人:proxy_pass后面是否带URI(哪怕只是一条斜杠),决定了Nginx转发给上游的路径是谁。
我先把规则说出来,再举例子。规则只有两条:
proxy_pass后面不带URI,只写到host:port,Nginx会把客户端请求的原始URI完整透传给上游。proxy_pass后面带了URI,无论是一个/还是/v2,Nginx会把客户端URI中匹配location的那一段前缀替换成proxy_pass里的URI,然后拼出最终转发路径。
这里有个容易误判的点:很多人以为http://backend:8080和http://backend:8080/是一样的,实际上完全不一样。前者是“无URI透传”,后者是“带/的URI替换”。
2.2 一条条拆解拼接实验
我直接做一组对照实验,假设所有请求都是/api/user,location统一写/api/。
| proxy_pass写法 | 上游实际收到的URI |
|---|---|
proxy_pass http://backend:8080; | /api/user |
proxy_pass http://backend:8080/; | /user |
proxy_pass http://backend:8080/v2/; | /v2/user |
proxy_pass http://backend:8080/v2; | /v2user,注意这里没有斜杠,拼接会非常怪 |
前三种都很直观。最后一种http://backend:8080/v2没有末尾斜杠,会直接替换成/v2,再拼上剩余的/user,结果变成/v2user,基本上一定404。所以写带URI的proxy_pass时,末尾该加斜杠就加,别偷懒。
再看一个更常见的组合。如果location写成location /api(不带末尾斜杠),请求/api/user,proxy_pass http://backend:8080/;,那么被替换的前缀是/api,最终上游路径是/user。但如果请求的是/apixxx,它也命中了/api前缀,替换后变成/xxx,这种“边界模糊”就是不带末尾斜杠的隐患。
2.3 正则location里不能带URI
这里有一个硬性规定:在正则location(~或~*)中,proxy_pass不能带URI部分,否则Nginx会直接报错,提示:
proxy_pass cannot have URI part in location given by regular expression我自己第一次看到这个报错也懵了一下。原因是正则匹配的URI不固定,Nginx无法判断该替换哪一段,所以干脆禁止带URI。如果你的需求是“正则匹配后还要改路径”,可以借助rewrite完成,后面我会单独讲。
注意:
proxy_pass http://backend:8080;和proxy_pass http://backend:8080/;在书写时肉眼很难分辨,建议每写一条都刻意检查一下末尾斜杠,尤其是在location里,这是线上404的重灾区。
3. 高频实战场景:多个路径转发、参数限定、rewrite联动
3.1 多个路径一并转发到同一个后端
“Nginx location 多个路径一并转发”这个问题很常见。比如你有/api、/auth、/pay三条路径都要转发到同一个网关服务,不必写三个重复的location,用正则一条搞定:
location ~ ^/(api|auth|pay)/ { proxy_pass http://gateway; proxy_set_header Host $host; }这种写法简洁,但注意proxy_pass不能带URI,因为是正则location。如果后端网关希望去掉前缀,比如把/api/user转成/user给上游,可以加一条rewrite:
location ~ ^/(api|auth|pay)/ { rewrite ^/(api|auth|pay)/(.*)$ /$2 break; proxy_pass http://gateway; }这里rewrite后面的break很关键。如果用last,rewrite后的URI会重新进入location匹配,很可能又匹配回这个正则,造成死循环。break则是在当前location内直接完成改写,不再重新匹配,然后交给proxy_pass转发,正好符合我们的需求。
如果多个路径对应的是不同后端,那就老老实实拆开写多个location,每个里面各自proxy_pass。比如/api走订单服务,/auth走认证服务,拆开更清晰,也方便单独加限流、超时配置。
3.2 只转发带参数的URL,怎么限制
热搜里有“nginx限制只转发带参数的url”,我实际也遇到过类似需求:某些搜索或查询接口,必须带参数才允许转发,不带参数直接拒绝或者走另一个逻辑。
Nginx里可以用if配合内置变量$args判断。$args就是原始请求的查询参数串,没有参数时它是空字符串。
location /search/ { if ($args = "") { return 400; } proxy_pass http://backend; }这样不带参数的/search/请求会直接返回400,压根不会到达后端。如果你不想拒绝,而是想转发到另一个服务,也可以这样:
location /search/ { if ($args != "") { proxy_pass http://backend-with-param; break; } proxy_pass http://backend-default; }注意proxy_pass后面不能直接跟$args这类变量来拼URL?其实Nginx是支持在proxy_pass里使用变量的,但变量方式会带来DNS解析时机变化,容易搞出性能问题,一般不推荐。对于“带参不带参走不同后端”这种需求,if够用,但if里别搞复杂逻辑,只做简单的判断和跳转就好。
3.3 rewrite与proxy_pass的组合拳
rewrite和proxy_pass是配合最频繁的一对。除了上面提到的“去掉前缀”,还有一个典型场景是前端路径和后端路径不一样。
比如前端访问/api/user,后端接口实际是/user。你可以选择两种方案。
方案一,用rewrite改路径:
location /api/ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://backend; }方案二,用带URI的proxy_pass:
location /api/ { proxy_pass http://backend/; }两个方案效果一样,后端收到的都是/user。区别在于,方案一更灵活,可以写复杂的正则改写;方案二简单直接,但只能做固定前缀替换。
我在实际项目里的经验是:一个location里尽量只用一个机制。要么用rewrite改路径,要么用proxy_pass的URI替换,两者混用容易把自己绕晕。如果确实要混用,务必用curl实测最终上游路径,别靠猜。
3.4 静态资源与接口转发拆分
除了接口转发,静态资源的处理也是一个高频场景。如果项目里静态资源由Nginx直接磁盘服务,可以这样:
location /static/ { alias /data/www/static/; expires 7d; access_log off; }如果静态资源放在另一个文件服务或CDN上,就转发过去:
location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ { proxy_pass http://cdn-server; }这里用正则匹配文件后缀,注意proxy_pass不能带URI,因为正则是不能带URI的。更稳妥的写法是把静态资源目录用^~前缀匹配,避免正则参与:
location ^~ /assets/ { proxy_pass http://cdn-server; expires 30d; }^~的好处是命中后直接跳过正则检查,性能和可预测性都好一些。这是我在生产环境比较推荐的做法。
4. 高频报错与排查手册
4.1 常见故障速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 502 Bad Gateway | 上游服务没启动、端口错误、防火墙拦截 | 直接curl上游地址;检查upstream配置 |
| 504 Gateway Timeout | 上游处理慢,proxy_read_timeout过短 | 调大超时;检查后端性能 |
| 404 Not Found | proxy_pass带URI导致路径拼接不对;rewrite改写后路径不对 | 看日志里上游实际URI;调整斜杠 |
| 400 Bad Request | 请求头过大;proxy_pass变量用法不对 | 查error.log;检查配置 |
| 301/302循环 | 后端根据Host判断跳转,Host头不对 | 设置proxy_set_header Host $host;处理proxy_redirect |
| 静态资源404 | root与alias路径错误 | 区分root和alias拼接规则 |
这里额外说一下root和alias的区别。root会把当前location的URI追加到路径后面,比如location /static/ { root /data/www; },实际访问的是/data/www/static/a.css;alias则是直接用路径替换location前缀,比如location /static/ { alias /data/www/static/; },访问/static/a.css实际是/data/www/static/a.css。很多人混淆这两者导致404,建议记牢。
4.2 一套有效的排查思路
遇到Nginx反代问题时,我一般按下面的顺序来:
- 先看客户端请求。浏览器Network面板里确认实际请求的完整URL。
- 看Nginx的access.log,确认请求有没有进入Nginx,进入后命中了哪个server和location。日志级别调到
notice会输出更详细的内部跳转信息。 - 看上游服务日志,确认请求有没有到达后端。如果到达了,对比后端收到的URI和前端请求的URI。
- 在后端服务器上手动curl先测上游地址通不通。比如后端是
192.168.1.10:8080,直接在Nginx服务器上执行curl http://192.168.1.10:8080/health,判断基础连通性。 - 临时在location里加一个调试响应头,快速确认命中分支:
location /api/ { add_header X-Debug-Location $request_uri always; proxy_pass http://backend/; }请求后看响应头里X-Debug-Location的值,就能判断是不是这个location在处理。
4.3 反向代理必配的Header,少了就出诡异问题
反代场景下,后端拿到的请求来源可能是Nginx,而不是真实客户端。如果后端要做用户IP白名单、审计、生成绝对链接,就必须让Nginx传递真实的客户端信息。下面这组Header我几乎在每个反代配置里都会写:
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变量如果处理不当,容易出现“后端跳转到内网地址”、或者“循环重定向”的坑。X-Forwarded-Proto在Nginx后面挂了HTTPS证书时尤其重要,后端需要知道“客户端是通过HTTPS来的”才能生成正确的https://链接。
如果是WebSocket转发,还需要额外加两行:
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";否则WebSocket握手会失败,表现为“连接已断开”。
5. 性能与可维护性方面的优化
5.1 upstream与keepalive,减少握手开销
当一个location要转发到一组后端时,用upstream比直接写IP更规范,还能做负载均衡:
upstream backend { server 192.168.1.10:8080 weight=3 max_fails=2 fail_timeout=10s; server 192.168.1.11:8080 backup; keepalive 32; } location /api/ { proxy_http_version 1.1; proxy_set_header Connection ""; proxy_pass http://backend; }这里有个细节:keepalive要生效,proxy_http_version必须设为1.1,并且要把Connection头清空。否则Nginx到上游还是短连接,每次请求都要重新握手,在高并发下性能差距非常明显。我在压测里见过,加上keepalive之后QPS能提升不少。
5.2 超时参数要按业务调,不能全用默认值
Nginx反代的默认超时参数有时候不匹配业务场景。比如上传大文件,或者后端某个接口本身就跑得比较慢,如果proxy_read_timeout设置过短,就会出现“明明后端在处理,Nginx却返回504”的情况。
调整示例:
location /upload/ { proxy_connect_timeout 5s; proxy_send_timeout 30s; proxy_read_timeout 60s; proxy_pass http://upload-server; }proxy_connect_timeout控制Nginx与上游建立连接的超时,proxy_send_timeout控制发送请求体的超时,proxy_read_timeout控制等待上游响应的超时。一般按业务接口的实际耗时去调,别一次性调得过大,否则会把后端故障拖成Nginx大量堆积请求。
5.3 配置分层,别把所有location堆在一个文件里
项目规模一大,一个server块里堆几十个location是常事,后期改起来非常痛苦。我现在习惯用include拆分配置文件:
# /etc/nginx/conf.d/example.com.conf server { listen 80; server_name example.com; include /etc/nginx/locations/api.conf; include /etc/nginx/locations/static.conf; include /etc/nginx/locations/auth.conf; }每个location文件只管自己的规则,互不干扰。改动其中一个,nginx -t校验通过后单独reload,风险范围小很多。另外,相同的Header设置可以抽到server层,减少重复。
还有一个小技巧:如果要给一组location共用同一个代理配置,可以先把公共的都写在server块里,location里只保留差异项。Nginx的配置继承机制会帮你省掉很多重复代码。
结尾:一点个人经验
我这两年处理过的Nginx相关问题里,至少有三成最后都落在location优先级和proxy_pass拼接规则上。这类问题理论上不难,但出问题时很容易被前端的报错误导,绕远路。
我现在写任何一段转发配置,都会先在脑子里过两件事:这个请求进来会命中哪个location?转发出去上游看到的URI是什么?想清楚这两点,绝大多数404、502问题都能在配置阶段被拦住。另外养成一个习惯:每次改完配置先nginx -t,然后nginx -s reload,别用restart。我有一次在线上图省事直接restart,把几千个长连接全断了,教训惨痛。
最后再分享一个实用小技巧:遇到路径问题,不用抓瞎,直接在Nginx里临时配一行add_header X-Backend-URI $upstream_uri always;,然后在浏览器或curl里看响应头,后端实际收到的URI一目了然。这个办法我用了很久,排查效率比翻日志高得多。