image
在分布式环境中,各个微服务相互调用,当某些情况下,比如后端中间件服务故障、第三方服务中断导致某个服务无限期不可用,短时间无法恢复,则可能会导致连锁故障,最终影响压垮整个业务集群
断路器模式不同于重试模式,重试模式是使应用程序可以重试操作以期望它会成功,而断路器模式是防止应用程序执行一个可能失败的操作,减少执行可能失败操作的CPU、内存、线程等资源的浪费,从而保证服务的整体可用
image
断路器相当于一个请求操作执行的代理,托管请求操作的执行
实现原理流程:
image
断路器状态机实现上有三种状态:Closed(断路器关闭)、Open(开放)、HalfOpen(半开放)
状态 | 说明 | 备注 |
---|---|---|
Closed | 关闭 | 断路器关闭正常执行操作 |
Open | 打开 | 断路器开放,所有请求直接返回错误,不执行任何请求 |
HalfOpen | 半开放 | 允许有限数量的请求通过,如果执行成功,恢复到关闭状态,如果仍然失败,则恢复到开放,然后重新启动超时定时器 |
image
断路器实现实现主要分为三部分:状态统计、状态转移、请求执行
状态统计:统计已经执行的请求的成功失败的数量,以确定是否需要进行状态转移
状态转移:根据当前统计信息和当前状态来进行目标状态的确定及转移操作
请求执行:代理前端任务的执行,如果当前状态不需要进行尝试执行,就直接返回错误,避免资源浪费
Golang里面已经有开源的实现, https://github.com/sony/gobreaker/blob/ , 接下来救市剖析它的实现
Counts就是一个计数器,记录当前请求成功和失败的数量
type Counts struct { Requests uint32 // 请求数 TotalSuccesses uint32 // 成功 TotalFailures uint32 // 失败 ConsecutiveSuccesses uint32 // 连续成功 ConsecutiveFailures uint32 // 连续失败 }
计数器完成对应请求状态的次数,为后续状态转移提供数据, Counts提供了onRequest、onSuccess、onFailure、clear几个辅助接口用于实现对应请求状态的操作,感兴趣可以看下
type CircuitBreaker struct { name string // maxRequests限制half-open状态下最大的请求数,避免海量请求将在恢复过程中的服务再次失败 maxRequests uint32 // interval用于在closed状态下,断路器多久清除一次Counts信息,如果设置为0则在closed状态下不会清除Counts interval time.Duration // timeout进入open状态下,多长时间切换到half-open状态,默认60s timeout time.Duration // readyToTrip熔断条件,当执行失败后,会根据readyToTrip决定是否进入Open状态 readyToTrip func(counts Counts) bool // onStateChange断路器状态变更回调函数 onStateChange func(name string, from State, to State) mutex sync.Mutex //. state 断路器状态 state State // generation 是一个递增值,相当于当前断路器状态切换的次数, 为了避免状态切换后,未完成请求对新状态的统计的影响,如果发现一个请求的generation同当前的generation不同,则不会进行统计计数 generation uint64 // Counts 统计 counts Counts // expiry 超时过期用于open状态到half-open状态的切换,当超时后,会从open状态切换到half-open状态 expiry time.Time }
请求执行,对外开放的请求执行接口
func (cb *CircuitBreaker) Execute(req func() (interface{}, error)) (interface{}, error) { // 执行请求钩子,会根据当前状态,来返回当前的generation和err(如果位于open和half-open则不为nil), 通过err来进行判断是否直接返回 generation, err := cb.beforeRequest() if err != nil { return nil, err } // 捕获panic,避免应用函数错误造成断路器panic defer func() { e := recover() if e != nil { cb.afterRequest(generation, false) panic(e) } }() // 执行请求 result, err := req() // 根据结果来进行对应状态的统计, 同时传递generation cb.afterRequest(generation, err == nil) return result, err }
func (cb *CircuitBreaker) beforeRequest() (uint64, error) { cb.mutex.Lock() defer cb.mutex.Unlock() // 获取当前的状态 now := time.Now() state, generation := cb.currentState(now) // open和half-open状态则直接返回 if state == StateOpen { return generation, ErrOpenState } else if state == StateHalfOpen && cb.counts.Requests >= cb.maxRequests { // 避免海量请求对处于恢复服务的影响,这里有一个限流的操作,避免请求数超过最大请求数 return generation, ErrTooManyRequests } // 统计状态 cb.counts.onRequest() return generation, nil }
func (cb *CircuitBreaker) afterRequest(before uint64, success bool) { cb.mutex.Lock() defer cb.mutex.Unlock() // 重新获取状态 now := time.Now() state, generation := cb.currentState(now) // 如果前后状态不一致,则不计数 if generation != before { return } // 根据状态计数 if success { cb.onSuccess(state, now) } else { cb.onFailure(state, now) } }
func (cb *CircuitBreaker) currentState(now time.Time) (State, uint64) { switch cb.state { case StateClosed: // 如果当前当前是closed状态,并且有设置expiry,则递增Generation到新一轮统计计数 if !cb.expiry.IsZero() && cb.expiry.Before(now) { cb.toNewGeneration(now) } case StateOpen: // 如果是Open状态,并且超时,则尝试到半打开状态 if cb.expiry.Before(now) { cb.setState(StateHalfOpen, now) } } return cb.state, cb.generation }
func (cb *CircuitBreaker) toNewGeneration(now time.Time) { // 递增generation, 清除状态 cb.generation++ cb.counts.clear() // 设置超时时间 var zero time.Time switch cb.state { case StateClosed: if cb.interval == 0 { cb.expiry = zero } else { cb.expiry = now.Add(cb.interval) } case StateOpen: cb.expiry = now.Add(cb.timeout) default: // StateHalfOpen cb.expiry = zero } }
image
断路器比较适合针对远程服务或者第三方服务的调用,如果该操作极有可能会失败,则断路器可以尽可能的减小失败对应用的影响,避免资源浪费
但缺点也显而易见,断路器本身相当于一层代理,在应用程序执行进行统计和控制,本身就有一定的资源消耗,同时内部基于synx.Mutex锁来实现,高并发下肯定会有锁争用问题,可能需要根据业务来使用多个断路器,来分散这种锁争用,同时应该避免在断路器req函数内,去执行重试和过长时间的超时等待,因为断路器核心是快速失败
更多文章可以访问 http://www.sreguide.com/