微服务之断流
我们每个人应该都对保险丝很熟悉, 熔断器也是类似的设备, 一旦电流过载,电压异常,保险丝熔断, 电路就会自动断开, 从而保护电器和设备。
受此启发, 熔断器模式在微服务的设计中也大行其道。 微服务在分布式系统中, 多个微服务之间是存在诸多依赖关系, 当其中一个服务出现故障, 很可能会拖累整个系统. 一个微服务多数情况既会被其他微服务依赖, 也会依赖其他的微服务一起协同工作, 前者我们称为上游服务, 后者称为下游服务.
上游微服务 --> 微服务 --> 下游微服务
当微服务出现问题, 其上游服务会不断地尝试和等待, 如何不加限制, 快速止损, 就会很快耗尽宝贵的线程资源,于是大量的后续请求等待在线程队列中造成阻塞. 更加严重的是, 如果多个微服务之间存在调用链, 那么问题会出现传递效应, 一传十, 十传百, 从而造成雪崩, 一发而不可收拾。所以我们需要保险丝这样的装置来进行故障隔离。
微服务因为每一个服务都比较微小,并只专注于某个独立的业务功能, 所以整个业务流程需要多个服务协同工具, 调用层级也可能比较多, 三四层为好, 不宜过多, 试举一例
微服务A 调用 B, B 调用 C, C 会读取 mysql 和 cassandra,
A --> B --> C --> MySQL/Cassandea
如果mysql 出问题, 会造成 C 的资源耗尽,B, A 也会逐渐耗尽资源
但是 A, B 还有其他对外的服务因为一个依赖服务出问题, 整个服务的所有功能全面出问题了, 这个显然不合理. 其原因在于没有做好故障熔断和隔离.
为此, 我们要做好三件事:
- 超时与重试
在设定时间没有返回时, 不再苦苦等待, 重试一次以便负载均衡器连接另一台服务器, 重试仍然出错时, 直接返回
- 故障隔离
我们的服务可能依赖若干下游服务, 某一个服务出错, 相应功能受影响, 但其他的下游服务仍然可用, 故其他功能也应不受影响. 故障隔离的方法很多, 基本上是分离执行体, 使用不同的进程,线程或协程来分离任务的执行, 彼此之间互不影响
- 快速止损
如果一个依赖服务最近总是出错, 那么就不要重试,不要再使用它了, 等它彻底修复好以后再恢复使用.否则由于传递效应, 会造成大规模的多个服务不可用, 故障没有局限在局部
故障与隔离
故障常见的有两种现象
- 错误响应 Error
- 响应超时 Timeout
一个微服务向多个客户提供多种功能的服务, 一个客户或一个功能点有问题, 那只是局部问题, 不应该影响全局, 所以我们需要隔离故障。
故障的隔离的方式一般使用两种
(1)线程池隔离模式:使用一个线程池来存储当前的请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)
(2)信号量隔离模式:使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,请求到来时先判断计数器的数值,若超过设置的最大线程个数则丢弃改类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)
熔断器模式
就象保险丝一样, 在调用者与被调用者放入一个中间的连接代理, 如果错误发生的条件满足,触发了熔断, 调用者无需尝试和等待, 直接报错返回,或者回退到备用方案。
参见 Martin Fowler 绘制的 Circuit Breaker 的时序图, 连接问题发生时,无需等待超时,直接返回。
Martin Fowler Circuit Breaker Sequence Diagram熔断器的实现一般通过状态机来实现, 熔断器会有如下几种状态
- 闭合状态 Closed state: 和下游微服务连接和调用保持正常时的状态,所有请求直接通过熔断器到达下游微服务
- 断开状态 Open State: 和下游微服务连接和调用多次出现错误,就会从闭合状态变成断开状态, 所有到达熔断器的请求直接报错返回
- 半开状态 Half-Open State:当熔断器在断开的状态时,在一定时间之后, 从断开状态变为半开状态,允许一些请求通过熔断器,如果这些请求调用成功,则可以将状态转化为闭合状态
熔断器的状态转换的关键在于以下关键度量指标
- 多少次成功 NS (Numbe of Success),
- 多少次失败 NF(Number of Failure)
- 多少次超时 NT(Number of Timeout)
在 NS, NF, NT 到达一定阈值时, 保险丝熔断, 从闭合状态变为断开状态
同理,在预设的时间后从断开状态转为半开状态, 持续观察这三个指标
例如
- 当 NS /(NS + NF + NT ) > 80% 时, 从半开状态变成闭合状态
- 当 NS /(NS + NF + NT ) < 20% 时, 从闭合状态变成断开状态
这里的 80%,20% 应作为可配置值, 根据具体服务的可靠性要求做出相应调整。
在以 REST API 提供服务所使用的熔断器处于断开状态时, 熔断器不到向下游服务发送请求, 可立即向上游服务汇报 503 ,并在 Retry-After 中指示上游服务稍后再试。
当然, 这是最简单的方式,在直接面向终端用户的系统上, 应该提供更优雅的降级方式
- 尽早失败并提供友好的错误信息
- 节约资源
- 及时响应
- 简化诊断的复杂性
- 及时反馈给用户无效的输入, 不支持的请求方式和数量
- 优雅降级:
- 只提供有限功能, 显示马上回来, 屏蔽图片, 只有缺省选项等等
- 优雅补偿
- 显示道歉, 给用户投诉和帮助的链接或电话
熔断器实例 Hystrix
Hystrix [hɪst'rɪks]的中文含义是豪猪, 因其背上长满了刺,而拥有自我保护能力. Netflix的 Hystrix 是一个帮助解决分布式系统交互时超时处理和容错的类库, 它用于处理在调用相关服务或资源时会发生的不可避免的故障和延迟, 而且该库还提供了美观而全面的度量指标,以便进行监控和调整相关参数。
主要功能有:
- 快速失败
- 提供回退方案
- 提供熔断机制
- 提供监控组件
Hystrix的设计原则包括:
- 资源隔离
- 熔断器模式
- 命令模式
Hystrix 的 wiki 中给出了一个流程图
How it works具体做法如下
- 构造一个
HystrixCommand
或者HystrixObservableCommand
命令对象 - 执行这个命令对象
- 检查响应是否有缓存?
- 检查熔断器是否断开?
- 检查线程池/队列/信号量是否满载?
- 执行
HystrixObservableCommand.construct()
或HystrixCommand.run()
- 计算熔断器的健康状况
- 获取回退方法
- 返回成功的响应
具体逻辑如下
要点:
- 请求以 HystrixCommand 包装后到达熔断器 CircuiteBreaker , 如果熔断器处于闭合状态, 直接放行,如果处于断开状态, 再检查断开时间 Sleep Time 是否过了, 如果过了,也会放行, 否则直接报错返回。
- 在每次调用时会通过一个函数 markSuccess(duration), markFailure(duration) 来统计在指定时间间隔 duration 内的成功与失败调用次数
- 熔断器断开的触发条件为错误率= failure/(success + failure)
- 熔断器的断开与闭合状态的转换关键在于在于 NF, NS, NT 这样的度量数据
- 熔断器的度量数据统计会划分成若干个时间窗口或者称 bucket 桶,老的时间窗口内的统计数据会丢弃,以反映实时的系统状态