ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Nginx location与proxy_pass配置详解:匹配规则、路径替换与实战踩坑

Nginx location与proxy_pass配置详解:匹配规则、路径替换与实战踩坑 聊到 Nginx 的 location 和 proxy_pass 配置我猜大部分人都经历过这种现场明明 proxy_pass 写的是 http://127.0.0.1:8080后端收到的却是一个 /api 开头的奇怪路径或者两个 location 看上去都能匹配最后生效的却不是你以为的那个。这两个指令是 Nginx 反向代理的命门也是简历上写着“熟悉 Nginx”的人最容易在面试时翻车的两个点。我不打算把官方文档复述一遍而是把 location 的匹配顺序、proxy_pass 的 URI 替换逻辑拆开揉碎结合真实业务场景给出一套可以直接抄的写法。适合刚学会 Nginx 基础配置、又被反向代理路由绕晕的人也适合准备梳理面试知识点的老手。1. location 匹配规则Nginx 到底先选中谁再决定干嘛在配置 proxy_pass 之前先得知道请求最终落在哪个 location。location 匹配不是“自上而下找第一个”也不是“越短越先匹配”而是一套带优先级的前缀加正则混合流程。很多人一上来就写一堆正则 location结果某些路径老是被一个普通前缀截胡原因就在这里。1.1 四种匹配方式先背下优先级Nginx 里的 location 可以分成四类精确匹配。比如location /favicon.ico只匹配/favicon.ico这个路径query string 不参与 location 匹配所以/favicon.ico?id1也会被它命中。^~前缀匹配且命中后不再检查正则。比如location ^~ /static/一旦作为最长前缀命中就直接采用不再给正则机会。~/~*正则匹配前者区分大小写后者不区分。正则之间按配置文件出现顺序第一个匹配的生效。普通前缀没有任何修饰符如location /api/。它会参与最长前缀匹配但最终可能被正则覆盖。优先级可以归纳成一张表匹配类型例子优先级备注精确匹配location /x最高一旦命中立即结束前缀匹配 不检查正则location ^~ /x次高最长前缀命中后结束正则匹配location ~ /x第三按配置顺序第一个命中结束普通前缀location /x最低先记录最长前缀但会被正则覆盖这里最关键的一点普通前缀并不是在之后直接生效而是要等正则检查完后才可能被使用。Nginx 先扫描所有前缀 location 并记录最长的一个如果这个最长前缀是^~则直接使用否则继续按顺序扫描正则是谁的配置。有正则命中就改用正则没有正则命中才退回之前记录的最长前缀。1.2 一个请求同时命中多个 location最终落点是谁我直接给一个容易让人迷惑的例子。假设配置文件里这样写server { listen 80; location / { proxy_pass http://backend_a; } location /images/ { proxy_pass http://backend_b; } location ~ \.(png|jpg|gif)$ { proxy_pass http://backend_c; } location ^~ /images/banner/ { proxy_pass http://backend_d; } }请求/images/banner/logo.png会走哪一个先扫前缀/能匹配/images/能匹配/images/banner/也能匹配最长前缀是/images/banner/它带^~所以这里直接选中backend_d不再去检查后面的正则\.(png|jpg|gif)$。如果把^~ /images/banner/那一行删掉请求/images/banner/logo.png会先记录最长前缀/images/然后检查正则发现\.png$命中了最终选择backend_c而不是/images/这一条。很多人的误区是“Nginx 会先匹配最长前缀只要 /images/ 命中就会走 backend_b”。不对只要存在能命中的正则 location普通前缀就会被无视。想用普通前缀锁住一组路径要么给前缀加^~要么就接受正则的优先级。还有一点精确匹配一旦命中连^~也不会再考虑因为它的优先级最高。比如location /index.html和location /index.html同时存在时前者先命中并直接结束。^~特别适合用来放静态目录、下载目录这类路径往往文件名后缀复杂很容易被正则抢走加上^~后就能确保按目录前缀走。1.3 URI 标准化对匹配结果的影响location 匹配的是 Nginx 解码后的 URI不是浏览器地址栏里那个原始字符串。Nginx 会先对请求行中的 URI 做规范化处理解码%XX、把连续的/合并成单个/、解析.和..片段。也就是说/a/../b会被当成/b去匹配 location。这个细节在日常配置里影响不大但遇到特殊路径时会很困惑。比如有的程序会自己拼接 URL结果路径里带..Nginx 在转发前已经把路径改掉了后端的日志里看到的可能和你预期的不一样。另外空格、中文等字符在 URI 中经过编码后匹配时以解码后的形式为准。这部分的结论很简单想控制路由先把 location 的匹配顺序背下来再谈 proxy_pass 怎么写。2. proxy_pass 的 URI 替换逻辑斜杠差一点转发结果差一截location 决定“请求交给谁处理”proxy_pass 决定“交给上游时路径长什么样”。这两个事情经常被一起讨论是因为 proxy_pass 写完 URL 的斜杠会直接影响后端收到的路径。我在群里见过无数次这种问题proxy_pass http://backend;和proxy_pass http://backend/;只差一个斜杠后端接口却一个 404 一个正常。2.1 不带 URI 的 proxy_pass原样转发先看不带 URI 的情况。所谓“不带 URI”就是 proxy_pass 后面只有协议、域名或 IP、端口路径部分什么都不写比如location /api/ { proxy_pass http://127.0.0.1:8080; }请求/api/order/1001转发到后端时路径仍然是/api/order/1001Nginx 不会去掉前缀也不会额外加任何东西。这适合后端自身就声明了上下文路径的情况。比如 Spring Boot 项目配置了server.servlet.context-path/api或者网关里的路由前缀就是/api那用这种写法最省心。不带 URI 的另一个好处是基本不受 location 匹配类型影响。无论是普通前缀、^~还是正则 locationproxy_pass 后面只写http://host:port时请求 URI 基本都是原样转发。很多人折腾代理时发现“哎正则 location 下用不了带路径的 proxy_pass”于是干脆统一用不带 URI 的写法再配合 rewrite 改路径这样心智负担小很多。2.2 带 URI 的 proxy_pass前缀替换规则一旦 proxy_pass 的 URL 后面带了路径哪怕只是/这个根路径行为就完全不同了。Nginx 会用 proxy_pass 里的 URI 替换掉 location 匹配到的那一段前缀。location /api/ { proxy_pass http://127.0.0.1:8080/; }请求/api/order/1001转发到后端时路径会变成/order/1001。也就是说/api/被替换成了/。再举一个携带具体路径的例子location /api/ { proxy_pass http://127.0.0.1:8080/v2/; }请求/api/order会变成/v2/order。更复杂的替换也可以类推如果location /a/b/ { proxy_pass http://backend/c/; }请求/a/b/x会被替换成/c/x。这里的规则可以用一句话记去掉 location 前缀换上 proxy_pass 后面的 URI。要注意替换是按“匹配到的前缀”来计算的不是简单地把 proxy_pass 的字符串拼在前面。比如 location 写的是/api不带尾部斜杠匹配到了/api/order那被替换掉的是/api这一段结果同样是/order。所以location /api和location /api/虽然只差一个斜杠但能匹配的请求集合和替换结果都可能不同配置时尽量统一用带斜杠的写法避免测试时给自己挖坑。2.3 正则 location 里为什么不能直接加路径正则 location 的情况比前缀匹配更严格。Nginx 官方规则是如果 location 是用正则定义的proxy_pass 指令后面不能带 URI。比如下面这种写法location ~ ^/api/(.*)$ { proxy_pass http://127.0.0.1:8080/; }Nginx 会直接报错nginx -t阶段就过不去报错信息类似proxy_pass cannot have URI part in location given by regular expression。原因是正则匹配到的“前缀”不是一个固定字符串Nginx 不知道该用哪一段去替换。在正则 location 里想改写路径常规做法是先用 rewrite 把 URI 改掉再配合不带 URI 的 proxy_passlocation ~ ^/api/(.*)$ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://127.0.0.1:8080; }请求/api/order/1001会先被 rewrite 成/order/1001之后 proxy_pass 原样转发后端收到的就是/order/1001。这里break不能省否则 rewrite 后可能再次进入其他 location 或产生额外重定向。rewrite ... break和rewrite ... last的区别在于break 终止当前 location 的后续指令但不重新匹配 locationlast 会重新从 location 匹配流程走一遍。在 proxy_pass 之前做路径改写用 break 更可控。另一个可选方案是在 proxy_pass 中使用变量比如proxy_pass http://127.0.0.1:8080$request_uri;但如果变量里带的是原始$request_uri它会把/api也带过去和前端的匹配逻辑不一定一致。使用变量时 Nginx 需要能在运行时解析域名所以在 server 里通常要配置 resolver否则日志里会报no resolver defined to resolve。规范起见优先用 rewrite 加不带 URI 的 proxy_pass。2.4 从一次 /api 接口 404 看路径变化我之前帮人排查过一个典型问题。Nginx 配置是location /api/ { proxy_pass http://127.0.0.1:8081/; }后端的接口路径是/api/user客户端请求的也是/api/user。按替换规则location 匹配到的/api/被替换成 proxy_pass 的/所以后端收到的实际路径是/user而后端只发布了/api/user于是 404。后端说自己接口没问题前端说自己请求路径没问题最后一看 Nginx 的 access log 才发现它把/api前缀吃掉了。排查这类问题最快的方法不是猜而是看后端的访问日志里到底收到了什么 path或者直接在 Nginx 的 access log 里观察$request_uri和$uri。$request_uri是浏览器发来的原始请求路径$uri是 Nginx 改写和规范化后的路径。当proxy_pass带了 URI、或者 location 内执行了 rewrite两者就会不一致。这里再强调一次proxy_pass http://backend;是透传proxy_pass http://backend/;是替换两者只差一个/结论完全不同。3. 把 location 和 proxy_pass 放进真实业务场景理解规则后看几个常见的业务组合。这些配置不是互相孤立的很多人问“为什么我按教程写了还是不行”多半是只抄了零散片段没理清每个指令在整套链路里的作用。3.1 前后端分离静态资源与 API 分流最常见的是静态资源用 Nginx 直接返回动态接口转发到后端。比如一个 Vue 或 React 项目部署时可以这样写server { listen 80; server_name example.com; location ^~ /assets/ { alias /var/www/myapp/dist/assets/; expires 7d; add_header Cache-Control public; } 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; proxy_set_header X-Forwarded-Proto $scheme; } location / { root /var/www/myapp/dist; try_files $uri $uri/ /index.html; } }这里^~ /assets/是专门为了让静态资源不被后面的正则 location 干扰。比如某个资源 URL 恰好以.js结尾如果没有^~后面一旦出现正则~ \.js$资源请求就可能走代理而不是直接读文件。静态资源有几行头信息要说明一下Host $host让后端认为请求域名还是原来的域名X-Real-IP $remote_addr把直连 Nginx 的客户端 IP 带给后端X-Forwarded-For保存原始 IP 和经过的代理 IP 链X-Forwarded-Proto $scheme告诉后端客户端用的是 HTTP 还是 HTTPS。这些头在大多数反向代理场景下都是标配。try_files $uri $uri/ /index.html是 SPA 路由的保底方案让前端 history 路由刷新时能回到入口页。注意/api/的 location 优先级比/高所以 API 请求不会落到 try_files 逻辑里。3.2 负载均衡upstream proxy_pass 的高并发组合Nginx 做反向代理时后端不止一台机器是常态。这时要用 upstream 把一组后端服务器包起来再让 proxy_pass 指向这个组upstream backend_cluster { server 192.168.1.11:8080 weight3; server 192.168.1.12:8080; keepalive 32; } server { listen 80; location /api/ { proxy_pass http://backend_cluster; proxy_http_version 1.1; proxy_set_header Connection ; 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; } }默认策略是轮询weight 可以控制权重如果需要同一个用户粘到同一台后端可以改成ip_hash如果各后端机器性能差异不大用默认轮询就够了。upstream 里的机器还可以加max_fails和fail_timeout做被动健康检查比如server 192.168.1.11:8080 max_fails3 fail_timeout30s;连续失败 3 次后这台机器 30 秒内会被标记为不可用请求自动转发到其他后端。这个机制依赖后端连接层的错误不会真正执行 HTTP 健康检查但对多数场景足够。高并发下有个非常容易忽略的点Nginx 默认与后端通信使用 HTTP/1.0不能复用连接每个请求都要重新建 TCP连接成本很高。所以加上proxy_http_version 1.1;和proxy_set_header Connection ;配合 upstream 的keepalive 32;Nginx 与后端之间的连接可以复用显著降低握手开销。这是很多“Nginx 高并发优化”文章里提的长连接但它只适用于普通 HTTP 代理WebSocket 场景不能用这种方式。3.3 WebSocket 代理WS 端口穿透要带 Upgrade 头WebSocket 的握手本质上是 HTTP Upgrade所以 Nginx 代理 WebSocket 不需要什么特殊模块只要在 location 里把Upgrade和Connection两个头透传过去。配置如下upstream ws_backend { server 127.0.0.1:5066; } server { listen 80; location /ws/ { proxy_pass http://ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }proxy_set_header Connection upgrade是关键它会覆盖默认的Connection头让 Nginx 把前端的升级请求转给后端。proxy_read_timeout和proxy_send_timeout也需要调大默认 60 秒如果 60 秒内没有数据流动Nginx 会主动断开连接你看到的 WebSocket 就频繁掉线。有些业务比如代理 FreeSWITCH 的 WS 端口后端可能除了 WebSocket 还需要处理普通 HTTP 请求这时可以把/ws/单独分出来。如果后端 WebSocket 路径不是/ws/记得在 location 里做路径改写因为 WebSocket 握手路径对后端有语义location /ws/ { rewrite ^/ws/(.*)$ /$1 break; proxy_pass http://127.0.0.1:5066; }这样请求/ws/socket到后端是/socket后端不用感知外层前缀。写完后一定用浏览器或wscat实际连一次别只看握手 101 就以为成功还要看后续消息是否正常收发。如果是 HTTPS 站点前端应该连wss://your-domain.com/ws/Nginx 层做 SSL 终结后再以 WebSocket 转发到后端。3.4 HTTPS 入口 内部 HTTP 回源现在的站点基本都离不开 HTTPS。Nginx 最常见的角色是统一终结 SSL再通过 proxy_pass 把请求转发到内网 HTTP 服务。这样后端服务本身不用处理证书证书更新只在 Nginx 一层做。自己测试环境可以用自签名证书生成命令很简单openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 -subj /CNyour-domain.com然后 Nginx 配置server { listen 443 ssl; server_name your-domain.com; ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; } }X-Forwarded-Proto $scheme一定要带否则后端不知道用户实际用的是 HTTPS可能在生成绝对链接或做跳转时把协议降级成 HTTP。如果要强制 HTTP 跳转到 HTTPS可以再加一个只监听 80 的 serverserver { listen 80; server_name your-domain.com; return 301 https://$host$request_uri; }这里$request_uri是原始请求的 URI比直接写$uri更稳妥因为$uri经过 Nginx 解码和规范化可能会丢失原始参数或编码形式的路径。生产环境如果证书是正规 CA 签发的Nginx 配置主体不变只是证书文件路径换成正式证书。4. 配置调试与踩坑实记出现问题怎么定位配置语法没问题不代表路由就对了。很多问题是运行时才暴露的比如 502、404、重定向循环。下面这些方法是我每次排查实际问题的固定流程。4.1 验证匹配与转发路径curl、日志、nginx -T 三件套第一步永远是确认请求实际走到了哪个后端、后端收到了什么路径。curl是最快的验证方式curl -I http://localhost/api/user/1001 curl -v http://localhost/api/user/1001-I可以看到响应头和状态码-v能显示连接过程。但这两个命令只能看到 Nginx 返回的结果看不到后端收到的具体请求。想看真实转发路径必须依赖日志。建议在 http 块里定义一个调试用的 log_format把关键变量都打出来log_format debug_log $remote_addr $request $status uri$uri request_uri$request_uri upstream_addr$upstream_addr upstream_status$upstream_status request_time$request_time;然后在对应 server 里临时开启access_log /var/log/nginx/debug.log debug_log;如果请求打到后端后返回 404重点看uri和后端日志里的 path 是否一致。$uri是 Nginx 内部重写和规范化后的路径$request_uri是浏览器发来的原始路径两者不同往往说明中间有 rewrite 或 location 替换发生。再用nginx -T输出渲染后的完整配置检查 include 进来的片段是不是和你预期一致nginx -T这个命令很有用特别是在多个配置文件相互 include 时人肉找很容易漏。另外error log 也别放过很多关键报错都写在/var/log/nginx/error.log里。如果没看到日志先确认error_log级别是不是info或debug默认的warn对运行时路由问题几乎不会输出。4.2 常见故障对照表404、502、循环重定向分别找哪里我在群里回答过太多“Nginx 代理后 404/502”的问题原因翻来覆去就那么几个整理成一张表现象常见原因排查方向404 Not Foundproxy_pass 带 URI 后把路径前缀替换错了后端接口本身路径不存在静态文件 root/alias 写错看 access log 的$uri和后端访问日志502 Bad Gateway后端进程挂了端口没监听Nginx 连不上后端upstream 配置的地址不对telnet 127.0.0.1 8080或curl http://127.0.0.1:8080/health504 Gateway Timeout后端处理时间太长proxy_read_timeout 太小后端连接池满了看 error log配合后端耗时日志重定向循环Host 头传错后端根据 X-Forwarded-Proto 判断协议错误location 匹配又 rewrite 回原路径抓包或 curl -L 看响应头 Location400 Bad Requestproxy_pass URL 写法有问题后端返回了 Nginx 不认识的头部请求头过大看 error log检查 proxy_pass URL 和 proxy_buffer_size有些问题不是 Nginx 配置语法错误而是业务逻辑错误。比如重定向循环很多时候是后端不知道自己的对外地址生成了指向 Nginx 自身地址的跳转。此时重点检查Host、X-Forwarded-Proto、X-Forwarded-Host这些头部是否被正确设置。4.3 性能与健壮性把配置写得更稳的小习惯除了功能正确生产环境还要考虑性能和可维护性。几个我常用的习惯用 upstream 的keepalive配合proxy_http_version 1.1与proxy_set_header Connection 减少与后端建连次数。把不同站点的 server 块拆分到/etc/nginx/conf.d/下的独立文件结构清晰出问题不用在大文件里翻。涉及域名变量的 proxy_pass 记得配resolver否则启动可能正常运行时报no resolver defined。SSE、流式响应或长轮询场景要么关闭proxy_buffering要么把响应体积阈值调大否则前端会等 Nginx 缓冲攒到一定量才收到数据。正则 location 能少用就少用。功能相同的前提下普通前缀和^~的匹配成本远低于正则尤其在高并发下正则匹配会拖慢请求处理。worker_processes 和 worker_connections 不要照抄网上的大数字先观察机器 CPU 核数和并发模型再逐步调整否则只是把压力转移给内存和上下文切换。一个融合了上述点的完整 server 示例upstream api_cluster { server 127.0.0.1:8080 max_fails3 fail_timeout30s; keepalive 16; } server { listen 80; server_name example.com; location /api/ { proxy_pass http://api_cluster; proxy_http_version 1.1; proxy_set_header Connection ; 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; proxy_connect_timeout 5s; proxy_read_timeout 60s; } }这里proxy_connect_timeout 5s是 Nginx 与后端建立 TCP 连接的超时时间proxy_read_timeout 60s是两包数据之间的读取超时不是总处理时间。二者别调反了。4.4 常见面试追问与我的回答思路很多 Nginx 面试题其实就是在考这两个指令。我列几个被问过的高频问题也附上自己的回答思路。第一个location 的匹配顺序是什么精确匹配优先然后是^~前缀再是正则最后才是普通前缀。普通前缀虽然会先记录最长匹配但会被正则覆盖。带上“^~命中后不再检查正则”和“正则之间按顺序”这两句面试官就知道你理解到位了。第二个proxy_pass带不带 URI 有什么区别不带 URI 是原样转发带 URI 是用 proxy_pass 的 URI 替换 location 匹配到的前缀。一句话说完再举/api/到/的例子基本不会扣分。第三个为什么有时候后端收到 404但接口明明存在让面试者先看 Nginx access log 里的$upstream_addr和后端日志的 path多半是 proxy_pass 多写了一个/或者 Host 头没传对。这题其实考的是排查思路。第四个Nginx 做反向代理后后端如何知道用户真实 IP通过X-Real-IP或X-Forwarded-For。前者存真实客户端 IP后者是链路上一串 IP后端取的时候注意别盲信最后一个。如果前面还有一层代理需要按需配置set_real_ip_from和real_ip_header。第五个WebSocket 代理为什么要单独设置 Connection 头因为 WebSocket 握手是 UpgradeNginx 默认会把客户端连接视为普通 HTTP 请求不设置升级头握手就会失败。设置Upgrade $http_upgrade和Connection upgrade之后连接才能升级。我对这套配置的理解是location 管“到哪里去”proxy_pass 管“去了之后用什么路径见后端”。很多所谓疑难杂症到最后都是一个小斜杠或一个顺序问题。配置写完别急着 reload先用nginx -t检查语法再用 curl 和多看几行日志验证行为这个习惯能让生产环境少折腾很多次。
返回列表