状态机设计:熔断器三态转换逻辑与半开状态探活机制

熔断器的核心,说白了就是一个状态机。

我做了这么多年风控系统,见过太多人把熔断器想得太复杂。其实它就三个状态:Closed(关闭)、Open(打开)、Half-Open(半开)。嗯,就这么简单。但真正难的是——这三个状态之间怎么转?转的时机对不对?转错了会出什么后果?

三态转换的核心逻辑

先看一张图,这是我个人习惯用的状态转换图,清晰明了:

Closed (关闭) Open (打开) Half-Open (半开) 失败率超阈值 超时后自动进入 探活成功 探活失败 熔断器三态转换图 Closed → Open:失败率超过阈值 | Open → Half-Open:超时自动进入 | Half-Open → Closed/Open:探活结果决定

Closed → Open:什么时候打开?

Closed 状态下,所有请求正常通过。但系统会默默统计失败率。

我在项目中遇到过一个问题:阈值设得太低。有一次线上业务波动,失败率短暂冲到 15%,结果熔断器直接打开了。其实那只是网络抖动,根本不需要熔断。后来我学乖了——阈值要结合业务容忍度来设。

核心参数:

  • 失败率阈值:比如 50%,超过就熔断
  • 滑动窗口大小:统计最近 N 秒的数据
  • 最小请求数:请求太少时不触发熔断,避免误判

举个例子:

// 伪代码:判断是否要熔断
func shouldTrip(stats *WindowStats) bool {
    // 请求太少,不触发
    if stats.TotalRequests < minRequestThreshold {
        return false
    }
    // 失败率超过阈值,触发
    return stats.FailureRate >= failureRateThreshold
}

你想想看,如果最小请求数设成 1,那一个请求失败就直接熔断了——这显然不合理。我建议最小请求数至少设到 10 以上。

Open → Half-Open:什么时候半开?

Open 状态下,所有请求直接拒绝,返回快速失败。但总不能一直拒绝下去吧?

所以需要一个超时时间。比如 30 秒后,自动进入 Half-Open 状态。

我的经验:

超时时间不要设成固定值。我曾经踩过坑——固定 30 秒,结果下游服务恢复需要 40 秒,那 10 秒的窗口期所有请求都白探活了。后来我改成指数退避:第一次 10 秒,第二次 20 秒,第三次 40 秒……

// 伪代码:计算下次探活时间
func nextRetryTimeout(retryCount int) time.Duration {
    baseTimeout := 10 * time.Second
    return baseTimeout * time.Duration(1 << uint(retryCount))
}

Half-Open 探活机制:最关键的环节

Half-Open 是熔断器最精妙的设计。说白了,就是放一个探针进去试试水

这个状态只允许有限数量的请求通过。比如只放 3 个请求过去。如果都成功了,就切回 Closed;如果有一个失败,立刻回到 Open。

注意:

探活请求的数量一定要控制好。我曾经见过一个系统,半开状态下放了 100 个请求过去——结果下游服务还没恢复,直接被打挂了。这叫「探活风暴」。建议探活数不超过 5 个。

探活的具体实现:

// 伪代码:半开状态探活逻辑
type CircuitBreaker struct {
    state           State
    halfOpenCount   int       // 已放行的探活请求数
    maxHalfOpenReqs int       // 最大探活请求数
    successCount    int       // 探活成功数
    requiredSuccess int       // 需要连续成功数
}

func (cb *CircuitBreaker) allowRequest() bool {
    switch cb.state {
    case Closed:
        return true
    case Open:
        return false
    case HalfOpen:
        // 探活请求数没超,就放行
        if cb.halfOpenCount < cb.maxHalfOpenReqs {
            cb.halfOpenCount++
            return true
        }
        return false
    }
    return false
}

func (cb *CircuitBreaker) onRequestComplete(success bool) {
    if cb.state != HalfOpen {
        return
    }
    
    if success {
        cb.successCount++
        // 连续成功达到阈值,切回 Closed
        if cb.successCount >= cb.requiredSuccess {
            cb.resetToClosed()
        }
    } else {
        // 任何一个探活失败,立刻回到 Open
        cb.resetToOpen()
    }
}

状态转换的完整流程

当前状态 触发条件 下一状态 说明
Closed 失败率 >= 阈值 Open 开始拒绝请求
Open 超时时间到 Half-Open 允许少量探活请求
Half-Open 探活全部成功 Closed 恢复正常
Half-Open 探活有失败 Open 继续熔断

避坑指南

我曾经踩过几个坑,分享给你:

  • 状态转换的原子性:多线程环境下,状态转换必须加锁。我见过一个系统,两个线程同时判断「应该切到 Open」,结果状态被覆盖了。用 CAS 或者锁来保证。
  • 探活请求的超时:探活请求本身也要设超时。如果下游服务一直不返回,探活请求会卡住,导致探活数被耗尽。
  • 不要用全局状态:每个业务线、每个接口最好有独立的熔断器。我见过有人用一个全局熔断器,结果一个接口挂了,整个系统都熔断了——那叫「熔断雪崩」。

总结一下:

熔断器的三态转换,核心就三句话:

  1. Closed 下失败多了 → Open
  2. Open 等一会儿 → Half-Open
  3. Half-Open 探活成功 → Closed,失败 → Open

记住这个循环,熔断器你就掌握了 80%。剩下的 20% 是参数调优和边界处理。


专注资料整理