高可用调度:轮换、固定账号、粘性会话与失败重试
你把 Antigravity Tools 当成本地 AI 网关用了一段时间后,基本都会撞到同一个问题:账号越少越容易 429/401/invalid_grant,账号越多越容易"哪个账号在干活"说不清,缓存命中率也会掉。
这节课就把调度这块讲清楚:它到底怎么选账号、什么叫"粘性会话"、什么时候会强制轮换、以及如何用"固定账号模式"把调度变成可控的。
学完你能做什么
- 看懂 Antigravity Tools 的 3 种调度模式在真实请求里各做了什么
- 知道"会话指纹(session_id)"怎么生成,以及它怎么影响粘性调度
- 在 GUI 里启用/关闭"固定账号模式",并理解它会覆盖哪些调度逻辑
- 遇到 429/5xx/invalid_grant 时,知道系统会怎么标记限流、怎么重试、什么时候轮换
你现在的困境
- Claude Code 或 OpenAI SDK 跑着跑着突然 429,一重试就换账号,缓存命中率下滑
- 多个客户端并发跑任务,经常"互相踩掉"对方的账号状态
- 你想排障,但不知道当前请求是哪个账号在服务
- 你只想用某个"最稳的账号",但系统总在轮换
什么时候用这一招
- 你需要把"稳定性(少报错)"和"缓存命中(同账号)"做权衡
- 你想让同一条对话尽量复用同一个账号(减少 Prompt Caching 抖动)
- 你要做灰度/排障,想把所有请求都固定到一个账号
🎒 开始前的准备
- 至少准备 2 个可用账号(账号池越小,轮换空间越小)
- 反代服务已启动(在「API Proxy」页面能看到 Running 状态)
- 你知道配置文件在哪里(如果你需要手动改配置)
先把配置系统这课补上
如果你还不熟 gui_config.json 和哪些配置能热更新,先看 配置全解:AppConfig/ProxyConfig、落盘位置与热更新语义。
核心思路:一次请求会经过哪几层"调度"
调度不是一个"单独的开关",而是几层机制叠在一起:
- SessionManager 先给请求打一个会话指纹(session_id)
- Handlers 每次重试都会要求 TokenManager 强制轮换(
attempt > 0) - TokenManager 再根据:固定账号 → 粘性会话 → 60s 窗口 → 轮询 选出账号
- 遇到 429/5xx 时会记录限流信息,后续选账号会主动跳过限流账号
什么是"会话指纹(session_id)"?
会话指纹就是一个"尽量稳定的 Session Key",用来把同一段对话的多次请求绑在同一账号上。
在 Claude 请求里,优先级是:
metadata.user_id(客户端显式传入,且非空且不含"session-"前缀)- 第一条"足够长"的 user 消息做 SHA256 哈希,然后截断成
sid-xxxxxxxxxxxxxxxx
对应实现:src-tauri/src/proxy/session_manager.rs(Claude/OpenAI/Gemini 都有各自的提取逻辑)。
小细节:为什么只看第一条 user 消息?
源码里明确写了"只哈希第一条用户消息内容,不混入模型名称或时间戳",目标是让同一对话的多轮请求尽量生成相同的 session_id,从而提高缓存命中率。
TokenManager 选账号的优先级
TokenManager 的核心入口是:
TokenManager::get_token(quota_group, force_rotate, session_id, target_model)
它做的事可以按优先级理解:
- 固定账号模式(Fixed Account):如果在 GUI 中启用了"固定账号模式"(运行时设置),且该账号没被限流、也没被配额保护,就直接用它。
- 粘性会话(Session Binding):如果有
session_id且调度模式不是PerformanceFirst,会优先复用该 session 绑定的账号。 - 60s 全局窗口复用:如果没传
session_id(或还没绑定成功),在非PerformanceFirst下会尽量在 60 秒内复用"上一次用过的账号"。 - 轮询(Round-robin):以上都不适用时,按一个全局自增索引轮询选择账号。
此外还有两条"隐形规则",很影响体感:
- 账号会先排序:ULTRA > PRO > FREE,同 tier 内优先剩余配额高的账号。
- 失败或限流会被跳过:已尝试失败的账号会进入
attempted集合;被限流标记的账号会被跳过。
3 种调度模式到底差在哪
在配置里你会看到:CacheFirst / Balance / PerformanceFirst。
以"后端 TokenManager 真实分支"为准,它们的关键差异只有一个:是否启用粘性会话 + 60s 窗口复用。
PerformanceFirst:跳过粘性会话与 60s 窗口复用,直接走轮询(并继续跳过限流/配额保护账号)。CacheFirst/Balance:都会启用粘性会话与 60s 窗口复用。
关于 max_wait_seconds
前端/配置结构里有 max_wait_seconds,并且 UI 只在 CacheFirst 下允许调整。但目前后端调度逻辑只基于 mode 分支,并没有读取 max_wait_seconds。
失败重试与"强制轮换"怎么联动
在 OpenAI/Gemini/Claude 的 handler 里,都会用类似下面的模式处理重试:
- 第 1 次尝试:
force_rotate = false - 第 2 次及以后:
force_rotate = true(attempt > 0),TokenManager 会跳过粘性复用,直接找下一个可用账号
遇到 429/529/503/500 等错误时:
- handler 会调用
token_manager.mark_rate_limited(...)把这个账号记录为"限流/过载",后续调度会主动跳过它。 - OpenAI 兼容路径还会尝试从错误 JSON 里解析
RetryInfo.retryDelay或quotaResetDelay,等待一小段时间再继续重试。
跟我做:把调度调到"可控"
第 1 步:先确认你真的有"账号池"
为什么 调度再高级,池子里只有 1 个账号也没得选。很多"轮换不生效/粘性没感觉"的根因都是账号太少。
操作 打开「Accounts」页面,确认至少有 2 个账号处于可用状态(不是 disabled / proxy disabled)。
你应该看到:至少 2 个账号能正常刷新配额,并且反代启动后 active_accounts 不为 0。
第 2 步:在 GUI 里选择调度模式
为什么 调度模式决定了"同一段对话"到底是尽量复用同一账号,还是每次都轮询。
操作 进入「API Proxy」页面,找到 "Account Scheduling & Rotation" 卡片,选择其中一个模式:
Balance:推荐默认值。大多数情况下更稳(会话粘性 + 失败时轮换)。PerformanceFirst:并发高、任务短、你更在意吞吐而不是缓存时选它。CacheFirst:如果你希望"对话尽量固定账号",可以选它(当前后端与Balance的行为差异很小)。
如果你要手动改配置,对应片段是:
{
"proxy": {
"scheduling": {
"mode": "Balance",
"max_wait_seconds": 60
}
}
}你应该看到:切换模式后会立即写入 gui_config.json,反代服务运行时直接生效(无需重启)。
第 3 步:启用"固定账号模式"(把轮换关掉)
为什么 排障、灰度、或者你想把某个账号"钉死"给某个客户端用时,固定账号模式是最直接的手段。
操作 在同一张卡片里打开 "Fixed Account Mode",然后在下拉框里选择账号。
别忘了:这个开关只在反代服务 Running 时可用。
你应该看到:后续请求都会优先使用这个账号;如果它被限流或被配额保护,会回退到轮询。
固定账号是运行时设置
固定账号模式是运行时状态(通过 GUI 或 API 动态设置),不会持久化到 gui_config.json。重启反代服务后,固定账号会恢复为空(回到轮询模式)。
第 4 步:需要的时候清掉"会话绑定"
为什么 粘性会话会把 session_id -> account_id 记在内存里。如果你在同一台机器上做不同实验(比如切换账号池、切换模式),旧绑定可能会干扰你观察。
操作 在 "Account Scheduling & Rotation" 卡片右上角点 "Clear bindings"。
你应该看到:旧会话会重新分配账号(下一个请求会重新绑定)。
第 5 步:用响应头确认"到底是哪个账号在服务"
为什么 你想验证调度是否符合预期,最靠谱的办法是拿到服务端返回的"当前账号标识"。
操作 对 OpenAI 兼容端点发一个非流式请求,然后观察响应头里的 X-Account-Email。
# 例子:最小化的 OpenAI Chat Completions 请求
# 注意:model 必须是你当前配置里可用/可路由的模型名
curl -i "http://127.0.0.1:8045/v1/chat/completions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-REPLACE_ME" \
-d '{
"model": "gemini-3-pro-high",
"stream": false,
"messages": [{"role": "user", "content": "hello"}]
}'你应该看到:响应头里出现类似下面的内容(示例):
X-Account-Email: example@gmail.com
X-Mapped-Model: gemini-3-pro-high检查点 ✅
- 你能说清楚
fixed account、sticky session、round-robin三个机制谁覆盖谁 - 你知道
session_id是怎么来的(优先metadata.user_id,否则哈希首条 user 消息) - 你遇到 429/5xx 时能预期:系统会先记录限流,再换账号重试
- 你能用
X-Account-Email验证当前请求到底是哪个账号在服务
踩坑提醒
账号池只有 1 个时,不要期待"轮换能救你" 轮换只是"换另一个账号",池子里没第二个账号时,429/invalid_grant 只会更频繁暴露。
CacheFirst不是"永远等到可用" 当前后端调度逻辑遇到限流会倾向于解绑并切换账号,而不是长期阻塞等待。固定账号不是绝对强制 如果固定账号被标记为限流、或被配额保护命中,系统会回退到轮询。
本课小结
- 调度链路:handler 提取
session_id→TokenManager::get_token选账号 → 出错时attempt > 0强制轮换 - 你最常用的两个开关:调度模式(是否启用粘性/60s 复用)+ 固定账号模式(直接指定账号)
- 429/5xx 会被记录成"限流状态",后续调度会跳过该账号,直到锁定时间过期
下一课预告
下一课我们看 模型路由:当你希望对外暴露"稳定的模型集合",以及想做通配符/预设策略时,该怎么配置与排查。
附录:源码参考
点击展开查看源码位置
更新时间:2026-01-23
| 功能 | 文件路径 | 行号 |
|---|---|---|
| 调度模式与配置结构(StickySessionConfig) | src-tauri/src/proxy/sticky_config.rs | 1-36 |
| 会话指纹生成(Claude/OpenAI/Gemini) | src-tauri/src/proxy/session_manager.rs | 1-159 |
| TokenManager:固定账号模式字段与初始化 | src-tauri/src/proxy/token_manager.rs | 27-50 |
| TokenManager:选账号核心逻辑(固定账号/粘性会话/60s 窗口/轮询/配额保护) | src-tauri/src/proxy/token_manager.rs | 470-940 |
| TokenManager:invalid_grant 自动禁用并移出池 | src-tauri/src/proxy/token_manager.rs | 868-878 |
| TokenManager:限流记录与成功清理 API | src-tauri/src/proxy/token_manager.rs | 1087-1147 |
| TokenManager:更新调度配置 / 清理会话绑定 / 固定账号模式 setter | src-tauri/src/proxy/token_manager.rs | 1419-1461 |
| ProxyConfig:scheduling 字段定义与默认值 | src-tauri/src/proxy/config.rs | 174-257 |
| 反代启动时同步 scheduling 配置 | src-tauri/src/commands/proxy.rs | 70-100 |
| 调度相关的 Tauri 命令(get/update/clear bindings/fixed account) | src-tauri/src/commands/proxy.rs | 478-551 |
| OpenAI handler:session_id + 重试时强制轮换 | src-tauri/src/proxy/handlers/openai.rs | 160-182 |
| OpenAI handler:429/5xx 记录限流 + 解析 retry delay | src-tauri/src/proxy/handlers/openai.rs | 349-367 |
| Gemini handler:session_id + 重试时强制轮换 | src-tauri/src/proxy/handlers/gemini.rs | 62-88 |
| Gemini handler:429/5xx 记录限流并轮换 | src-tauri/src/proxy/handlers/gemini.rs | 279-299 |
| Claude handler:提取 session_id 并传给 TokenManager | src-tauri/src/proxy/handlers/claude.rs | 517-524 |
| 429 retry delay 解析(RetryInfo.retryDelay / quotaResetDelay) | src-tauri/src/proxy/upstream/retry.rs | 37-66 |
| 限流原因识别与指数退避(RateLimitTracker) | src-tauri/src/proxy/rate_limit.rs | 154-279 |
关键结构体:
StickySessionConfig:调度模式与配置结构(mode,max_wait_seconds)TokenManager:账号池、会话绑定、固定账号模式、限流跟踪器SessionManager:从请求中提取session_id