05 - 密钥池管理

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)│
  │  (降级管理)  │          │              │    │  (分布式同步) │
  └──────────────┘          └──────────────┘    └──────────────┘

三、ApiKeyEntry — 密钥状态机

每个API Key都有三种状态:

ApiKeyEntry 状态机:

           获取Key
             │
             ▼
        ┌─────────┐
        │  ACTIVE  │ ← 可用状态,正常参与轮转
        └────┬────┘
             │ 收到 429 (Rate Limit)
             ▼
        ┌─────────┐        冷却时间到期
        │ COOLING  │ ───────────────────→ ACTIVE (自动恢复)
        └────┬────┘
             │ 连续失败次数 > 阈值
             ▼
        ┌──────────┐
        │ DISABLED  │ ← 不可用,需手动恢复或健康探测恢复
        └──────────┘

  每个Key还追踪:
  ├── callCount / successCount / failureCount
  ├── rateLimitCount (被限流次数)
  ├── totalPromptTokens / totalCompletionTokens
  ├── totalLatencyMs / maxLatencyMs
  ├── windowCallCount / windowFailureCount (滑动窗口)
  └── consecutiveFailures (连续失败次数)

四、Key轮转策略

策略一:Round-Robin(轮询)

Round-Robin 轮询:

  请求1 → Key1 ✅
  请求2 → Key2 ✅
  请求3 → Key3 ✅
  请求4 → Key1 ✅ (循环回来)
  请求5 → Key2 ✅

  使用 AtomicInteger 保证线程安全
  分布式模式下使用 Redis INCR 保证跨实例一致

策略二:Weighted Random(加权随机)

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 — 透明的Key轮转与降级

PooledChatModel 同时实现了 ChatModelStreamingChatModel,对上层透明:

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() → 沿降级链降级      │
  └──────────────────────────────────────────────┘

六、DegradationManager — 三态熔断器

三态熔断器状态机 (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_rateerrorRateThreshold / errorRateWindow滑动窗口内错误率超过阈值
latencylatencyThresholdMs / latencyWindowSeconds池内平均延迟超过阈值
consecutive_failconsecutiveFailThreshold任一Key连续失败次数超过阈值

多条件之间为或逻辑,任一命中即触发熔断。可在Web UI中通过复选框自由组合。

降级链(Fallback Chain)

降级链示例:

  主通道 zhipu
    │ 熔断触发
    ▼
  降级链: [dashscope, deepseek, moonshot]
    │
    ├── dashscope 未熔断? → 使用 dashscope ✅
    ├── dashscope 已熔断, deepseek 未熔断? → 使用 deepseek ✅
    ├── deepseek 已熔断, moonshot 未熔断? → 使用 moonshot ✅
    └── 全部已熔断? → 循环降级保护,跳过降级,报错 ❌

半开探测(Half-Open Probe)

核心机制:OPEN 超时后不直接恢复,而是进入 HALF_OPEN 状态,将部分真实请求路由到原通道试探。探测成功数达标后才恢复为 CLOSED,失败则回退 OPEN。
半开探测流程:

  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 → OPENWARN渠道 [zhipu] 触发降级条件: error_rate (45.0% >= 30%)
OPEN → HALF_OPENINFO渠道 [zhipu] 降级超时已到期,进入 HALF_OPEN 状态 (需 2/3 请求成功)
半开探测请求INFO渠道 [zhipu] 半开探测请求 成功 (进度: 1/2 成功, 1/3 已探测)
HALF_OPEN → CLOSEDINFO渠道 [zhipu] 半开探测通过 ✓ → 恢复为 CLOSED (成功 2/3)
HALF_OPEN → OPENWARN渠道 [zhipu] 半开探测失败 ✗ → 回退为 OPEN,60s后再次尝试半开

七、HealthProbe — 健康探测

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)                │                            │

九、面试高频问题

Q: Key被限流后怎么处理?
A: 三级处理:1) 冷却(COOLING):Key被设为COOLING状态,冷却期(默认60秒)内不参与轮转,到期自动恢复ACTIVE;2) 轮转重试:当前Key被限流后,自动切换到下一个可用Key,重试时排除已限流的Key;3) 通道熔断:所有Key都不可用时,DegradationManager通过三态熔断器(CLOSED→OPEN→HALF_OPEN→CLOSED)自动将请求路由到降级链中的备用通道(如zhipu→dashscope→deepseek),对上层完全透明。
Q: 降级系统采用什么模式?与简单的主备切换有什么区别?
A: 采用业界标准的三态熔断器模式(Circuit Breaker),有三个状态:CLOSED(正常)、OPEN(已熔断,请求走降级链)、HALF_OPEN(半开探测)。与简单主备切换的核心区别在于:1) 多触发条件可组合——支持Key池耗尽、错误率、延迟、连续失败4种条件的任意组合,不是二选一;2) 降级链而非单一备用通道——配置有序的降级目标列表,逐级降级;3) 半开探测恢复——OPEN超时后不直接恢复,而是进入HALF_OPEN状态用真实请求试探原通道,探测成功数达标才恢复,避免雪崩式回切。
Q: 如何防止循环降级?
A: 两层保护:1) DegradationManager在执行降级前会遍历降级链,检查每个目标通道的状态——如果降级链中所有通道都已处于OPEN或HALF_OPEN状态,就不会降级,而是直接报错并记录日志;2) PooledChatModel使用ThreadLocal isFallbackCall标志标记当前请求已在降级路径中,防止降级调用再次触发降级形成无限递归。
Q: 半开探测是怎么工作的?
A: OPEN状态超时到期后,先检查原通道是否至少有1个ACTIVE状态的Key(如果没有则推迟60秒重试)。若有,则进入HALF_OPEN状态,配置探测请求数(默认3)和需成功数(默认2)。在HALF_OPEN期间,PooledChatModel会将真实用户请求路由到原通道,成功则记录一次探测成功,失败则走降级链同时记录探测失败。当成功数达标则恢复CLOSED;当剩余次数已不可能凑够成功数则回退OPEN,60秒后再次尝试。所有探测过程都有详细日志记录进度。
Q: 为什么用SHA-256指纹而不是存储原始Key?
A: 安全考虑。Redis是共享存储,如果直接存储原始API Key,一旦Redis被攻破,所有Key都会泄露。使用SHA-256截断后的16位hex指纹,可以唯一标识Key但不暴露原始值。