Skip to content

「渠道内轮询」调度模式(roundRobinScheduling)—— 低并发场景的负载均衡 #40

Description

@bozarzza

问题

严格优先级队列在低并发流量下有个天然短板:请求串行、间隔大于单请求时长时,每次选路都命中同一个队首账号——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 行含测试与文档)。

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions