Hystrix对依赖服务熔断降级实践

一,浅谈分布式服务稳定性

在分布式系统中,一个业务场景可能会依赖多个服务,若其中任何一个服务存在异常,则引发的整个链路不可用的状态。比如,前段时间订单详情页所依赖的评价服务超时,造成线上频繁告警。可见,任何一个分布式服务的系统的稳定性是保障业务能够良好运转的基本保证。

1.1 什么是系统稳定性?

1948年,诺伯特·维纳在《控制论》(Cybernetics)中,提出了“稳定性”的概念,指一个受控系统的状态在特定条件下可以维持在一个预期范围内。也就是说,受控系统的实际状态与预期状态之间的偏差越小,系统越稳定。通俗来说,就是保证系统在99.9%的时间内,是能够正常运行的。

1.2 如何保证系统稳定性

为了提高自身服务的稳定性,防止连锁失败,在通过网络调用其他服务的时候,需要考虑到下游服务故障的情况,并做相应的容错设计,也即系统容忍错误的能力。在这方面,有一些经典的设计模式,包括:

Bulkhead (隔离)。Bulkhead 一词来源于船只中的隔板,用来分离防水的密封舱。这样船只在触礁进水的时候,不会导致整条船直接沉没。在软件工程中,它被用来根据用途、用户等,对重要的资源进行隔离,防止系统中错误的蔓延。

Circuit Breaker (断路器)。和电路中的保险丝相似,断路器通过包装下游的调用,检测下游是否发生故障。如果故障率达到指定的阈值,断路器会打开,之后一段时间内的调用都会直接返回错误。在过一段时间后,它如果发现下游服务已经恢复正常,则恢复正常对下游服务的调用。它遵循 Fail Fast 的原则,让来自上游的调用不用苦等无法正常工作的下游服务的回复,也给艰难挣扎的下游服务喘息的空间。

Retry (重试)。对某些偶发短暂的错误,可能重试是一个比较好的选择,也能够一定程度上提高顶层系统的可用性。需要注意的是,并不是所有的错误、所有的接口都适合重试,重试的次数和间隔也需要进行考量。

Degradtion (降级)。当下游出现错误无法提供服务时,降级为返回默认值、缓存值等不依赖出错服务的响应,保障服务的总体可用性。

1.3 熔断器的引入

业界广为使用的熔断器有Netfilix的Hystrix(豪猪),阿里的Sentinal(哨兵)等。在复杂的分布式环境中,它能够有效地阻止连锁故障,提高系统的可用性。在字节、美团、去哪儿等互联网企业也都使用了Hystrix做为分布式服务的熔断限流利器。而在公司的SCF框架中,也集成了Hystrix的能力,可以开箱即用,快速接入。

而对于依赖服务,又可以分为强弱依赖,执行不同的稳定性保障。强依赖,用户有感知,直接监控报警。弱依赖,用不无感知,执行熔断降级。本次我们主要介入的就是,对于强依赖服务,使用线程池做到服务隔离。弱依赖服务,使用熔断器进行熔断降级。二者均需要监控埋点,并根据不同的阈值进行报警,支持一线研发快速介入。

二,Hystirx的原理及实践

Hystrix所表现出的具体特性为:服务隔离、快速熔断、优雅降级、实时监控、动态配置。

2.1 执行流程

当一个请求进入,并调用通过 Hystrix 进行保护的下游依赖,Hystrix 的执行流程如下:

A, 构建 HystrixCommand 或 HystrixObservableCommand。

Hystrix 中采用 Command Pattern 来包装对下游依赖的请求。在 Command 的实现中包含对下游进行调用的逻辑,而每一次调用则会构建一个新的 Command 实例。

根据调用产生的结果是单个还是多个,用户可以选择是继承 HystrixCommand 还是 HystrixObservableCommand。

B, 执行上一步创建的 Command 实例。

根据 Command 的类型 (HystrixCommand/ HystrixObservableCommand) 以及执行方式 (同步 / 异步 / 即时 / 延后),选择如下方法中的一种来执行 Command:

execute(): 仅适用于 HystrixCommand,同步阻塞执行,并返回下游返回的响应或抛出异常,是 queue().get() 的 shortcut。

queue(): 仅适用于 HystrixCommand,异步执行,返回 Future 对象,是 toObservable().toBlocking().toFuture() 的 shortcut。

observe(): 订阅发送响应的 Observable 对象,并返回该 Observable 的镜像。(即时执行)

toObservable(): 返回发送响应的 Observable 对象,仅当用户主动订阅后才开始执行具体逻辑并返回响应。(延时执行)

可以发现,其实四种执行方法都是基于 Observable 的实现。Observable 的实现 是RxJava,其在 Hystrix 的实现中被大量使用。简单来说,是基于数据流的发布-订阅模式,Observable 则是一个可供订阅的发布源。

C,判断是否有启用响应缓存。

如果有可用的缓存,将在这一步直接返回缓存中的值。

D,判断断路器是否打开。

若断路器打开,则不会继续执行 Command,而是直接去尝试获取 Fallback。

若断路器关闭,则继续执行。

E,判断线程池 / 排队队列 / 信号量 是否已经被占满。

根据 Command 采用的隔离策略 (后面会详细说),如果正在进行的请求数已满,则放弃执行,尝试获取 Fallback。

F,进行实际调用。

触发具体的调用实现:HystrixCommand.run() 如果调用超过了配置的超时时间,会抛出一个 TimeoutException,随后和抛出其他除了 HystrixBadRequestException 的异常一样进入获取 Fallback 的流程。

如果调用正常执行 (没有出现超时或异常),Hystrix 则在写日志、记录监控信息后返回。

G,计算线路健康程度

根据新取得的监控信息,包括:异常量、超时等,判断是否要打开或关闭断路器。

H,获取 Fallback。

在上述提到的数种情况下不执行具体调用或者调用失败,Hystrix 均会尝试获取 Fallback 响应,也就是调用用户实现的 HystrixCommand.getFallback()。Fallback 顾名思义是一种降级的举措,所以用户尽量应该让这一步不会失败。如果恰巧获取 Fallback 也是网络调用,则需要通过 HystrixCommand再包一层。

如果用户没有实现 Fallback 方法或者 Fallback 本身抛出异常,则 Hystrix 会返回直接发送 onError 通知的 Observable 实例。

I,通过 Observable 的方式,返回调用成功的响应。

2.2 核心部件

Hystrix主要包含对应的线程池、断路器、监控数据收集等部件。其中,DanamicProperties、Caching 和 Collapser 相对其他三个模块不是那么重要。

2.2.1 隔离机制

A,线程隔离

为Netflix 中使用最广泛并且推荐使用的机制。Thread 隔离级别很好理解,就是让调用在另外的线程中执行,并且相关的调用都使用一个线程池中的线程。

这样做的好处如下:

对委托线程来说,能够随时在出现超时调用时 walk away,执行 fallback 的逻辑,不会阻塞到连接超时从而拖累服务的响应时间。

对隔离效果来说,当下游服务出现超时故障时,仅仅该线程池会爆满,对使用其它线程池的不相关服务以及服务本身没有任何影响。当下游恢复健康后,线程池会再次变得可用,恢复往常状态。

对监控来说,由于线程池有多种监控数据,例如占用线程数、排队请求数、执行任务数等,当我们错误地配置了客户端或是下游服务出现性能变化我们都能够第一时间感知到并做出应对。

对项目来说,相当于引入了一个小型并发模块,可以在使用同步客户端的情况下方便构建异步系统 (Netflix API 就是这么做的)

然而,线程池的大小需要怎么设置呢?大多数场景下,默认的 10 个线程就能足够了。如果想要进一步调整的话,官方给出了一条简单有效的公式:

requests per second at peak when healthy × 99th percentile latency in seconds + some breathing room

峰值 qps * P99 响应时间 + 适当数量的额外缓冲线程

举个简单的例子,对于一个峰值每秒调用 30 次,p99 响应时间为 0.2s 的接口,可算出需要 30 * 0.2 + 4 = 10 个线程。不过在实际应用中还是需要具体情况具体分析,对于一些数值方差较大的接口,这个公式就不太适用了。

B,信号量隔离

如果你的接口响应时间非常小,无法接受线程隔离带来的开销,且信任该接口能够很快返回的话,则可以使用 Semaphore 隔离级别。原因是使用信号量隔离自然就无法像线程隔离一样在出现超时的时候直接返回,而是需要等待客户端的阻塞结束。

在 Hystrix 中,command 的执行以及 Fallback 都支持使用 Semaphore。将 execution.isolation.strategy 配置为 SEMAPHORE 即可将默认的 THREAD 隔离级别改为信号量隔离。根据接口的响应时间以及单位时间内的调用次数,你可以根据和计算线程数相似的方式计算出可允许并发执行的数量。

C,具体执行

HystrixThreadPool中,获取线程池的入口,实例如下图

后续,执行到HystrixConcurrencyStrategy,获取线程池策略

另外,线程池支持动态配置

2.2.2 断路器

断路是 Hystrix 除了隔离外的另一个重要功能。

默认情况下,当一段时间内有一定数量的请求失败和超时数量达到一定百分比,可以触发断路,之后一段时间内新进入的请求会直接失败,不会请求故障的下游服务。过去一段时间后,Hystrix 再通过放行一个请求的方式检查下游是否恢复正常,如果已恢复,则停止断路,放行后续请求,否则重复上述断路流程。

断路的功能不但能够帮助上游服务在下游故障时不浪费过多资源请求下游,可以快速返回,而且能够给下游留出一些喘息空间,有利于下游及时恢复。

在代码中,断路器会实现 HystrixCircuitBreaker 接口:

A,断路器的实现类

断路器默认实现为 HystrixCircuitBreakerImpl。上面所说的断路器行为,其实都是特指默认实现下的行为。如果用户有特定的需求,完全可以通过自己实现断路器达成。

Command 只通过外部暴露的 allowRequest 和 isOpen 等部分方法和断路器实例交互,内部可以实现多种多样的逻辑。在我们的实践中,我们发现默认的断路器实现使用单个请求进行下游是否恢复健康的判断是不够全面的,有可能会碰到单个请求恰好成功,之后放行大量请求导致发生超时的尴尬情况。因此,我们实现了具备阶梯恢复特性的断路器用于替换流量较大的 Command 下的默认断路器实现。

B,断路器的三种状态

在默认断路器实现下有三种断路器状态: CLOSED, OPEN 和 HALF_OPEN。

CLOSED 代表当前断路器关闭,请求正常通过;

OPEN 则代表断路器打开,一定时间内请求不可通过;

HALF_OPEN 代表断路器打开一段时间后,放行了一个请求到下游,待结果返回。

断路器会被并发调用,因此需要保证状态的转变是并发安全的。断路器使用 AtomicReference 保存当前的状态,当需要进行状态变更时使用 CAS 进行修改。

C,断路器状态间的转换

HystrixCircuitBreakerImpl,在滑动窗口发生滚动的时候根据最新窗口内的请求量和成功率判断是否要将断路器的状态从关闭改为打开。

假设这里由于错误率太高,断路器打开,那么在用户配置的休眠窗口内 (circuitBreakerSleepWindowInMilliseconds),它将持续拒绝进入的请求。过了这个窗口,则在下一个请求进入时将状态修改为 HALF_OPEN,具体执行逻辑在 attemptExecution 方法内:

这个请求在执行完成后,通过调用 markSuccess 或者 markNonSuccess 回调方法,决定断路器是关闭还是重新打开。

整体状态图如下:

2.2.3 运行数据

在 HystrixCommand 运行期间,会产生许多运行数据如延迟、结果、排队时间等。这些数据单独来看或者是聚合起来都对用户了解系统的运行情况并做调整非常有用。下面是 Command 一边执行一边记录运行数据的示意图:

Metrics 在顶层根据类别分为了三个类,用于更新以及获取监控数据。包括 HystrixCommandMetrics, Hystrix ThreadPoolMetrics 以及 HystrixCollapserMetrics 。

它们底层实现相似,使用桶结构存放单位时间内的数据,并保存一定时间窗口内的桶,以此提供滑动时间窗口内的 metrics 统计。每过一个单位时间,会删除最老的桶,添加最新完成统计的桶,并新开一个桶统计下一个单位时间的数据。实现基于 RxJava 的 Observable 订阅流及其 window 函数,阅读起来会比较繁琐,并且逻辑已经比较清晰了,这里就不再展开说明。这里给出一张结构图,有兴趣的同学可以自行翻看源码。

2.3 SCF中Hystrix的接入

2.3.1 自定义SetterFactory

以派单系统为例

2.3.2 自定义熔断服务接口CallBack

降级实现返回空数据或本地缓存。

2.3.3 注册熔断服务SetterFactory和CallBack

2.3.4 注册到SCFHystrixConfig

2.3.5 SCF执行的入口为HystrixFilter

具体的逻辑,可以通过分析源码

三,后记

3.1 正在完善的事

动态配置项接入

动态熔断器,发布jar包

持续完成动态引擎工具,欢迎加入

3.2 参考博文

1,www.jianshu.com/p/b9af028ef…

2,juejin.cn/post/684490…

3,tech.meituan.com/2018/04/19/…

4,www.iteye.com/blog/hot66h…

5,www.cnblogs.com/xinzhao/p/1…

6,blog.fintopia.tech/60868c70ce7…

猜你喜欢

转载自juejin.im/post/7105780186792067079