Skip to content

feat(routing): 会话首请求上游竞速——等权组内择快并绑定亲和 #234

Description

@g1331

背景

当前上游选择完全由人工配置驱动:候选集经能力匹配、密钥授权、健康/熔断过滤、并发准入后,按 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:等权同级配置已是控制面,暂不需要。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature新功能请求

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions