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

很多人理解熔断器,第一反应都是:
错误率超过 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 最经典的是三态模型:
逻辑很好理解。
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)
≈

浙公网安备 33010602011771号