本文会介绍断路器模式,以及断路器模式在 Golang 的实现框架:gobreaker
在分布式环境中,对远程资源和服务的调用可能会由于临时性故障(如网络连接缓慢、超时、资源过载或资源暂时不可用)而失败。 这些故障通常会在短时间内自行更正,而且,应该会准备一个可靠的云应用程序,通过重试模式这样的策略来处理它们。
但是,也可能遇到由于意外事件而导致的故障,且需要更长的时间来进行修复。 这些故障轻则导致部分连接中断,重则导致服务完全瘫痪。 这类情况下,让应用程序持续重试不可能成功的操作是毫无意义的。相反,应用程序应该快速认识到操作已失败,并相应地处理此故障。
在这些情况下,更有益的做法便是让操作立即失败并只在服务可能成功时才尝试调用服务。
断路器模式的目的与重试模式不同。 重试模式在预期操作将成功的情况下让应用程序重试操作。 断路器模式则防止应用程序执行很可能失败的操作。 应用程序可以使用重试模式通过断路器调用操作,来组合这两种模式。 但重试逻辑应该对断路器返回的任何异常保持敏感,并且在断路器指示故障为非临时性的情况下放弃重试尝试。
针对可能失败的操作,断路器充当其代理。 代理应监视最近发生的失败次数,并使用此信息来决定是允许操作继续进行,还是立即返回异常:
- 关闭:将来自应用程序的请求路由到操作。 代理维护最近失败次数的计数,如果对操作的调用不成功,代理将递增此计数。 如果在给定时间段内最近失败次数超过指定的阈值,则代理将置于 打开 状态。 此时,代理会启动超时计时器,并且当此计时器过期时,代理将置于 半开 状态。
超时计时器的目的是给系统一段时间来解决导致失败的问题,并允许应用程序再次尝试执行操作。
-
打开:来自应用程序的请求立即失败,并向应用程序返回异常。
-
半开:允许数量有限的来自应用程序的请求通过并调用操作。 如果这些请求成功,则假定先前导致失败的问题已被修复,并且断路器将切换到 关闭 状态(失败计数器重置)。 如果有任何请求失败,则断路器将假定故障仍然存在,因此它会恢复到 打开 状态,并重新启动超时计时器,再给系统一段时间来从故障中恢复。
半开 状态对于防止恢复服务突然被大量请求淹没很有用。 在服务恢复的同时,它或许能够支持数量有限的请求,直至恢复完成;但当恢复正在进行时,大量的工作可能导致服务超时或再次失败。
在图中,关闭 状态所使用的失败计数器是基于时间的。 它会定期自动重置。 这有助于防止断路器在遇到偶然失败时进入 打开 状态。 仅当在指定间隔期间内发生指定数量的失败时,才会达到将断路器跳闸到 打开 状态的故障阈值。 半开 状态使用的计数器记录成功调用操作的次数。 在指定数量的连续操作调用成功后,断路器将恢复到 关闭 状态。 如果任何调用失败,断路器会立即进入 打开 状态,成功计数器会在下次进入 半开 状态时重置。
系统恢复是从外部进行的,可能的方法是通过还原或重新启动失败的组件,或修复网络连接。
断路器模式在系统从故障中恢复时提供稳定性,并将对性能的影响降至最低。 它可以通过快速拒绝很可能失败的操作的请求(而非等待操作超时或永不返回)来帮助维持系统的响应时间。 如果断路器在每次改变状态时引发事件,则该信息可以用于监视由断路器保护的系统部分的运行状况,或者当断路器跳闸到 打开 状态时,对管理员发出警报。
gobreaker
是索尼公司开源的组件,使用Golang
语言开发,用于实现断路器模式。
go get github.com/sony/gobreaker
CircuitBreaker
是一个状态机,用于防止发送可能失败的请求。函数NewCircuitBreaker
用于创建一个新的CircuitBreaker
。
状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。
func NewCircuitBreaker(st Settings) *CircuitBreaker
你可以通过Settings
来配置CircuitBreaker
type Settings struct {
Name string
MaxRequests uint32
Interval time.Duration
Timeout time.Duration
ReadyToTrip func(counts Counts) bool
OnStateChange func(name string, from State, to State)
}
-
Name:
CircuitBreaker
的名字。 -
MaxRequests:
CircuitBreaker
半开时允许通过的最大请求数,如果MaxRequests
为0,CircuitBreaker
只允许 1 个请求。 -
Interval:
CircuitBreaker
清除内部Counts
的闭合状态的循环周期,如果间隔为0,CircuitBreaker
在闭合状态下不清除内部Counts
。 -
Timeout:
CircuitBreaker
处于半开状态后的断开状态时间,如果Timeout
为 0,则CircuitBreaker
的超时值设置为 60 秒。 -
ReadyToTrip:每当请求在关闭状态下失败时,都会使用
Counts
的副本调用ReadyToTrip
。如果ReadyToTrip
返回 true,CircuitBreaker
将设置为断开状态,如果ReadyToTrip
为 nil,则使用默认的ReadyToTrip
。当连续故障数超过5时,默认的ReadyToTrip
返回 true。 -
OnStateChange:只要
CircuitBreaker
的状态发生变化,就会调用OnStateChange
。
参数Counts
保存请求数量以及成功,失败的数量:
type Counts struct {
Requests uint32
TotalSuccesses uint32
TotalFailures uint32
ConsecutiveSuccesses uint32
ConsecutiveFailures uint32
}
CircuitBreaker
清除状态变化或闭合状态间隔的内部Counts
。Counts
忽略清除前发送请求的结果。
CircuitBreaker
可以包装任何函数用来发送请求:
func (cb *CircuitBreaker) Execute(req func() (interface{}, error)) (interface{}, error)
如果CircuitBreaker
接受给定的请求,则Execute
方法将运行该请求。
如果CircuitBreaker
拒绝请求,Execute
方法立即返回错误。否则,Execute
返回请求的结果。
如果请求中发生panic
,CircuitBreaker
将其作为错误处理,并向外抛出相同的panic
。
var cb *breaker.CircuitBreaker
func Get(url string) ([]byte, error) {
body, err := cb.Execute(func() (interface{}, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
})
if err != nil {
return nil, err
}
return body.([]byte), nil
}