API Key轮转、限流冷却、三态熔断器降级与半开探测
调用LLM API需要API Key,而每个Key都有速率限制(Rate Limit):
在多用户同时使用的场景下,单个Key很快就会被限流。密钥池通过管理多个Key、轮转使用、自动冷却和降级来解决这个问题。
密钥池管理架构
══════════════
┌────────────────────────────────────────────────────────────┐
│ KeyPoolManager │
│ (所有Channel池的注册中心) │
│ │
│ channel:"zhipu" → ApiKeyPool([Key1, Key2, Key3]) │
│ channel:"dashscope"→ ApiKeyPool([Key4, Key5]) │
│ channel:"deepseek" → ApiKeyPool([Key6]) │
│ │
│ 每个Pool → PooledChatModel (装饰了Key轮转+降级) │
└────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Degradation │ │ HealthProbe │ │ RuntimeState │
│ Manager │ │ (健康探测) │ │ Store(Redis)│
│ (降级管理) │ │ │ │ (分布式同步) │
└──────────────┘ └──────────────┘ └──────────────┘
每个API Key都有三种状态:
ApiKeyEntry 状态机:
获取Key
│
▼
┌─────────┐
│ ACTIVE │ ← 可用状态,正常参与轮转
└────┬────┘
│ 收到 429 (Rate Limit)
▼
┌─────────┐ 冷却时间到期
│ COOLING │ ───────────────────→ ACTIVE (自动恢复)
└────┬────┘
│ 连续失败次数 > 阈值
▼
┌──────────┐
│ DISABLED │ ← 不可用,需手动恢复或健康探测恢复
└──────────┘
每个Key还追踪:
├── callCount / successCount / failureCount
├── rateLimitCount (被限流次数)
├── totalPromptTokens / totalCompletionTokens
├── totalLatencyMs / maxLatencyMs
├── windowCallCount / windowFailureCount (滑动窗口)
└── consecutiveFailures (连续失败次数)
Round-Robin 轮询: 请求1 → Key1 ✅ 请求2 → Key2 ✅ 请求3 → Key3 ✅ 请求4 → Key1 ✅ (循环回来) 请求5 → Key2 ✅ 使用 AtomicInteger 保证线程安全 分布式模式下使用 Redis INCR 保证跨实例一致
Weighted Random 加权随机: Key1: weight=5 (50%) Key2: weight=3 (30%) Key3: weight=2 (20%) 随机数 0~10: [0,5) → Key1 [5,8) → Key2 [8,10)→ Key3 适用场景: 付费Key和免费Key混合使用
重试时可以排除已经尝试过的Key:acquire(excludeKeys: Set<String>)
PooledChatModel 同时实现了 ChatModel 和 StreamingChatModel,对上层透明:
PooledChatModel.call() 执行流程: ┌──────────────────────────────────────────────┐ │ 1. 是否为降级调用?(isFallbackCall) │ │ └── 是 → 直接 callWithRetry() (防递归) │ │ │ │ 2. 是否处于 HALF_OPEN 状态? │ │ └── 是 → callHalfOpenProbe() │ │ ┌── 成功 → recordProbeResult(true) │ │ │ → 返回结果 │ │ └── 失败 → recordProbeResult(false) │ │ → tryFallback() 降级链 │ │ │ │ 3. 正常调用 callWithRetry() │ │ maxRetries 次尝试循环: │ │ ├── pool.acquire(excludeKeys) → 获取Key │ │ ├── delegate.call(request) → 调用LLM │ │ │ │ │ ├── 成功 → markSuccess(tokens) → 返回 │ │ ├── 429 → coolKey() → 重试下一个Key │ │ └── 其他 → markFailure() → 抛出异常 │ │ │ │ 4. 所有Key耗尽? │ │ └── 是 → notifyDegradation() │ │ → tryFallback() → 沿降级链降级 │ └──────────────────────────────────────────────┘
三态熔断器状态机 (Circuit Breaker Pattern): ┌──────────────────────────────────────────────────────────┐ │ │ │ ┌──────────┐ 触发条件命中 (可多条件组合): │ │ │ CLOSED │ · Key池耗尽 (pool_exhausted) │ │ │ (正常) │ · 错误率超阈值 (error_rate) │ │ │ │ · 延迟超阈值 (latency) │ │ │ │ · 连续失败达上限 (consecutive_fail) │ │ └────┬─────┘ │ │ │ │ │ ▼ │ │ ┌───────────┐ │ │ │ OPEN │ 请求路由到降级链中第一个可用通道 │ │ │ (已熔断) │ 降级链: [channelB, channelC, ...] │ │ └────┬──────┘ │ │ │ recoveryTimeout 到期 │ │ │ + 原通道至少1个Active Key │ │ ▼ │ │ ┌───────────┐ │ │ │ HALF_OPEN │ 试探性向原通道发请求 │ │ │ (半开探测) │ 需 halfOpenSuccess / halfOpenRequests │ │ └─────┬─────┘ │ │ │ │ │ ┌────┴────┐ │ │ ▼ ▼ │ │ 成功数达标 剩余次数不够 │ │ │ │ │ │ ▼ ▼ │ │ CLOSED OPEN (60s后再次尝试半开) │ │ │ │ 循环降级保护: │ │ · 降级链中所有通道均已熔断 → 不降级,报错 │ │ · ThreadLocal isFallbackCall 防递归降级 │ └──────────────────────────────────────────────────────────┘
| 触发条件 | 配置参数 | 说明 |
|---|---|---|
| pool_exhausted | — | 所有Key均不可用(COOLING或DISABLED) |
| error_rate | errorRateThreshold / errorRateWindow | 滑动窗口内错误率超过阈值 |
| latency | latencyThresholdMs / latencyWindowSeconds | 池内平均延迟超过阈值 |
| consecutive_fail | consecutiveFailThreshold | 任一Key连续失败次数超过阈值 |
多条件之间为或逻辑,任一命中即触发熔断。可在Web UI中通过复选框自由组合。
降级链示例:
主通道 zhipu
│ 熔断触发
▼
降级链: [dashscope, deepseek, moonshot]
│
├── dashscope 未熔断? → 使用 dashscope ✅
├── dashscope 已熔断, deepseek 未熔断? → 使用 deepseek ✅
├── deepseek 已熔断, moonshot 未熔断? → 使用 moonshot ✅
└── 全部已熔断? → 循环降级保护,跳过降级,报错 ❌
半开探测流程:
OPEN 超时到期
│
├── 原通道有 Active Key? ──否──→ 推迟60s后重试
│
└── 是 → 进入 HALF_OPEN (需 2/3 成功)
│
├── 请求1: 成功 ✅ (1/2 成功, 1/3 已探测)
├── 请求2: 失败 ✗ (1/2 成功, 2/3 已探测)
└── 请求3: 成功 ✅ (2/2 成功, 3/3 已探测)
│
▼
恢复为 CLOSED ✓
| 状态转换 | 日志级别 | 日志示例 |
|---|---|---|
| CLOSED → OPEN | WARN | 渠道 [zhipu] 触发降级条件: error_rate (45.0% >= 30%) |
| OPEN → HALF_OPEN | INFO | 渠道 [zhipu] 降级超时已到期,进入 HALF_OPEN 状态 (需 2/3 请求成功) |
| 半开探测请求 | INFO | 渠道 [zhipu] 半开探测请求 成功 (进度: 1/2 成功, 1/3 已探测) |
| HALF_OPEN → CLOSED | INFO | 渠道 [zhipu] 半开探测通过 ✓ → 恢复为 CLOSED (成功 2/3) |
| HALF_OPEN → OPEN | WARN | 渠道 [zhipu] 半开探测失败 ✗ → 回退为 OPEN,60s后再次尝试半开 |
HealthProbe 定期检测:
for each ACTIVE key in channel:
│
├── 发送轻量 "Hi" 测试消息
│
├── 结果判断:
│ ├── 成功 → HEALTHY ✅
│ ├── 429限流 → RATE_LIMITED → 冷却Key (不禁用)
│ └── 其他错误 → FAILED → 禁用Key
│
└── 异常链检测:
遍历完整异常链查找 "TooManyRequests"
或 HTTP 429 标识
多实例部署时,Key状态需要跨实例同步(通过Redis):
实例A Redis 实例B │ │ │ │ acquire() → Key1 │ │ │ Key1被限流(429) │ │ │ coolKey(Key1, 60s) │ │ │ → syncEntryState() ───────→ │ SET key:status=COOLING │ │ │ SET key:coolUntil=now+60s │ │ │ │ │ │ ←── acquire() 前读取状态 ──│ │ │ Key1=COOLING │ │ │ → 跳过Key1 │ │ │ │ Key指纹: SHA-256(key)[:16] │ │ (不存储原始Key) │ │
isFallbackCall标志标记当前请求已在降级路径中,防止降级调用再次触发降级形成无限递归。