熔断器不只是开和关:从传统 Circuit Breaker 到 LLM Gateway 自适应熔断

image

很多人理解熔断器,第一反应都是:

错误率超过 50%
        ↓
      OPEN
        ↓
   全部请求失败

这套模型没有错,而且至今仍然非常实用。

但如果下游并没有完全挂掉,只是成功率从 99% 降到了 70% 呢?

如果它只是因为突发流量开始出现一部分 429 呢?

如果某个 LLM Provider 还能处理请求,只是 TTFT 从 500ms 突然涨到了 8 秒呢?

这时候简单地把服务定义成:

Healthy / Unhealthy

就有些粗糙了。

这也是 go-kratos/aegis 这个库比较有意思的地方:它实现的并不是一个典型的 Closed / Open / Half-Open 三态熔断器,而是一套更接近 Google SRE Adaptive Throttling 的概率熔断模型。

本文从传统 Circuit Breaker 出发,看看这种自适应熔断到底解决了什么问题,以及它为什么很适合 LLM Gateway 这类多 Provider 场景。


一、熔断器到底在保护谁?

假设 Gateway 调用一个下游服务:

Client
  │
  ▼
Gateway
  │
  ▼
Provider

如果 Provider 开始变慢:

Provider RT 上升
       │
       ▼
Gateway 请求堆积
       │
       ▼
连接数 / Goroutine / Inflight 上升
       │
       ▼
客户端 Timeout
       │
       ▼
客户端 Retry
       │
       ▼
Provider 收到更多请求
       │
       └──────────────┐
                      ▼
                 进一步恶化

真正危险的地方并不是:

某几个请求失败了。

而是:

Gateway 明明已经知道这个 Provider 大概率失败,却还在不断往里面发送请求。

Circuit Breaker 的价值就是:

尽早失败,把资源留给仍然有机会成功的请求。

因此熔断器其实主要保护的是调用方自己。

它需要避免:

下游故障
   ↓
请求持续堆积
   ↓
调用方资源耗尽
   ↓
级联故障

这和其他几个稳定性组件解决的问题并不一样:

机制 它真正解决的问题
Timeout 这一次请求最多等多久
Retry 一次偶发失败是否值得再试
Circuit Breaker 下游已经持续异常,还要不要继续打
Rate Limit 系统允许多少流量进入
Bulkhead / Inflight Limit 一个下游最多占多少资源
Fallback 当前下游不可用后找谁
Load Balance / Routing 下一次请求应该发给谁

一个稳定的 Gateway 往往不是只有一个 Circuit Breaker,而是:

Request
   │
   ▼
Rate Limit
   │
   ▼
Circuit Breaker
   │
   ▼
Inflight Limit
   │
   ▼
Timeout
   │
   ▼
Provider
   │
   ├── transient failure ──> Retry
   │
   └── unhealthy ─────────> Fallback

Circuit Breaker 只是其中的一环。


二、经典熔断器的问题:服务真的只有“开”和“关”吗?

传统 Circuit Breaker 最经典的是三态模型:

stateDiagram-v2 [*] --> Closed Closed --> Open: 错误率/失败数超过阈值 Open --> HalfOpen: 等待恢复时间 HalfOpen --> Closed: Probe 成功 HalfOpen --> Open: Probe 失败

逻辑很好理解。

Closed

正常放行请求,同时统计失败率。

Open

认为下游已经不健康,快速失败,不再访问下游。

Half-Open

等待一段时间后,允许少量 Probe 请求过去。

Probe 成功:

Half-Open -> Closed

Probe 失败:

Half-Open -> Open

Sony 的 gobreaker 就是非常典型的实现。目前 v2 支持 MaxRequests、Timeout、ReadyToTrip、Rolling Window,以及 IsSuccessful、IsExcluded 等错误分类能力。

这套模型最大的优势是:

简单、确定、容易理解。

但它存在一个天然的问题。

假设规则是:

Error Rate >= 50%
        ↓
       Open

那么:

49% Error Rate
      │
      ▼
   全部放行


51% Error Rate
      │
      ▼
   全部拒绝

错误率只变化了 2%,系统行为却发生了巨大变化。

可以把它理解成一种:

Threshold Cliff,阈值悬崖。

更重要的是,真实世界中的服务健康状态往往不是:

正常
 │
 ▼
死亡

而更像:

99%
 │
 ▼
95%
 │
 ▼
80%
 │
 ▼
60%
 │
 ▼
30%
 │
 ▼
慢慢恢复

对于这种“部分退化”,一个只有 0 和 1 的熔断模型未必是最好的选择。


三、Aegis 的核心:把熔断从开关变成概率

go-kratos/aegis 是 Kratos 项目中的 Service Reliability Algorithm 库,当前仓库包含 Circuit Breaker 和 Rate Limit 等实现。

它的 Circuit Breaker 接口非常简单:

type CircuitBreaker interface {
    Allow() error
    MarkSuccess()
    MarkFailed()
}

这就是完整接口。

一次调用:

Request
   │
   ▼
 Allow()
   │
   ├── Reject
   │
   └── Allow
          │
          ▼
       Provider
        /    \
       /      \
 Success     Failure
    │           │
    ▼           ▼
MarkSuccess MarkFailed

真正值得看的,是它的 sre.go。

当前默认参数是:

success: 0.6
request: 100
bucket:  10
window:  3 * time.Second

内部通过 Rolling Counter 维护统计窗口,并计算:

k = 1 / success

所以默认:

K ≈ 1.667

核心逻辑可以简化成下面几行:

accepts, total := b.summary()

requests := b.k * float64(accepts)

if total < b.request || float64(total) < requests {
    return nil
}

dropRatio := math.Max(
    0,
    (float64(total)-requests)/float64(total+1),
)

if random() < dropRatio {
    return ErrNotAllowed
}

这正是 Aegis 和传统 Breaker 最大的区别。当前源码中的默认窗口、最小请求数以及概率拒绝公式都可以直接在 sre.go 中看到。

如果重新给变量起一个更容易理解的名字:

R = 最近窗口总请求数
A = 最近窗口成功数
K = 容忍系数

那么拒绝概率大致就是:

                R - K × A
P(drop) = max(0, ---------)
                  R + 1

也就是说:

服务成功率越低,Gateway 主动拒绝请求的概率越高。


用几个数字就能看懂

为了方便理解,假设:

K = 2

当:

100 个请求
90 个成功

那么:

100 < 2 × 90

P(drop) = 0

全部放行。

如果:

100 个请求
40 个成功

则:

P(drop)

= (100 - 2 × 40) / 101

≈ 19.8%

大约:

100 Request
     │
     ▼
 Circuit Breaker
   /          \
约20%         约80%
Reject        Allow
                │
                ▼
             Provider

如果情况继续恶化:

100 个请求
10 个成功

则:

P(drop)

≈
posted @ 2026-09-11 23:08  alin_qu  阅读(29)  评论(0)    收藏  举报