2.1 路由设计:路径、版本与灰度
场景:四十个接口三种版本,路由乱成一锅粥
网关开工第一个模块就是路由。起因是我们的接口版本格局实在没法看:老客户还在调 /v1/inventory(三年前的路径风格,下划线),新客户调 /v2/inventory-query(驼峰),还有个 /api/v2/inventory 的历史前缀。同一个业务在三套路径下并存,文档站上密密麻麻全是兼容说明。更糟的是后端服务在拆分,库存服务从订单服务里独立出来,需要按路径分发到不同机器。
我们给路由模块定了三个目标:统一路径风格带版本号、支持按服务名转发到多台后端、支持灰度分组把新版本接口先放给试点调用方。不做花哨的功能,不做可视化编辑器,路由表就是一份 PHP 配置,git 里 review,改完走灰度流程生效。
定目标时还砍掉了两个诱人功能:可视化路由编辑器与按地域分发。前者会让配置变更脱离 git review,后者我们根本没有多地域部署。每砍掉一个功能就等于消灭一类未来事故。路由模块的边界讨论我们只花了一次会,但那次会至少省了三个月返工。
方案:三段式路径加路由表
路径规范我们定为 /v{版本}/{服务}/{接口},例如 /v2/inventory/query。旧路径不做强迁,网关里做一层 rewrite 映射,老路径静默跳新路径,同时给响应头加一条废弃警告。路由表则按服务组织,每个服务挂多个后端地址,为灰度做准备:
| 路由需求 | 实现方式 | 上线效果 |
|---|---|---|
| 统一风格 | 三段式路径规范 | 新接口必然合规 |
| 旧路径兼容 | rewrite 映射表 | 老客户零改造 |
| 按服务分发 | 路由表挂后端地址池 | 支持服务拆分 |
| 灰度分组 | 路由项加 group 字段 | 按调用方放量 |
灰度分组的设计是整篇的精华:同一个接口可以注册多个路由项,每个路由项标记 group=stable 或 group=beta,再由灰度中间件判断当前调用方是否在试点名单里。名单命中就走 beta 路由(指向新版后端),否则走 stable。这样我们能把新接口先放给两三家配合度高的客户验证,而不是按流量比例盲抽。
配置:路由表与匹配
路由表的数据结构很直白,一个嵌套数组讲清全部规则。匹配顺序从前到后,先命中先生效,表里越具体的路径越靠前:
<?php
// 路由表示例:改这里即改全站分发
return [
[
'pattern' => '/v2/inventory/query',
'service' => 'inventory',
'group' => 'stable',
'backend' => ['10.0.1.11:8080', '10.0.1.12:8080'],
],
[
'pattern' => '/v2/inventory/query',
'service' => 'inventory',
'group' => 'beta', // 灰度组
'backend' => ['10.0.2.21:8080'],
],
[
'pattern' => '/v1/*',
'rewrite' => '/v2/{1}', // 旧路径静默升级
'service' => 'legacy',
'group' => 'stable',
'backend' => ['10.0.1.31:8080'],
],
];
匹配逻辑就是一次循环加通配符比对,灰度判定交给第 3.4 节的中间件。路由命中后把选中的后端地址塞进请求属性,转发中间件只用读,职责清楚。
路由匹配的性能账
有同事担心每请求一遍循环匹配扛不住 800 QPS。实际测下来四十条路由的线性匹配耗时不到 0.1 毫秒,完全不是瓶颈。但为了给未来留余量,我们还是加了一层 APCu 缓存:匹配结果按路径加 appkey 做 key 缓存五秒,命中后直接复用。灰度变更生效延迟因此最多五秒,可以接受。你知道哪些场景值得优化、哪些纯属自嗨,是工程成熟度的标志,这次我们选择了不优化,把精力留给限流。
版本迁移的运营节奏
路由表上线只是技术开始,版本迁移的客户沟通才是苦活。我们给 v1 接口定了十二个月的迁移窗口,节奏固定:前三个月文档站飘黄条提示、第四个月起响应头加废弃警告(X-Deprecated: v1-sunset-2026-12)、第九个月开始限速到每秒一次、第十二个月关停并定向到迁移指南。每一步时间点都在合同与文档里提前公示,客户有充足时间排期。
结果是四十三家使用 v1 的客户里,三十九家在第三个月内自行完成迁移,剩四家被商务同学逐个电话催完,无一因强制关停产生纠纷。这套节奏后来成了公司所有版本下线的标准流程。技术人常以为系统上线就是终点,其实客户的那一侧,路才走了一半。
灰度分组的运维也有讲究。名单生效前必须经过三重确认:改动的同学自测、值班同学复核、产品同学确认客户已知晓。改名单这件事不需要敲三下锣,但流程跑在群里留痕,出问题时能一秒定位是谁在什么时间放了哪家客户进来。我们有一次差点把一家未签约客户放进 beta 名单,幸亏产品同学在群里拦下,这种低级错误靠流程兜住了。灰度名单的每次变更同时会写进 CLS 留痕,与第 4.4 节的审计链路打通。
踩坑:rewrite 链与缓存串号
第一个坑是 rewrite 写出了链:/v1/* 映到 /v2/{1},而某条 v2 路径又配了 rewrite 到 /v3/*,结果请求转了三手。修复方式是规定 rewrite 只允许一层,代码里 rewrite 完立刻跳出匹配循环。
第二个坑是缓存串号。APCu 缓存的键最初只用了路径,没带 appkey,结果灰度名单内的客户也命中了其他普通客户的 stable 路由结果,灰度等于失效了十分钟。所幸发现得早——灰度验证本来就要专人盯着试点客户反馈。修复方式是把缓存 key 加上 appkey 与 group,并在灰度名单变更时主动清缓存。后面所有缓存设计我们都引以为戒:键里必须带一切影响结果的维度。
第三个坑是后端地址池写死 IP,某次后端换机器忘了同步,网关 502 了半小时。后来后端地址池改为从内网 DNS 服务发现拉取,每三十秒刷新一次。
第四个坑是通配符的贪心匹配。/v1/* 这条规则最初写成了 /v1/ 前缀匹配,任何以 /v1/ 开头的路径都会命中,包括 /v1/orders/export-big 这条本应由专属路由处理的接口,导致它被发到了错误的后端服务,参数格式不合直接报错。修复方式是通配符只匹配单段,跨段匹配用显式声明,并在路由表变更后跑一遍样本回归测试。所有看似省事的前缀匹配都是定时炸弹,越精确越安全。
路由模块上线后我们还做了一次性能复核:把全部路由命中情况打点,统计出热点前三名的接口占了六成流量,于是给这三条加了独立的匹配快速通道。数据驱动的优化比凭感觉抠性能靠谱一百倍,这也是后面第四篇可观测性理念在路由层的具体体现。
路由表最终成为团队协作的公约数:后端的同学声明服务,网关同学维护路由,产品同学在文档站声明接口,三方各管一段互不越界。这份公约数在团队扩张到二十人时仍然稳固,新同学 read 半小时就能上手改路由并自主走灰度。模块边界划得清楚,扩展就是加表加行;边界划得混乱,每个改动都是雷区。
路由变更的发布也形成了一套固定仪式:改路由表的同学自己在压测环境跑回放,然后提 MR 艾特值班同学复核,复核过了才允许灰度名单里的一家试点客户先跑半小时,确认无误后清缓存全量。这套仪式每个环节不超过五分钟,但把路由变更事故挡到了零。仪式感在运维里不是形式主义,是把信任建立在流程之上——人可以换,流程不会走样。路由这节的所有细节——分组、灰度名单、缓存键设计——下一章流量治理会直接接着用,建议两篇连读。
小结
路由设计的三条心得:路径规范一次定死、版本与灰度用 group 字段表达、后端地址走服务发现别写死 IP。路由模块是网关里最简单也最重要的一层——简单到可以一百行代码写完,重要到出 bug 全站都趴下。灰度分组为后面的 3.4 节铺路,记住它的运作方式:路由表提供多份,灰度中间件负责选哪份。路由模块上线三个月零事故,靠的不是代码精妙,是它足够简单到没什么地方可坏。
再给一句量化的底气:路由模块上线半年后我们统计过,全部路由变更二十六次,平均每次从提 MR 到全量生效二十二分钟,零事故。如果当初上了可视化编辑器,变更会绕过 git 留痕,这个安全记录绝无可能。简单与留痕是最好的两道护栏。
顺带一个路由规范的推广经验:路径规范我们写进了一份 OpenAPI 风格的契约文件,它同时是文档站、客户端 SDK 与网关路由表的共同来源。单一契约驱动三处,改一处全站同步,路由再没对不齐过。规范最好的归宿不是 wiki,而是代码与文档之间的那座桥。
