Sarama 源码阅读番外(一): Sarama 实现的精简版熔断器

1,138 阅读3分钟

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, 代码精炼, 且实现了熔断器模式的核心功能, 值得学习