反向代理与 API 网关
匹配引擎讲的是「path → 哪个 handler」。把同一件事搬到服务端最前面那一层,就是 reverse proxy(反向代理)与 API gateway:外界只看见一个入口(shop.vega.link),网关拿一张 location 规则表决定这条请求该转发给哪个
upstream,可能是后端服务、静态 CDN 或 SPA 前端。这是路由这一层里 dispatch 那一半在生产环境的真实形态。
它和平表 matcher 最大的不同在消歧规则。平表是「写在前面的先命中」,而 nginx 用一套与书写顺序无关的固定流程 [1]:先在所有前缀 location 里选出最长匹配的那条并记住;如果它带
^~ 修饰符,就直接采用、不再检查正则;否则按配置文件里的出现顺序逐条检查正则,第一个匹配即终止;正则全不中时才回落到刚才记住的那条前缀。而 = 的精确匹配一旦命中就立即终止整个搜索,排在最前。
四档由高到低即 =、^~、正则、普通最长前缀。注意最后两档的相对次序——正则排在普通前缀之前,这是最容易记反的一处。
/static/app.js 观察正则反超前缀,再勾开关把前缀标成 ^~ 看这一翻转。
警示 · 请求 /static/app.js 时前缀 /static/ 明明匹配,命中的却是后面那条正则 ~ \.(js|css|png)$,于是走了 CDN 而非 SPA 静态目录。原因就是上面那条次序:普通前缀被记住之后还要过一遍正则,正则中了就用正则。想让前缀赢回来,得把它标成优先前缀
^~,命中后直接采用、根本不再看正则。这是运维里极易踩的配置坑。
1 · 命中之后的改写
匹配只是上半场。命中一条规则后,网关通常还要改写请求再发给 upstream,最常见的是剥掉 location 前缀:外界访问 /api/v2/orders/42,而后端 api-v2 服务自己只认识 /orders/42,它并不知道被挂在 /api/v2 下。nginx 里
proxy_pass 末尾带不带斜杠决定剥不剥前缀,又一个一字之差、行为两样的经典点。图 0-1 的转发框会显示改写后真正发给后端的 path。
2 · 四种 location 的档位
同一张表里,=、^~、~ 与普通前缀四种 location 各有固定档位,与书写先后无关。
server {
listen 443 ssl;
server_name shop.vega.link;
location = /healthz { proxy_pass http://health-check; }
location ^~ /static/ { proxy_pass http://spa-files; }
location ~ \.(js|css|png)$ { proxy_pass http://cdn-static; }
location /api/v2/ { proxy_pass http://api-v2/; }
location /api/ { proxy_pass http://api-v1/; }
location / { proxy_pass http://spa-fallback; }
}
| location | 类型 | 优先级 | 转发行为 |
|---|---|---|---|
= /healthz |
精确 = |
最高,命中即停 | 原样转发给 health-check |
^~ /static/ |
优先前缀 ^~ |
高于正则 | 命中即采用、跳过正则 |
~ \.(js|css|png)$ |
正则 ~ |
高于普通前缀 | 按定义顺序取第一个命中 |
/api/v2/ |
普通前缀 | 取最长匹配 | 末尾带 /,剥掉 /api/v2 再转发 |
/api/ |
普通前缀 | 比 /api/v2/ 短,输给它 |
同样剥前缀转发 |
/ |
普通前缀 | 最短,兜底 | 转发给 spa-fallback |
3 · 网关的匹配内核
网关择优不是「第一条命中」,而是按四档固定优先级择优:
- 精确
=:命中即停,最高。 - 优先前缀
^~:最长那条若带^~,采用并跳过正则。 - 正则
~:按定义顺序取第一个命中,排在普通前缀之前。 - 普通前缀:回退到最长匹配前缀。
function resolve(rules, path) {
const exact = rules.find(r => r.type === 'exact' && r.loc === path);
if (exact) return exact;
let prefix = null;
for (const r of rules)
if (r.type === 'prefix' && path.startsWith(r.loc))
if (!prefix || r.loc.length > prefix.loc.length) prefix = r;
if (prefix && prefix.caret) return prefix;
const rx = rules.find(r => r.type === 'regex' && new RegExp(r.loc).test(path));
if (rx) return rx;
return prefix;
}
建议 · 网关和前端 router 内核一致,都是「一条 path 跑一张规则表」,差别只在两点。消歧上,router 多用「顺序即优先级」或 specificity 打分,网关用上面那套固定档位;出口上,router 命中后渲染一个视图,网关命中后把请求转发到另一台服务器并可改写 path 与 header。Envoy、Traefik 与 Kubernetes Ingress 概念相同,只是规则写法各异。
匹配引擎讲的「顺序即优先级、specificity、radix tree」是另外三套消歧策略,与本页这套固定档位对照着看。分享入口里 SPA 接入的三条服务端路由,正是本页 location 表的一个最小实例。网关选中一个 upstream 组之后、组里多个版本按权重分流,是加权流量切分的题目。
4 · 参考文献
- nginx. Module ngx_http_core_module,
location指令的匹配流程与优先级. nginx.org.