核心指标定义:成功率、错误率、RT阈值与滑动窗口
好,咱们正式开始。这一节我要讲的是熔断机制里最基础、也最要命的东西——指标定义。
你想想看,一个系统要不要熔断,总得有个判断依据吧?不能拍脑袋说「我觉得它快挂了,先断一下」。那依据什么呢?就是三个核心指标:成功率、错误率、响应时间(RT)。再加上一个统计模型——滑动窗口。
我个人习惯,先把这几个指标的定义敲死,再谈阈值怎么设。不然你后面代码写得再漂亮,指标算错了,全是白搭。
1. 成功率与错误率
先说成功率。这个最简单,也最容易踩坑。
成功率 = 成功请求数 / 总请求数
嗯,公式看着简单。但什么叫「成功」?
我在项目中遇到过一个问题:业务方说「我接口返回了200,就算成功」。结果呢?返回200了,但业务逻辑其实失败了——比如余额不足,但HTTP状态码还是200。这种算成功吗?
错误率就更直接了:错误率 = 错误请求数 / 总请求数。但这里有个细节——超时算不算错误?
我个人认为,超时应该算错误。因为对调用方来说,超时和报错没区别,都是拿不到结果。你想想看,用户等5秒得到一个超时,跟等1秒得到一个500,哪个体验更差?
所以我的做法是:
- 业务成功码 → 成功
- 业务失败码 → 错误
- 超时 → 错误
- 网络异常 → 错误
- 限流拒绝 → 这个不算错误,算「被保护」
2. 响应时间(RT)阈值
RT阈值,说白了就是「你觉得多慢算慢」。
这个没有标准答案。我见过有的团队拍脑袋设个500ms,结果线上天天熔断。为什么?因为他们的接口本身就要300ms,再加上网络抖动,很容易就超了。
我的建议是:RT阈值应该基于P99或者P95的延迟来设。
什么意思?就是看过去一段时间内,99%的请求都在多少毫秒内完成。然后把这个值乘以1.5或2,作为熔断阈值。
举个例子:
| 分位 | 延迟 |
|---|---|
| P50 | 120ms |
| P90 | 250ms |
| P99 | 480ms |
那我会把RT阈值设在700ms~1s之间。为什么?因为P99是480ms,说明正常情况下99%的请求都在半秒内。设到1s,给足了余量。如果超过1s,那肯定是系统出问题了。
3. 滑动窗口统计模型
好,指标定义清楚了。但怎么统计呢?
你不能说「从系统启动到现在,成功率是多少」。因为系统可能已经跑了三天三夜,前面的数据早没参考价值了。我们要的是最近一段时间的指标。
这就是滑动窗口的用武之地。
滑动窗口,说白了就是一个固定大小的、不断移动的时间窗口。比如窗口大小是10秒,那每过1秒,窗口就往前滑1秒,丢掉最旧的那1秒数据,加入最新的1秒数据。
我画个图你就明白了:
看到没?整个窗口被切成了10个1秒的子桶。每个桶里记录这1秒内的请求数、成功数、失败数、总RT。
当新的一秒到来时:
- 创建一个新桶,放到最右边
- 丢掉最左边那个旧桶
- 重新计算窗口内所有桶的汇总数据
这样,你任何时候查「当前成功率」,都是最近10秒的真实数据。
4. 代码实现思路
光说不练假把式。我贴一段伪代码,你看看滑动窗口在Go里怎么实现:
type Bucket struct {
Success int64
Failure int64
TotalRT int64 // 总响应时间,用于算平均RT
Count int64 // 总请求数
}
type SlidingWindow struct {
buckets []*Bucket
size int // 窗口大小(桶数量)
interval int64 // 每个桶的时间间隔(毫秒)
lastTime int64 // 上次滑动的时间
}
func (sw *SlidingWindow) Record(success bool, rt int64) {
now := time.Now().UnixMilli()
// 检查是否需要滑动
if now - sw.lastTime >= sw.interval {
sw.slide(now)
}
// 获取当前桶
currentBucket := sw.buckets[len(sw.buckets)-1]
currentBucket.Count++
currentBucket.TotalRT += rt
if success {
currentBucket.Success++
} else {
currentBucket.Failure++
}
}
func (sw *SlidingWindow) slide(now int64) {
// 丢掉最旧的桶
sw.buckets = sw.buckets[1:]
// 新建一个空桶
sw.buckets = append(sw.buckets, &Bucket{})
sw.lastTime = now
}
func (sw *SlidingWindow) GetSuccessRate() float64 {
totalSuccess := int64(0)
totalCount := int64(0)
for _, b := range sw.buckets {
totalSuccess += b.Success
totalCount += b.Count
}
if totalCount == 0 {
return 1.0 // 没有请求,默认成功
}
return float64(totalSuccess) / float64(totalCount)
}
这段代码看着简单,但有几个坑:
- 并发安全: 上面没加锁。实际生产环境要用atomic或者sync.RWMutex。我习惯用atomic,性能更好。
- 时间对齐: 滑动要按整秒对齐,不能每次请求都去滑动。否则高并发下性能会崩。
- 内存分配: 每次slide都创建新桶,GC压力大。可以预分配环形数组,避免频繁分配。
5. 阈值怎么定?
指标定义清楚了,滑动窗口也搭好了。最后一步:阈值设多少?
这个真没有标准答案。但我给你一个参考范围:
| 指标 | 建议阈值 | 说明 |
|---|---|---|
| 成功率 | < 80% 触发熔断 | 低于80%说明系统已经不太行了 |
| 错误率 | > 20% 触发熔断 | 和成功率对应,二选一即可 |
| RT阈值 | P99 × 1.5 ~ 2 | 具体看业务容忍度 |
| 最小请求数 | ≥ 10次/窗口 | 请求太少时,统计无意义 |
注意最后一行——最小请求数。这个很容易被忽略。你想想看,如果窗口内只有1个请求,还失败了,那错误率就是100%。但你能说系统挂了吗?不能。所以一定要设一个最小请求数,样本太少时不触发熔断。
好了,这一节的核心内容就这些。指标定义是熔断机制的基石,搞不清楚后面全是空中楼阁。下一节我们聊熔断状态机的设计——半开、全开、关闭,这三个状态怎么流转。
公众号:蓝海资料掘金营,微信deep3321