问题
严格优先级队列在低并发流量下有个天然短板:请求串行、间隔大于单请求时长时,每次选路都命中同一个队首账号——10 个号、每个 maxConcurrent=1、实际并发恒为 1 时,2~10 号几乎不会被调用。负载、额度消耗与风控风险全部压在 1 号身上(先把 1 号打到限流/风控才轮到 2 号)。号池场景里这更像 failover 队列,不像 load-balanced pool。
方案
新增调度模式开关 roundRobinScheduling(默认关,原优先级行为逐字保留):
- 判据完全共用:启用 / 该模型未限额(真名冷却键)/ 未达
maxConcurrent / 未被本次请求尝试过——两种模式只差「池内取谁」;三级兜底(恢复最早 / 余量挤占 / 全禁用 503)不动
- 候选按渠道分组:渠道之间仍按优先级排先后(failover 语义不变),最高优先级渠道的候选池内从「上一个选中者的下一位」开始轮转
- 轮询粒度 = 单渠道×单模型(游标键
provider×请求名),不跨渠道均衡
- 游标存「上次选中者 id」而非下标:候选池随禁用/冷却/并发动态进出,id 天然表达「从谁的下一位继续」,跳过者恢复后自动回轮,无需登记
- Consume/Peek 两口径:转发选路与换号顺延消耗轮次;★ 推算(
/api/session 的 routedAccountId)与排障快照只窥视不拨游标——查面板不能改变转发的负载均衡
- 开关走
GET/POST /api/config(roundRobinScheduling 布尔),逐请求读快照,改完下一个请求生效
核心选路(routing.rs 的 pick_account_by_priority 尾部):
candidates.sort_by(compare_by_priority);
let Some(turn) = rr else {
return candidates.into_iter().next(); // 优先级模式:原行为逐字保留
};
// 轮询模式:排序后首位所属渠道 = 本轮轮询渠道(failover 语义不变)
let pool_provider = provider_of(&candidates[0]).to_string();
let pool: Vec<Value> = candidates
.iter()
.filter(|account| provider_of(account) == pool_provider)
.cloned()
.collect();
let key = format!("{}×{}", pool_provider, keys.requested());
rr_next(&pool, &key, turn)
rr_next:进程级游标 Mutex<HashMap<渠道×模型, 上次选中 id>>,取上次选中者的下一位(回绕;不在池内则池首);Consume 写游标,Peek 只读。
验证
- 单测 10 项:轮转/回绕、并发跳过、限额跳过与恢复、窥视不消耗、渠道分组与整池 failover、按模型独立、exclude/禁用;全套 263 项通过
- 实测(30 号池,全部
maxConcurrent=1):开轮询后 6 连发 → 6 个不同账号各扛 1 个;同一批号在优先级模式下是队首连吃 4 个
如果方向 OK,我可以提 PR(改动集中在 routing.rs / upstream/rotate.rs / config 三处,约 500 行含测试与文档)。
问题
严格优先级队列在低并发流量下有个天然短板:请求串行、间隔大于单请求时长时,每次选路都命中同一个队首账号——10 个号、每个
maxConcurrent=1、实际并发恒为 1 时,2~10 号几乎不会被调用。负载、额度消耗与风控风险全部压在 1 号身上(先把 1 号打到限流/风控才轮到 2 号)。号池场景里这更像 failover 队列,不像 load-balanced pool。方案
新增调度模式开关
roundRobinScheduling(默认关,原优先级行为逐字保留):maxConcurrent/ 未被本次请求尝试过——两种模式只差「池内取谁」;三级兜底(恢复最早 / 余量挤占 / 全禁用 503)不动provider×请求名),不跨渠道均衡/api/session的routedAccountId)与排障快照只窥视不拨游标——查面板不能改变转发的负载均衡GET/POST /api/config(roundRobinScheduling布尔),逐请求读快照,改完下一个请求生效核心选路(
routing.rs的pick_account_by_priority尾部):rr_next:进程级游标Mutex<HashMap<渠道×模型, 上次选中 id>>,取上次选中者的下一位(回绕;不在池内则池首);Consume写游标,Peek只读。验证
maxConcurrent=1):开轮询后 6 连发 → 6 个不同账号各扛 1 个;同一批号在优先级模式下是队首连吃 4 个如果方向 OK,我可以提 PR(改动集中在
routing.rs/upstream/rotate.rs/config三处,约 500 行含测试与文档)。