微服务如何保证不会出现连锁反应?Go 实现的断路器了解下

本文作者:杨锡坤
【微服务如何保证不会出现连锁反应?Go 实现的断路器了解下】原文链接:
本文学习参考自: Circuit Breaker pattern [1] 和 cep21/circuit [2]
业务问题场景在业务系统中 , 通常存在服务之间的相互调用 , 例如服务 A 调用服务 B , 当出现如下情形:

  1. 服务 A 与服务 B 之间的网络出现异常
  2. 服务 B 过载
  3. 服务 B 出现异常
服务 A 应该减少对服务 B 的调用量 , 甚至服务 A 应该停止调用服务 B , 有必要的话 , 还可以采取相应的降级措施 。 当服务 B 恢复正常后 , 才开始继续调用服务 B 。
断路器模式在家庭电路中有一个叫断路器的安全设备 , 当出现电路过载、短路、漏电等情况时 , 就会发生跳闸 , 防止出现安全事故 。 类比到上面描述的业务问题场景 , 我们需要在系统中实现一个类似断路器功能的组件 , 用于阻止系统 A 重复尝试很可能失败的调用 。
在断路器模式中 , 断路器组件需要监测到最近失败的调用 , 并且利用这些信息去决定新的调用是否执行 , 还是立即抛出异常 。 当断路器组件“跳闸”之后 , 还需要能探测被调用服务是否恢复正常 ,
断路器模式的代码实现 , 使用了有限状态机的思想 。 最基本的实现有三种状态:
  • 关闭(Closed):调用正常执行 。 断路器组件对最近失败的调用进行计数 , 当达到阈值时 , 则断路器组件“跳闸” , 进入“打开”状态 。
  • 打开(Open):调用请求会立即失败 , 断路器组件抛出异常 。
  • 半打开(Half-Open):当处于“打开”状态时 , 会启动一个超时定时器 , 当超时后 , 断路器组件会进入“半打开”状态 , 此时允许执行部分调用 , 断路器会对成功执行的调用进行计数 , 达到阈值后 , 会认为被调用服务恢复正常 , 断路器状态回到“关闭”状态 , 如果有请求出现失败 , 则回到“打开”状态 。
下图来自: Circuit Breaker pattern [3]
微服务如何保证不会出现连锁反应?Go 实现的断路器了解下文章插图
问题和注意事项
  • 异常处理:系统需要考虑到断路器抛出的各类异常该如何处理 。 比如采取降级措施 , 把请求转发给备份服务 , 或者通知上游稍后重试 。
  • 异常的类型:服务调用请求可能出现超时 , 或网络不通 , 下游服务明确返回失败的情况 , 断路器可能需要针对不同情况的错误 , 采取不同的状态切换策略 。 例如触发切换到“打开”状态的条件 , 可以是超时错误的阈值比下游服务明确返回失败的阈值更高 。
  • 日志:断路器需要记录下所有失败的请求 , 方便相关人员监控定位问题 。
  • 恢复:配置合适的策略 , 让断路器检测下游服务是否恢复正常 ,
  • “打开”到“半打开”的状态切换:可以不使用定时器 , 而是周期性的探测下游服务是否恢复 。
  • 人为干预:服务异常恢复需要的时间有长有短 , 断路器最好能提供人为控制的接口 , 方便将断路器强制切换到“打开”或“关闭”状态 。
  • 并发:一个断路器可能会被很多请求并发访问 , 所以断路器工程化实现所需的时间和空间消耗需要尽量的小 。
  • 资源差异:为不同的资源访问 , 单独创建相应的断路器 。
  • 加速“跳闸”:当可以从下游服务获取到足够明确的异常时 , 则立即切换到“打开”状态 。
Golang 中断路器的实现:cep21/circuitgithub.com/cep21/circuit [4] 实现了类似 Hystrix(Java 版本)的断路器模式 。 在示例代码 circuit/v3/example/main.go 中 , 模拟了服务调用可能出现的各种情况 , 以及对应的断路器配置 , 以下我总结了对该库的理解和应用 。
cep21/circuit 中主要的类型和接口:
circuit.Manager// 管理多个circuits对象实例type Manager struct {// func (h *Manager) CreateCircuit(name string, configs ...Config) (*Circuit, error) 方法创建circuits对象实例时 , 使用的配置 , 会按照逆序将多个配置合并为最终的配置DefaultCircuitProperties []CommandPropertiesConstructor// 每个circuits会有一个唯一命名的标识circuitMap map[string]*Circuit// 用于circuitMap的读写锁mu sync.RWMutex}circuit.Circuittype Circuit struct {//circuitStatsCmdMetricCollectorRunMetricsCollection// 统计调用出现的各种情况FallbackMetricCollector FallbackMetricsCollection// 统计降级调用出现的各种情况CircuitMetricsCollector MetricsCollection// 统计Circuit状态切换的情况// This is used to help run `Go` calls in the backgroundgoroutineWrapper goroutineWrapper // 用于异步调用的封装namestring// 断路器唯一命名的标识notThreadSafeConfig Config // 非线程安全的断路器配置notThreadSafeConfigMu sync.MutexthreadSafeConfigatomicCircuitConfig // 线程安全的断路器配置// Tracks if the circuit has been shut open or closedisOpen faststats.AtomicBoolean // 断路器只有“打开”和“关闭”两种状态// Tracks how many commands are currently runningconcurrentCommands faststats.AtomicInt64// 统计有多少并发调用// Tracks how many fallbacks are currently runningconcurrentFallbacks faststats.AtomicInt64// 统计有多少降级的并发调用// ClosedToOpen controls when to open a closed circuitClosedToOpen ClosedToOpen// 控制断路器由“关闭”状态切换到“打开”状态// openToClosed controls when to close an open circuitOpenToClose OpenToClosed// 控制断路器由“打开”状态切换到“关闭”状态timeNow func() time.Time// 对time.Now的封装 , 值始终为config.General.TimeKeeper.Now , 从config.TimeKeeper的解释看是为了方便测试 , 当没在测试代码里有看到使用}