尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux Nginx Nginx location 匹配顺序混乱怎么排查修复

Linux Nginx Nginx location 匹配顺序混乱怎么排查修复 前言location匹配顺序出问题症状往往是这样明明在配置文件里先写了location /api/ {}请求却落到了后面那个location ~ \.json$ {}上给静态目录加了location ^~ /static/也没用图片请求还是走了带正则的那个块返回 404把两个location的顺序调换一下本以为能修好结果问题换了个地方出现。根因在于Nginx 的location匹配不是「按配置书写顺序从上到下」这一条简单规则而是两组规则混在一起。前缀匹配/path、^~、走的是「最长者优先」而正则匹配~、~*走的是「配置中出现顺序优先第一个命中即停止」。这两套规则叠加再加上rewrite ... last、try_files、index、error_page这些会触发内部重定向internal redirect并让 location 匹配重跑一遍的机制「顺序混乱」几乎是必然的。本文先讲清匹配算法再讲内部重定向这个第二层干扰最后给出能真正定位问题的排查手段debug 级别的error_log会直接打印它逐个试了哪些 location。示例基于nginx 1.24RHEL 9 / Ubuntu 22.04 官方包配置目录/etc/nginx/。一、匹配算法两组规则两种胜负判定收到一个请求后Nginx 按下面的顺序决定用哪个location精确匹配把请求的 URI 与所有location xxx的值做完整字符串相等比较。相等就直接用它整个过程结束。找最长前缀在所有的普通前缀location /path和^~前缀location ^~ /path中找出前缀字符串最长的那一个先记下来不立即生效。看最长前缀是不是^~如果这个最长前缀带着^~就直接用它不再检查任何正则。按顺序试正则否则按这些正则location在配置文件中出现的先后顺序逐个尝试~区分大小写和~*不区分大小写第一个匹配上的就选中后面的不再看。都没有命中如果所有正则都不匹配就用第 2 步记下的那个最长前缀 location。嵌套选中某个前缀 location 之后还会在这个 location 内部继续按同样的规则查找嵌套的 location。把胜负判定的差异列出来location 类型匹配对象胜负判定依据 /exact完整 URI 相等最先检查命中即结束^~ /prefixURI 前缀在「最长前缀」竞争中胜出时直接生效/prefix普通URI 前缀只比长度最长者优先与书写顺序无关~ regex正则按配置中的出现顺序第一个命中即停止~* regex正则忽略大小写同~与~混在同一个顺序序列里named不直接匹配请求只能被try_files/error_page等内部跳转引用两条最容易记错的结论普通前缀 location 之间比的是长度不是书写顺序。把location /api {}写在location /api/v2 {}前面请求/api/v2/x依然会命中后者。正则 location 之间比的是出现顺序不是长度。location ~ ^/a写在location ~ ^/api/v2/前面请求/api/v2/x会命中第一个因为^/a也匹配更长的那个正则根本没机会。第 3 步的^~是唯一能「从前缀匹配这一侧强行终结竞争、跳过全部正则」的手段。当你发现某个前缀 location 总是被一个正则抢走时加^~才是正解而不是去调整配置的书写顺序。二、第二层干扰内部重定向让匹配重跑location匹配在下面这些情况下会重新执行一遍而这是「改了顺序还是乱」的真正原因触发方式说明index指令请求/时index index.html触发跳转到/index.html重新匹配 locationtry_files的最后一项try_files $uri $uri/ /index.php中最后那个 URI 是内部重定向目标error_pageerror_page 404 /404.html会把请求转到/404.html重新匹配rewrite ... last改写后回到 location 匹配阶段从头开始rewrite ... break不重新匹配停在当前 location 继续处理后续指令所以「我配了location ~ \.php$为什么 404 页面进去后报的错是静态目录的」这类问题答案几乎都是内部重定向把请求换了一个 URI然后落到了另一个 location。内部重定向还有一个后果是循环。如果location ~ \.php$里也写了try_files $uri 404而location /里写的是try_files $uri $uri/ /index.php那么当/index.php这个文件不存在时请求会被反复内部重定向到/index.phpNginx 直接报错rewrite or internal redirection cycle while internally redirecting to /index.php这条错误信息本身就说明了问题在重定向环上不在匹配顺序上修法是从循环里去掉一个环节比如给 PHP 那个 location 加try_files $uri 404或者确认index.php文件真的存在。三、排查手段让 Nginx 自己告诉你选了哪个 location手段一debug 级别的 error_log最直接Nginx 在 debug 日志里会逐条打印它尝试过的 location并以using configuration结尾指出最终选中的那一个。这就是「顺序混乱」问题的决定性证据。前提是二进制编译时带了--with-debug先确认nginx -V 21 | tr \n | grep -- --with-debug没有输出就说明不支持。Debian/Ubuntu 可以直接装nginx-debug包提供单独的/usr/sbin/nginx-debug二进制RHEL 系一般需要自行编译带--with-debug的版本或者用官方源里对应的 debug 包以发行版提供的包为准。开启方式只在临时排查时开# 只对有问题的那一个 server 开避免全站日志爆量 server { listen 80; server_name www.example.com; error_log /var/log/nginx/debug.log debug; # ... }sudo nginx -t sudo systemctl reload nginx curl -sS -o /dev/null -H Host: www.example.com http://127.0.0.1/some/path sudo tail -100 /var/log/nginx/debug.log日志里能看到两类行一串test location:开头的候选列表最后一行是using configuration加被选中的 location 值。看到test location的行数和顺序就知道 Nginx 到底试了哪些、在哪个停下来。排查完务必把debug改回warn或error并 reload。debug 级别下每一个请求都会产生大量日志磁盘很快会被写满把故障从「配置问题」升级成「磁盘满」。手段二给每个 location 打标记不想动 log 级别时用一个临时响应头确认命中的是哪个块location ^~ /static/ { add_header X-Loc static-prefix always; root /var/www/site; } location ~ \.json$ { add_header X-Loc json-regex always; default_type application/json; return 200 {ok:true}; }curl -sSI http://127.0.0.1/static/a.json | grep -i x-loc要点必须带always参数。不带always时add_header只在 200/201/204/206/301/302/303/304/307/308 这些响应码上发送404 情况下这个头根本不会出现会让人误判成「没命中任何块」。还有一点add_header不具备累加式的继承——某个 location 里只要出现了一条add_header父级的所有add_header在它这里就全部失效。所以做标记时不要指望server块的标记头能在所有 location 上都出现。手段三nginx -T看真正被加载的配置include会把配置拆到多个文件光看你编辑的那个文件往往看不出真实的匹配顺序sudo nginx -T | grep -n -E ^\s*(server|location)\s | head -40 sudo nginx -T /tmp/full.conf # 导出后慢慢比对-T会把 include 全部展开后打印是判断「实际生效的顺序」的唯一可靠依据。手段四rewrite_log on如果怀疑是rewrite与last/break造成的跳转在server块里加rewrite_log on;它会把 rewrite 过程按 notice 级别写进 error_log能看到「哪条 rewrite 规则被应用、改写成了什么」。排查完记得关掉。四、实战把一份含糊的配置改成确定的下面这份配置的匹配结果是含糊的正则写在前面会抢走后面的前缀请求server { listen 80; server_name www.example.com; root /var/www/site; # ❌ 有问题这条正则会抢走 /static/data.json location ~ \.json$ { add_header Content-Type application/json; return 200 {}; } location /static/ { expires 7d; } location /api/ { proxy_pass http://backend_api; } location / { try_files $uri $uri/ 404; } }问题在于请求/static/data.json时最长前缀是/static/但它是普通前缀所以算法会继续按顺序试正则\.json$匹配成功于是请求被 JSON 那个块接管/static/的expires完全没起作用。改法是让意图变得显式且无歧义server { listen 80; server_name www.example.com; root /var/www/site; # ✅ 用 处理必须精确相等的路径 location /healthz { access_log off; return 200 ok; } # ✅ 用 ^~ 声明「静态目录优先于一切正则」 location ^~ /static/ { expires 7d; add_header Cache-Control public; } # ✅ ^~ 同一效果图片目录不被 .json 正则抢走 location ^~ /assets/ { expires 30d; } # ✅ 正则放在最后统一收口且尽量写具体避免 ^ 开头过短 location ~* \.(gif|jpg|jpeg|png|webp|css|js)$ { expires 7d; add_header Cache-Control public; } location /api/ { proxy_pass http://backend_api; 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; } # ✅ 兜底放最后语义是「以上都不匹配时用它」 location / { try_files $uri $uri/ 404; } }验证用的命令基于 nginx 1.24sudo nginx -t sudo nginx -T | grep -n -E location | head -20 sudo systemctl reload nginx for p in / /healthz /static/a.json /assets/b.css /api/v1/user /no-such; do printf %-20s - %s\n $p \ $(curl -sS -o /dev/null -w %{http_code} -H Host: www.example.com http://127.0.0.1$p) done这套规则下匹配结果是可预测的/healthz走精确匹配/static/a.json和/assets/b.css因为^~直接终结竞争、不会被.json正则抢走/api/v1/user走代理其余交给兜底。常见坑点❌ 以为「location 按配置书写顺序从上到下匹配先写的赢」✅ 前缀 location 之间是最长优先与书写顺序无关只有正则 location 才是按出现顺序 原因两套胜负判定混在一起调换书写顺序只会让「谁被谁抢走」换一个地方出现。前缀想赢过正则唯一手段是加^~或改成。❌location ~ ^/a和location ~ ^/api/v2/同时存在以为更长的正则会赢✅ 正则之间按出现顺序取第一个命中的与正则表达式的长度无关 原因^/a也匹配/api/v2/x所以只要它写在前面/api/v2/那条就永远不会被用到。正则要么写得足够具体带$或用^/api/v2/并用精确化要么把顺序摆对。❌location /api本意是「只处理 /api 这个路径」结果/apixyz也被它接管了✅ 前缀匹配是纯字符串前缀不认路径分段要限定路径段用location /api/要精确相等用location /api原因/api是location /apixyz的前缀也是/api/v1的前缀。这类问题在同时存在/api和/apiv2时特别隐蔽表现是「某个接口偶尔返回了错误的后端响应」。❌ 在location /a/ { location /a/b/ { ... } }里写嵌套 location内层永远不生效✅ 嵌套前缀 location 的匹配对象是父前缀之后剩余的那一段 URI内层应写location /b/原因Nginx 在已匹配的父前缀 location 内部继续查找时使用的是去掉父前缀后的相对部分。写完整路径会永远匹配不上而且不报错。嵌套正则的语义与嵌套前缀不同遇到不确定的情况直接用 debug 日志确认。❌ 给location ~ \.php$配了try_files结果出现rewrite or internal redirection cycle✅ 检查try_files的最后一项是否又被同一个 location 接管index和try_files触发的是内部重定向会重新匹配 location 原因内部重定向会重跑 location 匹配如果 A 里的 fallback 指向 B、B 里的 fallback 又指向 B 自己或指回 A就形成环。报错信息里已经点名了被循环重定向的那个 URI从它反查所有会生成它的地方最快。❌ 排查时加调试响应头不带always404 时发现没有这个头误判成「请求没进这个 location」✅ 调试头一律写add_header X-Loc ... always;原因不带always时add_header只在少数几个 2xx/3xx 响应码上发送。404 是最常见的排查场景恰好是它不发送的那一类很容易得出完全错误的结论。❌ 在location ~正则里用alias但没写捕获组例如location ~ ^/img/ { alias /data/; }✅ 正则 location 里用捕获组形式location ~ ^/img/(.*)$ { alias /data/$1; }原因正则 location 中alias的替换逻辑依赖捕获没有捕获组时路径拼接结果不符合预期。「请求进来返回 404但文件确实存在」这类现象八成要先检查alias与root的用法是否匹配 location 的类型。❌ 靠nginx -t通过来判断 location 顺序对不对✅ 用nginx -T看展开后的真实顺序用 debug 日志或调试响应头确认真实命中的块 原因-t只检查语法和文件是否存在它对「哪个 location 会赢」完全不做判断。顺序类问题在没有请求跑过之前靠肉眼读配置很容易读错。总结想达到的效果写法关键点只匹配某一个确切路径location /path完整相等最先判定命中即结束某前缀要先于所有正则location ^~ /path在最长前缀竞争中胜出后直接生效跳过全部正则普通前缀兜底location /长度竞争中最短只有全都没命中时才用到按扩展名统一处理location ~* \.(png\jpg)$命名跳转目标location fallback不直接匹配请求只被try_files/error_page引用确认实际命中了哪个error_log ... debugusing configuration需二进制带--with-debug用完立刻改回确认实际加载的配置nginx -Tinclude 全部展开是唯一可信的顺序视图追踪 rewrite 过程rewrite_log on;notice 级别写入 error_log排查完关闭location匹配顺序并不混乱它有一条明确的算法先看再找最长前缀^~时终结否则按出现顺序试第一个命中的正则最后回落到最长前缀。所谓「混乱」通常是两套胜负判定被当成一套、或者忽略了内部重定向会让匹配重跑。排查时不要靠调换书写顺序去试直接开 debug 日志看using configuration指向谁一次就能定位。查完记得关掉 debug 日志级别。
返回列表