
做过Nginx排障的人应该都有这种体验明明配置看起来没毛病请求却总是走到意料之外的location里去。我之前就遇到过线上接口突然全部404的情况后端日志干干净净Nginx日志翻了半天才发现是新增的那条正则location把请求半路截走了。所以今天这篇我想把Nginx路由匹配规则和url转发这堆事一次讲透——不光是罗列配置项更重要的是告诉你当流量进来那一刻Nginx到底是怎么给请求找“归宿”的。搞懂这套机制你就能明白网上那些location示例为什么有的加斜杠、有的不加也就能真正看懂别人的反代配置而不是靠复制粘贴碰运气。这篇文章适合刚接手Nginx配置的运维、后端开发也适合那些已经把Nginx跑起来但始终没搞懂转发逻辑的同学。内容会从location匹配优先级讲起拆解proxy_pass转发时的URI替换规则再用三个实际场景带你配出一套可用的url转发服务最后附上一份我这些年积累的问题排查清单。每一步都包含配置原文和“为什么这么配”的说明照着做就能落地。1. 路由匹配规则拆解Nginx到底是怎么选中location的1.1 五种location匹配方式先明确一件事Nginx里的location不是一个简单的“字符串相等”判断它有自己的一套匹配体系。网上很多配置出错根源就是没分清这几种匹配方式的区别。location后面可以跟这么几种修饰符修饰符写法示例匹配逻辑典型用途location /login精确匹配请求URI必须完全等于/login登录页、固定入口命中率最高无修饰符location /api/前缀匹配请求URI以/api/开头按路径划分业务模块^~location ^~ /static/前缀匹配命中后不再检查后面的正则静态资源目录避免正则干扰~location ~ \.(php|php5)$正则匹配区分大小写动态脚本、接口路由~*location ~* \.(jpg|png)$正则匹配不区分大小写图片、静态文件后缀另外还有一个name形式的命名location它不直接参与外部请求匹配主要配合try_files做内部跳转。这个后面讲try_files时会用到。先记住一句话普通前缀匹配记录“最长匹配”正则匹配按配置顺序“先到先得”。这句话基本能解释绝大部分匹配问题。1.2 匹配优先级与流量走向判断Nginx实际处理location匹配时官方文档给出的流程是先做普通前缀匹配记录下最长的一个然后从第一条正则开始按书写顺序检查一旦有正则命中就用这个正则结果不再继续往下看如果正则都没命中才用之前记录的最长普通前缀。我把这个流程整理成一张判定表方便对照优先级匹配类型说明1精确匹配命中后立即结束不再做其他任何检查2^~前缀匹配在普通前缀中优先命中后跳过正则检查3~/~*正则匹配按nginx.conf中的书写顺序执行命中即停4普通前缀匹配取最长前缀但要等正则全部失败后才生效5/兜底匹配能匹配所有请求通常做最后一道防线这里有个最常见的误解有人以为location /api/写在location ~ ^/api/前面就能先匹配前缀、不让正则生效。错了。普通前缀匹配即使命中了也只是“暂存”起来正则还是会在后面被检查。只有^~才能阻止正则继续检查。比如请求是/api/user配置里有location /api/和location ~ ^/api/那么最终生效的永远是正则那个~ ^/api/。如果你希望前缀匹配优先必须写成location ^~ /api/。这是线上故障里出现频率极高的问题我在第4章排查清单里还会再提到。1.3 举例论证一条正则改变所有“我以为”说一个我自己踩过的坑。当时有个后端服务接口前缀是/openapi/我按常规配置了location /openapi/ { proxy_pass http://backend_api; }测试一切正常。后来运营要加一个对外公开的接口/openapi/health不想做鉴权我就顺手加了条正则location ~* ^/openapi/health { return 200 ok; }。结果这条配置上线后后端收到的请求变成了/health健康检查全部异常。原因很直接正则优先级高于普通前缀~* ^/openapi/health先命中把请求从后端转发路径上截走了而这条location里根本没写proxy_passNginx默认返回404。那次之后我给自己立了个规矩凡是同一路径前缀下既有转发又有特殊处理的要么用^~把转发路径锁死要么把特殊逻辑直接写进正则里并且把proxy_pass一并写全。不要指望靠“书写顺序”来控制匹配优先级——除非你非常清楚Nginx的决策顺序。2. 配置URL转发proxy_pass里那一个斜杠的差别2.1 带/不带URI的两种转发行为location匹配规则搞清楚了接下来是最容易写错的转发配置。proxy_pass后端地址末尾带不带/转发出去的效果是完全不同的。关键规则是这样的proxy_pass的地址里如果带了URI部分也就是/之后有内容Nginx会用这个URI替换掉location匹配成功的那一段路径。我直接拿一个例子演示请求GET /api/user?id1location配置location /api/ { ... }proxy_pass写法上游实际收到的URIhttp://192.168.1.10:8080;/api/user?id1http://192.168.1.10:8080/;/user?id1http://192.168.1.10:8080/v2;/v2user?id1很多人看第三行会觉得奇怪为什么是/v2user而不是/v2/user因为location匹配到的是/api/这个/api/整体被替换成了/v2而剩下的user?id1拼在后面于是得到/v2user?id1。所以如果你想让上游服务收到/v2/user?id1proxy_pass应该写成http://192.168.1.10:8080/v2/。正则location配proxy_pass还有个硬性限制当location后面跟的是正则~或~*时proxy_pass不能带URI部分。比如这样写会直接抛错、Nginx拒绝启动location ~ ^/api/(.*)$ { proxy_pass http://backend/v2; # 启动报错 }报错信息是proxy_pass cannot have URI part in location given by regular expression。解决办法是用变量或rewrite绕开后面章节会讲。2.2 rewrite、try_files和proxy_pass怎么搭配转发需求里除了proxy_pass直连后端还经常遇到两种情况一是想把请求URI改写成另外一套路径再继续处理二是前端单页应用刷新时找不到页面要回退到index.html。先说rewrite和proxy_pass配合。rewrite可以做“路径改写继续匹配”location /old/ { rewrite ^/old/(.*)$ /new/$1 break; proxy_pass http://backend; }这里break和last的区别很多人分不清。简单说break会停止后续rewrite指令的匹配但保留在当前的location里继续执行不会重新匹配locationlast则会结束当前rewrite模块的处理然后重新发起一次location匹配。所以如果rewrite后面紧跟proxy_pass建议用break因为你要的是“改成新路径后立刻转发给后端”而不是“改完了再让Nginx按新路径重新选一遍location”。try_files更适合处理静态资源回退和前端history路由location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; }它的含义是先尝试按当前URI找文件找不到就尝试加/的目录再找不到就内部重定向到/index.html。注意这里是“内部重定向”不会改变浏览器地址栏URL非常适合Vue、React这类单页应用。2.3 多Web项目复用Nginx的路径规划热词里有一项是“nginx部署多个web项目”这其实是多server和root/alias的经典问题。你完全可以在一个Nginx里用不同端口或不同域名挂多个项目server { listen 80; server_name project-a.example.com; root /data/www/a; } server { listen 80; server_name project-b.example.com; root /data/www/b; }如果只有一个域名、想按路径区分多个项目就要特别注意root和alias的区别。root会把location匹配部分拼在root目录后面alias则直接用alias定义的路径替换location匹配部分。# root方式请求 /a/static/x.js 会找 /data/www/a/static/x.js location /a/ { root /data/www; } # alias方式请求 /b/static/x.js 会找 /data/app-b/static/x.js location /b/ { alias /data/app-b/static/; }很多人在多项目部署时把root和alias混用导致静态资源404。一条经验如果你在location里写了具体项目目录优先考虑alias如果你是用root接整个web根目录那location本身就是目录路径的一部分两者别搞混。3. 从零配置一个可用的转发服务三个高频场景一次搞定3.1 场景一剥离前缀转发到后端服务最常见的转发需求就是“前端统一通过/api/访问后端但后端接口本身不带/api前缀”。用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 Host $host;这一行。如果不加后端默认收到的是192.168.1.10:8080这个IP作为Host头有些框架的请求校验、URL生成就会出错。X-Real-IP和X-Forwarded-For是让后端拿得到真实客户端IP的标配尤其是后端还有日志审计、风控需求时这两行几乎必须写。$proxy_add_x_forwarded_for这个变量很有意思它会自动把X-Forwarded-For里已有的内容加上当前连接的$remote_addr累加进去。也就是说如果客户端先经过了另一层代理这层代理转发时带了X-Forwarded-For: 1.2.3.4Nginx再透传时会变成1.2.3.4, 5.6.7.8完整保留链路。3.2 场景二前端history路由刷新不回退前端用了Vue Router或React Router的history模式后会出现一个典型问题用户从首页跳到/user/list没问题但在这个页面按F5刷新Nginx返回404。原因是前端路由只是URL变化服务器文件系统里根本没有/user/list这个文件。解决办法就是前面提到的try_files方案配合前端构建产物server { listen 80; server_name example.com; root /data/frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?)$ { expires 30d; add_header Cache-Control public, immutable; access_log off; } }后半段给静态资源加了缓存头避免每次刷新都重新走一次try_files逻辑。这里有一个需要留意的地方如果接口请求也走的是/api/开头那么/api/的location要写在静态资源location之前或者用^~锁住否则正则location有可能会把接口请求也当成静态文件来处理导致转发失效。3.3 场景三大文件上传不再断连很多人遇到“超过1G就断”的问题原因往往不是单一配置导致的而是几个配置叠加起来把连接掐断了。上传大文件时Nginx侧要同时关注三个点location /upload/ { proxy_pass http://upload-backend; client_max_body_size 2g; client_body_timeout 300s; proxy_request_buffering off; proxy_read_timeout 600s; proxy_send_timeout 600s; }client_max_body_size默认只有1m超过就直接返回413 Request Entity Too Large。这里要注意如果前端框架比如Spring Boot的spring.servlet.multipart.max-file-size也设置了文件大小那么它是第二道闸门Nginx是第一道闸门两边都需要调整否则你只改了一边另一边还是会拦。proxy_request_buffering off这一行很多人没注意到。默认情况下Nginx会先把客户端上传的整个请求体缓冲到磁盘临时文件再转发给上游。文件一但很大磁盘空间不够或临时路径没权限连接就会异常中断。关掉缓冲后请求体会直接从客户端流式转发给上游虽然上游接收速度跟不上时可能占用更多连接资源但大文件上传场景下这个取舍通常值得。proxy_read_timeout和proxy_send_timeout是控制读取上游响应和向后端发送数据的超时时间默认60秒对正常接口够用但大文件上传后的处理、视频转码这类慢接口很容易超时。改成600秒够覆盖绝大多数业务。3.4 顺手处理响应头隐藏X-Powered-By这类字段热词里有一条“nginx隐藏站点 response header 里的x-powered-by 字段”。X-Powered-By一般是后端应用框架自己加的响应头Nginx默认会原样透传。你可以在location或server里写proxy_hide_header X-Powered-By;proxy_hide_header的作用是从上游响应里移除指定字段然后Nginx再返回给客户端。同理如果你想改Nginx自己加的Server头用的是server_tokens off;它可以把Server头的版本号去掉但不会完全隐藏Server这个字段。如果业务对安全扫描要求严格可以在server块里用headers-more-nginx-module这种第三方模块做更彻底的响应头清理。4. 高频问题排查与排错实录附速查表4.1 热词里那些问题到底怎么回事这些年我在不同环境下处理过不少Nginx问题发现很多高频报错的根因其实是固定的。这里整理一张速查表按现象、原因、处理思路三列展开现象 / 报错常见原因处理思路nginx: 未找到命令安装后PATH路径没配置或根本没装成功先which nginx再检查/usr/local/nginx/sbin是否在PATH里没有可用软件包 nginx系统默认源里没有Nginx包CentOS/RedHat系先装EPEL源或直接走源码编译/离线rpm包安装请求返回413client_max_body_size太小按业务需要调大记得同时检查前端上传限制上传超过1G断连请求体缓冲、超时时间、临时目录空间综合导致按3.3节三项配置一起调返回502 Bad Gateway上游服务没起、端口不通、防火墙拦截curl直连上游验证再看error.log返回504 Gateway Time-outproxy_read_timeout或proxy_send_timeout太短加大超时时间优化上游接口耗时net::ERR_QUIC_PROTOCOL_ERRORHTTP/3/QUIC配置与客户端不兼容检查listen 443 quic、证书链是否正确客户端不支持HTTP/3时回退到HTTP/2PHP站点经常404缺少location ~ \.php$或fastcgi配置错误确认fastcgi_pass指向的PHP-FPM地址正确include fastcgi_params系统报告Nginx相关CVE漏洞版本过旧缺少安全补丁升级到修复版本离线环境用新版本源码重新编译网上很多关于“银河麒麟离线安装nginx”“欧拉系统离线安装nginx”“CentOS离线安装nginx”的提问本质都是在没有外网的环境里装一个带依赖的Nginx。这类情况的通用做法是准备一台同架构的联网机器用yum download或直接从源码包把pcre、zlib、openssl、nginx一起拉下来再打包传到离线机器编译安装。aarch64架构还要注意下载对应架构的源码包不能直接拿x86_64的rpm强装。4.2 排查Nginx转发问题的标准动作遇到转发异常我一般的排查顺序是先确认请求到底有没有到Nginx再确认请求被转发到了哪里最后看上游返回了什么。第一步看access.log。默认日志会记录请求URI、状态码、响应时间但看不出转发目标。这时候可以先临时改log_format把上游地址加进去log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_x_forwarded_for $upstream_addr $upstream_status $upstream_response_time;$upstream_addr记录了实际转发的上游地址和端口$upstream_status是上游返回的状态码$upstream_response_time是上游处理耗时。如果日志里$upstream_addr为空但请求进来了说明请求没走proxy_pass分支大概率是命中到了别的location。第二步用curl模拟。先直连上游看服务是否正常curl -i http://192.168.1.10:8080/health再走Nginx转发链路curl -i -H Host: example.com http://127.0.0.1:80/api/health对比两次响应能快速区分问题出在Nginx还是上游。加-L参数还能看到重定向走向排查返回值是301/302时特别有用。第三步看error.log。如果Nginx转发时出现connection refused、upstream timed out、no live upstreams这类错误error.log里都会写明。调日志级别可以看得更细error_log /var/log/nginx/debug.log debug;这个配置只建议在测试环境用debug日志量非常大生产环境开debug会拖垮磁盘。4.3 几个趁手调试技巧调试location匹配最直观的办法不是猜也不是反复reload而是看最终生效的location在哪里。你可以在server块里临时加一段默认响应把当前命中的配置“打出来”location /debug { return 200 hit debug location\n; }然后把location /里的配置换成return 200 hit root location\n;再用curl分别请求几个路径马上就能确认到底谁在生效。另外代理场景下经常需要确认转发后的最终URL。用curl加-v参数看请求行配--trace-ascii -可以把HTTP报文完整打出来连Nginx转发后的Host、请求路径都能看到。比在浏览器F12里看更直接。检查配置语法是每次改动后必做的一步nginx -t。如果是源码编译安装记得用sbin/nginx -t或者把/usr/local/nginx/sbin加进PATH。改完配置用nginx -s reload平滑重载不会中断现有请求。最后再分享一个我自己的小习惯每次准备改线上Nginx配置前我都会把当前配置做一次备份cp nginx.conf nginx.conf.bak-20240101这种格式然后先在本地或测试机跑一份nginx -t再用curl把关键路径测一遍。这套流程看着简单但真能避免很多“改完就回不去”的尴尬。Nginx路由匹配和url转发说到底就是“给请求找对门”的问题把匹配顺序和proxy_pass的URI替换逻辑吃透线上绝大多数转发问题都能在五分钟内定位到根因。偶尔遇到诡异现象不妨先停下来问一句请求真的走进我以为的那个location了吗如果答案不确定按我上面的调试思路走一遍基本就能水落石出。