路由选型与故障转移
这一页讲一次请求怎么落到某个渠道上,以及失败之后怎么换。规则都在 internal/routing,控制台的模型页「路由详情」用同一套评估器,所以界面上看到的原因码与实际行为一致。
一、候选池怎么来的
选型分两步:先从数据库取出候选池,再在池子里挑一个。
候选池由 RouteMemberStore.RoutingCandidates(model, group) 产出:
- 优先精确匹配
model_pattern;没有精确路由时,最长的启用通配符模式胜出(*匹配任意串,?匹配单个字符)。 - 取出该路由的全部成员,连同渠道与凭据事实。
- 按分组收窄:下游令牌可以带
route_group_name。该路由定义了那个分组就按它过滤;没定义时回落到default组。
route_members是优先级与权重的运行时真相源。 渠道自身的优先级与权重是兼容性元数据,不参与选型。
二、资格过滤:七种被排除的原因
每个候选都会被逐条检查,命中任意一条即出局。原因码会原样出现在控制台,所以它既是日志也是解释:
| 原因码 | 含义 |
|---|---|
member_disabled | 该成员被停用(人工或自动熔断) |
channel_disabled | 整个渠道处于停用状态 |
credential_unavailable | 没有可用的凭据 |
cooling_down | 该成员在冷却期内 |
already_attempted | 本次请求已经试过它 |
invalid_weight | 权重为负(配置错误) |
single_mode_other_member | 路由处于单成员模式,而它不是被固定的那个 |
外加两条不产生原因码的过滤:团队授权(模型与成员都要被授权)与调用方指定的排除项。
三、优先级层:先高后低
候选按优先级降序排列,同级内按成员 ID 升序(确定性)。
选型只在最高的、还有可用成员的那一层内进行。高优先层还有可用成员时,低优先层完全不会被考虑——这是「梯次承接」而不是「混合加权」。
四、层内怎么挑:四种策略
路由的 routing_mode 决定层内策略,它覆盖全局开关:
routing_mode | 行为 |
|---|---|
auto | 跟随全局设置(时延感知 / 错误感知开关) |
weighted | 只用成员优先级与权重,关掉时延与错误感知 |
latency | 强制启用时延感知 |
adaptive | 同时启用时延与错误感知 |
加权(默认)
按权重做轮盘赌。三个细节:
- 权重 ≤ 0 的候选永不获胜(在还有其他正权重候选时)。
- 所有权重都为 0 时退化为均匀选择,而不是拒绝服务——「这一层没有任何人配置权重」不应该等于「这一层不可用」。
- 并发感知开启时,权重再乘一个并发因子。
时延感知
得分 = 权重 × 3000 / (3000 + 时延ms)3000ms 是归一化基准,所以 3 秒的渠道保留一半权重,300 毫秒的渠道保留约 91%。基准刻意取得比较大:慢要被惩罚,但不该被惩罚得像失败一样重。
没有时延样本的渠道保留全部权重(冷启动不挨罚)。
错误感知
得分 = 权重 × max(1 − 错误倾向, 0.05)错误倾向是 0~1 的滑动平均,所以错误率 0.5 的渠道保留一半份额,并随成功逐渐恢复。0.05 的下限是刻意的:不健康的渠道保留一点机会,否则它会因为拿不到流量而永远无法证明自己已经恢复。
并发感知
并发因子 = 1 空闲
= (上限 − 在途) / 上限 繁忙
= 0.01 达到或超过上限0.01 的下限保证全部渠道都打满时仍然分散流量,而不是选不出人。
一个必须知道的差异
时延/错误感知模式下,权重 ≤ 0 会被当成 1,而不是「永不获胜」。 只有纯加权模式才把 0 权重当作淘汰。切到 adaptive 之前请确认这是你要的——一个权重设 0 的成员在感知模式下仍会分到流量。
五、健康记录按「上游模型名」记账
时延与错误样本的键是候选实际发给上游的模型名,不是路由的 pattern。
所以同一条路由下的别名成员(cn:glm-5.2 与裸名成员)各自维护独立的健康记录,而不会共享一份样本、永远同分。
六、灰度:先稳定后扩展
给渠道打上灰度标记后,它不会立刻进入主链路。选型在灰度池与稳定池之间做一次 1/N 抽签:
- 抽中灰度 → 只在灰度池里挑;否则只在稳定池里挑。
- 两个池子内部仍然遵守时延 / 错误 / 权重策略。
- 灰度池为空 → 直接按稳定池挑。
- 稳定池为空 → 直接按灰度池挑(整套新集群不会被抽签饿死)。
相关设置:stable_first_enabled、stable_first_denominator、stable_first_promote_requests;路由上还有 stable_first 与 stable_first_denominator 的逐路由覆盖。
七、会话粘性
同一会话内的多轮请求尽量落在同一个上游,避免上下文被拆到不同后端。
机制是一个进程内的 session key → channel 映射:
| 性质 | 行为 |
|---|---|
| 有效期 | sticky_ttl_minutes;配置不可用时回落到 30 分钟默认值 |
| 续期 | 每次成功转发都刷新,所以一段持续对话不会中途掉出窗口 |
| 容量 | 上限 10000 条,超出时淘汰最早到期的那条(最久没被续期的) |
| 命中 | 绑定的渠道仍然合格时才命中;不合格则逃逸(记录 escapes)并走正常选型 |
| 生效范围 | 全局开关 sticky_enabled + 路由级 sticky_session 覆盖 |
路由级覆盖有两种语义要分清:没设 = 跟随全局;设成 false = 这个模型明确不要粘性。两者不等价。
粘性只影响挑哪个渠道,不改变候选池——所以它不会绕过授权、冷却或分组。
八、故障转移与重试
选型与重试是一体的:代理每次尝试都从候选池里拿一个排除掉已试过渠道的新选择。
两层重试预算
| 设置 | 含义 |
|---|---|
retry_times(默认 2) | 跨渠道重试轮次上限 |
channel_retry_times(默认 1) | 同一渠道内换上游密钥再试几次 |
路由可以逐条覆盖这两个值。同一渠道在一次请求里只被尝试一次(成员级),但可以在该渠道内换密钥重试。
cross_channel_failover_enabled = false 会让请求在第一个选中的渠道之后停下,但不改变已保存的重试上限;同渠道的密钥轮换仍然启用。
什么会重试,什么不会
| 情况 | 行为 |
|---|---|
| 传输失败、408 / 429 / 5xx | 重试 |
| 上游 4xx | 也换渠道,但只冷却该成员,绝不进入渠道连续失败计数 |
生成类端点(图像 / 音频 / 视频 / 音乐 / responses) | 不重试(按字节原样转发) |
| 本地适配器 / 配置错误 | 立即返回,不重试——换个渠道也一样错 |
| 响应已经提交给客户端 | 不可能重试 |
上游 4xx 之所以也换渠道:渠道能力是异构的,一个上游拒绝的请求另一个可能接受(例如某网关支持、另一个不支持的
reasoning_effort取值)。但它不该把「请求本身有问题」记成「渠道不健康」,所以 4xx 只影响成员冷却。
别名变体的组内顺序
同一条路由上,一个逻辑模型可能有多个变体行([A] / [B] / [次])挂同一个渠道。
PreferChannel 约束负责这件事:一旦某渠道被选中,重试会在该渠道的成员里按顺序走完(限制在最高优先层、按成员 ID 升序,因此是确定性的),走完了才换到下一个渠道。
失败一个变体不会牵连同一渠道的健康兄弟——排除是成员级的(ExcludedMembers),不是渠道级。
冷却与熔断
| 机制 | 触发 | 持续 | 作用范围 |
|---|---|---|---|
| 成员冷却 | 该成员失败 | cooldown_seconds | 该路由成员 |
| 渠道熔断 / 自动禁用 | 渠道连续硬失败达阈值 | 到恢复探测通过 | 整个渠道 |
| 恢复探测 | 定时任务 | — | 把冷却或禁用中的渠道静默试回来 |
全部受 fault_protection_enabled 总开关约束;恢复探测另有 recovery_probe_enabled 与 recovery_probe_interval_seconds。
IMPORTANT
网络抖动不会直接禁用正常渠道。 偶尔超时只进入平滑冷却并在后台静默探活;只有连续硬失败且探活全部失败才触发渠道保护。
全都冷却时怎么办
如果这一层所有成员都在冷却里,选型不会直接失败,而是做最后手段选择(betterCoolingFallback):
- 优先级更高者胜;
- 然后冷却最早到期者胜;
- 最后按成员 ID 保证确定性。
这样「所有渠道都在冷却」表现为「用最快会恢复的那个」,而不是一次必然失败。
九、调用方可以给的三类约束
SelectionConstraint 由代理每次请求构造一次,并在故障转移轮次之间修改:
| 字段 | 作用 |
|---|---|
RouteGroup | 收窄到某一个成员分组 |
ExcludedMembers | 排除单个成员行(变体级排除) |
PreferChannel | 优先在该渠道内走完变体顺序 |
PinnedMemberID / PinnedChannelID | 调用方点名一个上游(控制台试调、健康探测) |
TeamAccess | 团队授权快照:模型与成员都要被授权,并按方案覆盖优先级与权重 |
点名比路由自身的单成员固定更具体:单成员路由上,被点名的行仍然可选——否则操作员刚点的那个渠道会收到一句「所选上游不可用」。
单成员模式的一个安全边角:如果被固定的成员已经被删除,这个固定就失效,路由按 auto 行为工作,流量不会被困住。同时跨渠道重试轮次被强制为 0(只有一个可用渠道,没有什么可转移的)。
十、为什么界面上看到的分数不含并发
控制台的候选分数是权重 × 时延因子 × 错误因子,刻意不含并发因子。
并发是瞬时的,不是策略信号。把它算进去会让「有效权重」随着在途请求数抖动,操作员就再也无法据此判断一个渠道的长期份额是否配置正确。
十一、排查入口
GET /console/routes/explain?model=<model>&route_group=<group>它复用同一个评估器,返回每个候选的 eligible 与 reasons,并且:
- 不发送任何上游请求;
- 不写代理日志或账单;
- 不改变任何状态。
返回里有两个分组字段要分清:requested_group 是你要的,route_group 是实际生效的;group_fallback 为真表示两者不同(回退发生了)。只有候选全部来自同一分组时 route_group 才有值——历史的全量回退池可能横跨多个分组,那时把第一个成员的分组当成整池分组是错的。
相关
- 核心概念 / 两个分组不是一回事
- 计费 —— 选中渠道之后怎么算钱
- 故障排查