go-resilency 源码阅读 - breaker

495 阅读6分钟

golang 的熔断弹性模式

创建断路器需要三个参数

  • 错误阈值(用于打开断路器)
  • 成功阈值 (用于关闭断路器)
  • 超时(断路器保持打开状态的时间)

仓库给的使用例子:

b := breaker.New(3, 1, 5*time.Second)

for {
	result := b.Run(func() error {
		// communicate with some external service and
		// return an error if the communication failed
		return nil
	})

	switch result {
	case nil:
		// success!
	case breaker.ErrBreakerOpen:
		// our function wasn't run because the breaker was open
	default:
		// some other error
	}
}

源码分析阶段

// New constructs a new circuit-breaker that starts closed.
// From closed, the breaker opens if "errorThreshold" errors are seen
// without an error-free period of at least "timeout". From open, the
// breaker half-closes after "timeout". From half-open, the breaker closes
// after "successThreshold" consecutive successes, or opens on a single error.
func New(errorThreshold, successThreshold int, timeout time.Duration) *Breaker {
	return &Breaker{
		errorThreshold:   errorThreshold,
		successThreshold: successThreshold,
		timeout:          timeout,
	}
}

根据注释我们知道,这里新建了一个新的断路器,初始状态是闭合的。从闭合开始,如果 "errorThreshold"(错误阈值)错误出现,而无错时间至少为 "timeout"(超时),则断路器断开。从打开开始,断路器在 "timeout" 后半闭。 从半开开始,断路器在 "成功阈值 "连续成功后关闭,或在出现一次错误后打开。

Breaker 的结构体所含有的参数如下所示:

// Breaker implements the circuit-breaker resiliency pattern
type Breaker struct {
	errorThreshold, successThreshold int
	timeout                          time.Duration

	lock              sync.Mutex
	state             uint32
	errors, successes int
	lastError         time.Time
}

其中 errorThreshold、successThreshold 、 timeout 是我们 New 这个 breaker 时所传入的,lock 字段在接下来的 processResult 函数和 timer 函数出现, 不言而喻就是为了防止并发执行出现竞争的情况。

state 状态 有三种,分别时关闭状态,打开状态和半打开状态,从这里我们也可以看出来,由于我们 New 一个 Breaker时,并没有传入 state 相关的状态,所以默认的状态相应的零值就是 closed

const (
	closed uint32 = iota
	open
	halfOpen
)

errors,success 是记录出现失败和成功的次数,当断路器更改状态的时候,会将它们重新置为 0

lastError 记录了最后一次出现错误的时间点

让我们进入执行函数:

// Run will either return ErrBreakerOpen immediately if the circuit-breaker is
// already open, or it will run the given function and pass along its return
// value. It is safe to call Run concurrently on the same Breaker.
func (b *Breaker) Run(work func() error) error {
	// 利用原子的方式取出,取出熔断器的状态,对 atomic 感兴趣的,可以看下我的第一篇文章最后一节
	state := atomic.LoadUint32(&b.state)

	if state == open {
		return ErrBreakerOpen
	}

	return b.doWork(state, work)

根据注释我们知道,如果断路器已经打开,Run 将立即返回 ErrBreakerOpen,否则它将运行给定函数并传递其返回值。并且在同一个断路器上同时调用 Run 是安全的。

执行相应的 work 函数的内部实现:

func (b *Breaker) doWork(state uint32, work func() error) error {
	var panicValue interface{}
	//这里的func()已经执行了,result其实是个error类型,
	//之前一直以为是把result当作一个func()来用的
	result := func() error {
		defer func() {
			panicValue = recover()
		}()
		return work()
	}()

	//如果work函数返回值为nil,panicValue为nil,并且熔断器的状态为关闭时,直接返回 nil
	if result == nil && panicValue == nil && state == closed {
		// short-circuit the normal, success path without contending
		// on the lock
		return nil
	}

	// 否则,根据 result 和 panicValue 进行处理
	// oh well, I guess we have to contend on the lock
	b.processResult(result, panicValue)
	// 如果 panicValue 不为 nil,返回 panic
	if panicValue != nil {
		// GO让我们尽可能地接近“重抛”,尽管不幸的是我们失去了原来的产生panic的地方
		// as close as Go lets us come to a "rethrow" although unfortunately
		// we lose the original panicing location
		panic(panicValue)
	}
	
	// 最后返回相应的 result 值
	return result
}

当上述代码 if result == nil && panicValue == nil && state == closed 不成立时,接下来要执行的 processResult 的内部实现:

func (b *Breaker) processResult(result error, panicValue interface{}) {
	// 加锁
	b.lock.Lock()
	// 最后需要释放锁
	defer b.lock.Unlock()

	//当没有错误产生时
	if result == nil && panicValue == nil {
		//这时如果熔断器的状态处于半开的状态
		if b.state == halfOpen {
			//熔断器成功的次数+1
			b.successes++
			//如果熔断器的成功次数等于最大的成功次数
			if b.successes == b.successThreshold {
				//关闭熔断器
				b.closeBreaker()
			}
		}
	} else {
		//当有错误产生的情况
		//如果错误的次数大于0
		if b.errors > 0 {
			//失效的时间加上熔断器的超时时间
			expiry := b.lastError.Add(b.timeout)
			//如果失效时间小于现在的时间
			if time.Now().After(expiry) {
				//将错误的次数置为0
				b.errors = 0
			}
		}
		//检查熔断器的状态
		switch b.state {
		//如果处于关闭的状态
		case closed:
			//将熔断器的错误次数+1
			b.errors++
			//如果熔断器的错误次数等于熔断器最大的错误次数
			if b.errors == b.errorThreshold {
				//打开熔断器
				b.openBreaker()
			} else {
				//否则将熔断器最后一次产生错误的时间置为现在这个时刻
				b.lastError = time.Now()
			}
		//如果处于半开的状态
		// 这里当熔断器处于半打开状态时,出现一次出现就打开熔断器
		case halfOpen:
			//打开熔断器
			b.openBreaker()
		}
	}
}

打开熔断器和关闭熔断器的内部实现:

func (b *Breaker) openBreaker() {
	//打开熔断器
	b.changeState(open)
	//执行定时器
	go b.timer()
}

func (b *Breaker) closeBreaker() {
	// 关闭断路器
	b.changeState(closed)
}

这里对比之下,有一点特别的点就是,打开熔断器的内部实现,里面还利用 go 的协程执行了一个定时器,我们来看以下定时器的逻辑:

func (b *Breaker) timer() {
	//打开熔断器的同时,隔固定的时间,将熔断器置为半打开的状态,以免熔断器一直处于打开的状态
	time.Sleep(b.timeout)
	//加锁
	b.lock.Lock()
	defer b.lock.Unlock()
	//将熔断器改为半开的状态
	b.changeState(halfOpen)
}

这个定时器的逻辑,其实就是根据我们 New 一个熔断器时传入的 timeout,进行休眠 timeout 时间后,将熔断器改为半开的状态

我们继续看下 changeState 的内部实现:

func (b *Breaker) changeState(newState uint32) {
	//更改断路器的状态,同时将失败和成功的次数置为 0
	b.errors = 0
	b.successes = 0
	atomic.StoreUint32(&b.state, newState)
}

从这里我们可以看到,改为熔断器的状态时,会将出现 errors 和 success 的次数重新置为 0,然后更改熔断器的状态

到此我们基本已经分析完毕,但是它内部还有一个 Go 函数

// Go will either return ErrBreakerOpen immediately if the circuit-breaker is
// already open, or it will run the given function in a separate goroutine.
// If the function is run, Go will return nil immediately, and will *not* return
// the return value of the function. It is safe to call Go concurrently on the
// same Breaker.
func (b *Breaker) Go(work func() error) error {
	state := atomic.LoadUint32(&b.state)

	if state == open {
		return ErrBreakerOpen
	}

	// errcheck complains about ignoring the error return value, but
	// that's on purpose; if you want an error from a goroutine you have to
	// get it over a channel or something
	go b.doWork(state, work)

	return nil
}

这个 Go 函数的逻辑其实跟上面的 Run 函数大部分是一致的,唯一的区别点是它用了协程执行去执行 dowork 函数,然后立即返回 nil 值,并且不会返回我们要执行的函数的返回值。