背景
当前上游选择完全由人工配置驱动:候选集经能力匹配、密钥授权、健康/熔断过滤、并发准入后,按 priority 分层、层内按 weight 加权随机选一个。系统不感知上游的实时快慢,个别请求卡在慢上游时只能等超时后 failover。
请求日志中已有完整的耗时数据,但仅用于事后报表,未参与选路决策。
方案:首请求竞速 + 亲和绑定
利用现有会话亲和性机制(session-affinity.ts,绑定键为 (apiKeyId, capability, sessionId))的 miss/hit 分叉作为竞速触发点:
请求进来 → 查亲和性
├─ 命中 → 直连绑定上游(会话内绝大多数请求,零额外成本)
└─ 未命中 → N 路并发竞速 → 胜者响应并写入绑定 → 本会话后续直连胜者
胜者是实测选出的“此刻对该会话最快的上游”,等于每个会话开头免费做了一次真实探测,且后续请求命中胜者的 prompt cache。
设计决策
| 决策点 |
结论 |
| 触发时机 |
亲和性 miss 且能提取出 sessionId(提不出的请求走原有选路,防止一次性调用每次都竞速) |
| 参赛资格 |
先按现有逻辑加权随机选出“种子”,与种子同优先级且同权重的候选构成竞速组 |
| 参赛人数 |
默认 2,上限 3,组内随机抽取;组内只有种子自身时不竞速,直连 |
| 胜负判定 |
首字节到达且状态码非 5xx;输家立即 abort 流 |
| 绑定 |
胜者写入现有亲和性绑定(TTL 语义不变:滑动 5 分钟 / 绝对 30 分钟) |
| 开关 |
全局开关,默认开启 |
| 记录 |
全部参赛路计入请求日志与计费快照(复用 failoverHistory 结构,竞速≈并行版 failover 首跳) |
参赛资格的语义依据
priority 表达“我更想用谁”,weight 表达“我要谁多承担”——竞速若跨层或跨权重,会用“谁快用谁”推翻这些人工意志。同优先级 + 同权重是配置中唯一“毫无偏好声明”的等价组,现有实现对它们用纯随机;把纯随机换成实测竞速,是不覆盖任何人工配置的升级。
由此天然获得细粒度控制面:想让哪些上游竞速就配成等权同级;不想让某个贵上游参赛,挪 1 点权重即可退出。无需逐上游开关与新增 UI。
种子选组机制保证跨组流量分配比例完全不变(weight 语义原样保留),竞速只发生在等权组内部。
成本与已知取舍
- 竞速不是严格“每会话一次”:亲和绑定 30 分钟到期或空闲超 5 分钟失效后会重新竞速,频率仍然很低。
- 输家上游照付首轮输入 token 费用(或消耗订阅账号一次请求额度),故 N 默认 2、上限 3。
- 等权组内 weight 的分流作用会部分失效(谁快谁赢)。存在一定自平衡:胜者负载升高变慢后开始输,流量自然溢出。额度型限制(如订阅 5 小时窗)不体现在延迟上,耗尽报错后由现有 failover 兜住。
- 已把多个按量付费上游配成等权的存量用户,升级后首轮 token 费会随竞速翻倍,需在发版说明中告知(默认开启的告知义务)。
边界与注意点
- 竞速期间并发槽位短暂占用 N 个,需与队列准入机制正确交互(并发满的候选本就已被过滤出候选集)。
- 胜负判定后输家 abort 必须及时,避免继续产生输出 token。
- 下游响应与错误体不得携带上游身份信息(沿用现有约束)。
不做的事
- 全量竞速(每请求 N 路):成本 ×N,不做;本方案阈值语义上是其安全子集。
- 对冲请求(hedged requests,绑定上游中途卡住时补发备胎):与竞速互补,另行考虑。
- 逐上游“参与竞速”开关与 UI:等权同级配置已是控制面,暂不需要。
背景
当前上游选择完全由人工配置驱动:候选集经能力匹配、密钥授权、健康/熔断过滤、并发准入后,按 priority 分层、层内按 weight 加权随机选一个。系统不感知上游的实时快慢,个别请求卡在慢上游时只能等超时后 failover。
请求日志中已有完整的耗时数据,但仅用于事后报表,未参与选路决策。
方案:首请求竞速 + 亲和绑定
利用现有会话亲和性机制(
session-affinity.ts,绑定键为(apiKeyId, capability, sessionId))的 miss/hit 分叉作为竞速触发点:胜者是实测选出的“此刻对该会话最快的上游”,等于每个会话开头免费做了一次真实探测,且后续请求命中胜者的 prompt cache。
设计决策
参赛资格的语义依据
priority 表达“我更想用谁”,weight 表达“我要谁多承担”——竞速若跨层或跨权重,会用“谁快用谁”推翻这些人工意志。同优先级 + 同权重是配置中唯一“毫无偏好声明”的等价组,现有实现对它们用纯随机;把纯随机换成实测竞速,是不覆盖任何人工配置的升级。
由此天然获得细粒度控制面:想让哪些上游竞速就配成等权同级;不想让某个贵上游参赛,挪 1 点权重即可退出。无需逐上游开关与新增 UI。
种子选组机制保证跨组流量分配比例完全不变(weight 语义原样保留),竞速只发生在等权组内部。
成本与已知取舍
边界与注意点
不做的事