Sarama 实现的 Breaker
在阅读 Sarama 代码的时候发现 Sarama 自己实现了一个熔断器, 整个实现不超过 200 行, 短小精悍, 所以拿来学习了一番~
代码地址 github.com/eapache/go-…, 作者 Evan Huus 也是 Sarama 的主要 contributor
Breaker 的状态及状态转移
Breaker 定义了三种状态, closed, open, halfOpen.
const (
closed uint32 = iota
open
halfOpen
)
Breaker 的初始状态是 closed, 从 closed 状态, 如果连续有 errorThreshold 个 error, Breaker 的状态改变为 open, 这里对连续发生有一个限制, 即每两个 error 发生的时间间隔在 timeout 之内, 否则 error 计数会清零, 在之后的代码中将会看到 Breaker 是如何处理这一逻辑的, 源码中是这样描述这一限制的 without an error-free period of at least "timeout"; 从 open 状态, 在经过 timeout 时间后, Breaker 会变成 halfOpen 状态; 从 halfOpen 状态, 如果之后有连续 successThreshold 次成功, Breaker 会变成 closed 状态, 这里没有时间间隔的限制, 如果遇到了一次 error, 则又变成 open 状态
下面是 Breaker 的结构, errorThreshold, successThreshold, timeout, state 在上面的状态转移描述中都有提到, errors 和 successes 用于维护连续的 error 或 success 数, lock 用来保护资源, lastError 记录了上一次 error 的时间, 用于在计数 errors 时实现 without an error-free period of at least "timeout" 的限制。
type Breaker struct {
errorThreshold, successThreshold int
timeout time.Duration
lock sync.Mutex
state uint32
errors, successes int
lastError time.Time
}
Breaker 的实现
Breaker 的使用者调用 Run, 传入希望执行的功能函数 work, 如果熔断器是 open 状态, 直接返回 ErrBreakerOpen, 此时 work 没有被执, 否则 work 将被执行, 并根据执行结果更新 Breaker 状态
func (b *Breaker) Run(work func() error) error {
state := atomic.LoadUint32(&b.state)
if state == open {
return ErrBreakerOpen
}
return b.doWork(state, work)
}
Breaker.doWork 调用 work 执行任务并 recover 了 work 中可能发生的 panic, 如果没有错误发生且 Breaker.state == closed, 则任务执行成功, Breaker 也不需要状态更新, 可以直接返回了
否则无论是有错误发生还是说 Breaker.state 为 halfOpen, 都需要更新 Breaker 的状态
func (b *Breaker) doWork(state uint32, work func() error) error {
var panicValue interface{}
result := func() error {
defer func() {
panicValue = recover()
}()
return work()
}()
if result == nil && panicValue == nil && state == closed {
// short-circuit the normal, success path without contending
// on the lock
return nil
}
// oh well, I guess we have to contend on the lock
b.processResult(result, panicValue)
if panicValue != nil {
// as close as Go lets us come to a "rethrow" although unfortunately
// we lose the original panicing location
panic(panicValue)
}
return result
}
Breaker 根据执行结果维护状态的逻辑实现在 Breaker.processResult 中, 如果没有错误发生, 此时 Breaker 的状态只可能是 halfOpen 了(open 状态在 Run 中就会返回, closed 状态在 doWork 中就会返回), 更新连续的成功数 successes 并在达到 successThreshold 限制时将 Breaker 状态设置为 closed; 如果有错误发生, 此时 Breaker 状态面临两种可能的变更, 一是从 closed 状态变为 open 状态, 二是从 halfOpen 状态变为 open 状态
上面提到过从 closed 变为 open 状态, Breaker 需要看到 errorThreshold 个 error, 且不存在一个时长超过 timeout 的 error-free 窗口, 否则 error 计数将清零, 为实现这一功能, Breaker 维护了上一次 error 的发生时间 Breaker.lastError, 如果两次 error 的间隔超时, 则将 Breaker.errors 设置为 0
func (b *Breaker) processResult(result error, panicValue interface{}) {
b.lock.Lock()
defer b.lock.Unlock()
if result == nil && panicValue == nil {
if b.state == halfOpen {
b.successes++
if b.successes == b.successThreshold {
b.closeBreaker()
}
}
} else {
if b.errors > 0 {
expiry := b.lastError.Add(b.timeout)
if time.Now().After(expiry) {
b.errors = 0
}
}
switch b.state {
case closed:
b.errors++
if b.errors == b.errorThreshold {
b.openBreaker()
} else {
b.lastError = time.Now()
}
case halfOpen:
b.openBreaker()
}
}
}
最后还有一个从 open 到 halfOpen 的状态变更没有提到, 这部分实现在 openBreaker 中, 在将 Breaker 的状态置为 open 的同时会开启一个计时器, 在 timeout 时间后将状态置为 halfOpen
func (b *Breaker) openBreaker() {
b.changeState(open)
go b.timer()
}
func (b *Breaker) timer() {
time.Sleep(b.timeout)
b.lock.Lock()
defer b.lock.Unlock()
b.changeState(halfOpen)
}
总结
Sarama 的作者用不到 200 行的代码实现了一个可用的 Breaker, 代码精炼, 且实现了熔断器模式的核心功能, 值得学习