第3章通用的服务可用性治理手段——3.3 熔断与隔离

第3章通用的服务可用性治理手段——3.3 熔断与隔离
John Yaml熔断和隔离都是上游服务可以采取的流量控制策略。
- 熔断可以有效防止我们的服务被下游服务拖垮,同时可以在一定程度上保护下游服务。
- 隔离可以防止一个服务内各个接口之间因质量问题而相互影响。
3.3.1 服务雪崩
由于网络原因或服务自身设计问题,每个微服务一般都难以保证100%对外可用。如果某服务出现了质量问题,那么与其相关的上游服务网络调用就容易出现线程阻塞的情况;如果有大量的线程发生阻塞,则会导致上游服务承受较大的负载压力而发生宕机故障。在微服务架构中,由于在服务间建立了依赖关系,所以一个服务的故障会不断向上传播,最终导致整个服务链路发生宕机故障。这就是服务雪崩现象。
假设有3个服务形成如图3-7所示的依赖关系,最上游的Server-1服务直接负责与用户请求交互。
如图3-8所示,某一天,Server-3服务因请求量暴增或设计不合理而宕机。由于Server-2是其上游服务,所以Server-2服务还会有源源不断的请求继续调用Server-3服务。
如图3-9所示,随着Server-2服务内大量的请求调用线程被阻塞在对Server-3服务的调用上,Server-2服务最终也由于大量的线程发生阻塞而宕机,即Server-3服务硬生生地把Server-2服务拖垮了。
以此类推,Server-1服务也被Server-2服务拖垮了,整个服务链路将不可用,如图3-10所示。作为Server-1服务的直接访问者,用户感知到服务宕机,开始对产品频繁吐槽。
对于“熔断”,大家应该都不陌生,它并不是一个计算机术语,它在股市、电力、交通等领域都频繁出现。服务熔断与这些熔断场景的道理相同,出发点都是为了及时控制风险。在电力领域,熔断器是一种特殊的电流保护器,当电流超过某个阈值时它主动通过产生热能使熔体熔断、电路断开,这可以有效防止电气设备因短路、过电而损坏。与此对应的是,服务雪崩场景也可以通过服务熔断的方式阻断对下游服务的调用,使我们的服务不被发生故障的下游服务拖垮。
接下来介绍通过Hystrix组件实现的一个经典的熔断器。
3.3.2 Hystrix 熔断器
Hystrix熔断器将下游服务分为3种熔断状态。
- 熔断关闭(Closed)状态:默认状态,此时下游服务正常,服务调用方可以正常进行请求调用。
- 熔断开启(Open)状态:如果服务调用方在最近一段时间内对某下游服务的请求调用失败率达到某个阈值,则认为它不可用,为此下游服务设置熔断开启状态,并且不再对此下游服务进行请求调用。
- 熔断半开(Half-Open)状态:如果某下游服务的熔断已经开启了一段时间,则会自动进入熔断半开状态。此时服务调用方会允许一个请求尝试调用下游服务,以检测下游服务是否已经恢复,然后根据检测结果将熔断状态设置为关闭或开启。
3种熔断状态的转化关系如图3-11所示。
在熔断状态转化条件中,有一些地方需要人为预设阈值,在Hystrix熔断器的实现中有一些关键变量,如表3-1所示。
接下来详细介绍熔断器的工作流程。在服务调用方准备请求调用某下游服务前,熔断器会先判断此请求是否被允许调用下游服务(下文简称“请求是否被允许执行”)。
- 如果熔断器处于Closed状态,则请求被允许执行。
- 如果熔断器处于Open状态,则进一步将当前时间戳和处于Open状态时设置的时间戳 的差值与 sleepWindowInMilliseconds 的值进行比较,如果 sleepWindowInMilliseconds 较大,则说明熔断器依然维持开启状态,请求不被允许执行;而如果差值较大, 则说明熔断器处于Open状态已经足够长时间了,实际上熔断器应处于Half-Open 状态,于是请求被允许执行,以便试探下游服务是否已经恢复,且熔断器状态转 化为 Half-Open。
- 如果熔断器处于Half-Open状态,则请求不被允许执行,因为在熔断器被设置为 Half-Open状态前就已经有一个请求被允许执行了。这个逻辑可以保证熔断器在 Half-Open状态仅有一个请求被允许执行。
如果请求被允许执行,那么请求调用下游服务后,会将调用结果(成功或失败)上报给Hystrix Metrics统计器。特别的是,如果此请求是来自Half-Open状态的试探请求,则会根据调用结果直接设置最新的熔断器状态。
- 请求调用成功,熔断器状态从Half-Open转化为Closed,即熔断器认为下游服务已经恢复。
- 请求调用失败,熔断器状态从Half-Open转化为Open,即熔断器继续保持开启状。
Hystrix Metrics统计器会统计每秒请求成功与请求失败的总数,同时对最近时间窗口内的统计数据进行检查。如果熔断器处于Closed状态、此时间窗口内的请求总数不小于 requestVolumeThreshold的值,且请求失败率不低于errorThresholdPercentage的值,则熔断器认为下游服务发生故障,于是设置熔断器状态为Open。
通过基于时间窗口的请求失败率开启熔断,Hystrix可以做到对下游服务故障的有效感知,而通过在Half-Open状态允许一个请求试探执行的机制,可以使Hystrix具有对下游服务恢复的感知能力。
Hystrix官网已经处于不再维护的状态,Hystrix官方推荐使用新一代熔断器Resilience4j作为其替代品。另外,阿里巴巴开源的Sentinel项目也是一个很好的选择。这里之所以介绍Hystrix,是因为它足够经典,其熔断器方案也被业界的替代品纷纷采用,只不过新增了一些优化机制。接下来简单介绍Resilience4j和Sentinel相比Hystrix有哪些更好的机制。
3.3.3 Resilience4j 和 Sentinel 熔断器
Hystrix熔断器通过时间窗口请求失败率的策略来开启熔断,Resilience4j除了采用此策略,还采用了一种称为慢调用比例的策略来开启熔断,即通过统计时间窗口内慢速请求调用在请求总数中的比例来判断是否对下游服务开启熔断。
如表3-2所示,Resilience4j为此策略提供了两个变量。
此策略与时间窗口请求失败率策略的计算方式较为相似,只不过前者关注请求调用的耗时,后者关注请求调用的失败率。通过这两种策略的结合,Resilience4j可以从下游服务的质量和性能两个角度,综合评判下游服务是否应该开启熔断。相比于Hystrix熔断器,它有更多的视角感知下游服务是否可用。
Sentinel熔断器在这方面的表现更为全面,除实现了时间窗口请求失败率策略和慢调用比例策略外,Sentinel熔断器还支持错误计数策略:如果最近1min的请求失败数超过阈值,则会开启熔断。此策略使用请求失败数而非请求失败比例判断是否开启熔断,可以有效防止当请求量样本数太大时,需要大量的请求失败才可以达到请求失败率阈值的问题,提高了对下游服务不可用的感知能力。
目前介绍的3个开源项目的熔断器策略的对比如表 3-3所示。
3.3.4 共享资源与舱壁隔离
一般的业务服务都会定义固定大小的线程池来处理对下游服务的请求调用,在下游服务响应慢的情况下,线程池作为服务内共享资源有可能导致服务内各接口的质量相互影响。
这里举一个例子,服务A的线程池大小为150,并对外提供3个接口,即I1、I2、I3,这3个接口分别依赖3个下游服务B、C、D。在正常情况下,服务A接收的请求量不超过150个是可以正常对外提供服务的。为了方便反映问题,我们假设服务A已经接收的请求量是150个,且3个接口的请求量都是50个。
如图3-12所示,3个接口分别从线程池中获取50个线程调用下游服务。
假设服务B 在某时间接口的响应速度变得非常慢,那么正在调用服务B的线程会发生较长时间的阻塞,于是接口I1相比于其他两个接口会更长时间地占用线程。随着时间的推移,线程池中大部分可用线程都被接口I1所持有,接口I2和接口I3所能获取的可用线程都达不到50个,于是这两个接口开始拒绝请求,接口质量下滑,如图3-13所示。
由于线程池是服务内接口间的共享资源,所以一个接口过度占用可用线程,就会对其他接口产生挤压。这不是我们想看到的结果。最理想的情况是,各个接口间应该互不影响,这就要求我们对共享资源进行隔离。
在船舶工业中经常会使用舱壁将船舱分隔成多个隔离的空间,当一个船舱漏水时不会影响到其他船舱。“舱壁”的概念可以被应用在资源隔离问题上,我们需要以某种形式的“舱壁”将共享资源分隔为多个独立资源。Hystrix、Resilience4j、Sentinel都实现了舱壁隔离。
3.3.5 舱壁隔离的实现
Hystrix提供了两种舱壁隔离的策略,分别是线程池隔离和信号量隔离。这两种策略都是通过限制对共享资源的并发请求量来实现资源隔离的。
(1)线程池隔离策略
线程池隔离的实现思路很简单,就是为服务调用方的每个下游服务单独创建各自的专用线程池,每个线程池仅可用于调用同一个下游服务。
仍以3.3.4节的服务A为例,Hystrix为服务A的3个下游服务创建了3个线程池,每个线程池的大小为50,如图3-14所示。
由于服务B的响应时间变长,其对应的线程池被很快用完,之后服务A在处理接口I1的请求时,没有可用线程,于是直接拒绝调用服务B,而不从其他线程池中获取可用线程。通过将共享线程池切分为多个独立线程池,即使服务调用方的一个下游服务发生问题,也不会影响到对其他下游服务的调用。
不过,线程池隔离策略要求服务调用方为其所依赖的每个下游服务都建立线程池。如果其所依赖的下游服务较多,那么对应数量的线程池会带来较大的内存开销,也为服务器带来更大的线程调度开销和上下文切换开销。Hystrix提供的另一种策略是通过信号量PV 操作来控制共享资源并发调用,即信号量隔离策略。
(2)信号量隔离策略
我们先回顾一下信号量PV操作。信号量是一种并发控制机制,其数据结构由一个整数值S和一个指针组成,指针指向被阻塞在信号量的下一个进程。信号量的S值一般与资源使用情况有关。
- S > 0:表示当前可使用的资源数量为S。
- S <= 0:表示当前无可用资源,但有S个进程被阻塞,正在等待使用该资源。
信号量的值仅可以通过PV操作修改。PV操作由P操作和V操作组成。
- P操作:表示从信号量中获取一个资源。如果S <= 0,则P操作发生阻塞等待;否则,S值减去1,即S = S - 1。
- V操作:表示向信号量归还一个资源,S值加1,即S=S+1,并尝试唤醒队列中第一个等待信号量的进程。
信号量的S值决定了并发访问量。Hystrix为服务调用方依赖的每个下游服务都创建一 个信号量并赋初始值为X,于是实现了对每个下游服务的并发调用控制。服务调用方在试图调用某个下游服务前,先对相关信号量执行P操作,操作成功后才可调用该下游服务,再执行V操作。
比如Hystrix在服务A内为下游服务B、C、D分别创建值为50的信号量,保证每个下游服务的并发调用量不超过50个。这样一来,即使服务B的响应时间过长,在服务A内最多也就占用50个并发调用,不会对其他下游服务调用产生影响。
总结
熔断和隔离的作用?
- 熔断可以有效防止我们的服务被下游服务拖垮,同时可以在一定程度上保护下游服务。
- 隔离可以防止一个服务内各个接口之间因质量问题而相互影响。
什么是服务雪崩?
- 由于网络原因或服务自身设计问题,每个微服务一般都难以保证100%对外可用。
- 如果某服务出现了质量问题,那么与其相关的上游服务网络调用就容易出现线程阻塞的情况;
- 如果有大量的线程发生阻塞,则会导致上游服务承受较大的负载压力而发生宕机故障。
- 在微服务架构中,由于在服务间建立了依赖关系,所以一个服务的故障会不断向上传播,最终导致整个服务链路发生宕机故障。
如何解决服务雪崩?
- 可以通过服务熔断的方式阻断对下游服务的调用,使我们的服务不被发生故障的下游服务拖垮
Hystrix熔断器将下游服务分为哪三种熔断状态?
- 熔断关闭(Closed)状态:默认状态,此时下游服务正常,服务调用方可以正常进行请求调用。
- 熔断开启(Open)状态:如果服务调用方在最近一段时间内对某下游服务的请求调用失败率达到某个阈值,则认为它不可用,为此下游服务设置熔断开启状态,并且不再对此下游服务进行请求调用。
- 熔断半开(Half-Open)状态:如果某下游服务的熔断已经开启了一段时间,则会自动进入熔断半开状态。此时服务调用方会允许一个请求尝试调用下游服务,以检测下游服务是否已经恢复,然后根据检测结果将熔断状态设置为关闭或开启。
介绍熔断器的工作流程?
- 在服务调用方准备请求调用某下游服务前,熔断器会先判断此请求是否被允许调用下游服务。
- 如果熔断器处于Closed状态,则请求被允许执行。
- 如果熔断器处于Open状态,则进一步将当前时间戳和处于Open状态时设置的时间戳 的差值与 sleepWindowInMilliseconds 的值进行比较,如果 sleepWindowInMilliseconds 较大,则说明熔断器依然维持开启状态,请求不被允许执行;而如果差值较大, 则说明熔断器处于Open状态已经足够长时间了,实际上熔断器应处于Half-Open 状态,于是请求被允许执行,以便试探下游服务是否已经恢复,且熔断器状态转 化为 Half-Open。
- 如果熔断器处于Half-Open状态,则请求不被允许执行,因为在熔断器被设置为 Half-Open状态前就已经有一个请求被允许执行了。这个逻辑可以保证熔断器在 Half-Open状态仅有一个请求被允许执行。
什么是慢调用比例策略?
- 通过统计时间窗口内慢速请求调用在请求总数中的比例来判断是否对下游服务开启熔断。
什么是错误计数策略?
- 如果最近1min的请求失败数超过阈值,则会开启熔断。
- 此策略使用请求失败数而非请求失败比例判断是否开启熔断。
Hystrix提供了哪两种舱壁隔离的策略?
- 线程池隔离
- 信号量隔离
线程池隔离策略的实现思路?
- 为服务调用方的每个下游服务单独创建各自的专用线程池,每个线程池仅可用于调用同一个下游服务。
信号量隔离策略的实现思路?
- 通过信号量PV 操作来控制共享资源并发调用。
什么是信号量?
- 信号量是一种并发控制机制,其数据结构由一个整数值S和一个指针组成,指针指向被阻塞在信号量的下一个进程。
如何理解信号量的S值呢?
信号量的S值一般与资源使用情况有关。
- S > 0:表示当前可使用的资源数量为S。
- S <= 0:表示当前无可用资源,但有S个进程被阻塞,正在等待使用该资源。
如何修改信号量的值呢?
信号量的值仅可以通过PV操作修改。PV操作由P操作和V操作组成。
- P操作:表示从信号量中获取一个资源。如果S <= 0,则P操作发生阻塞等待;否则,S值减去1,即S = S - 1。
- V操作:表示向信号量归还一个资源,S值加1,即S=S+1,并尝试唤醒队列中第一个等待信号量的进程。