Loading... 服务保障有SpringCloud集成的 `Hystrix`(不再维护)、基于前者轻量级模块化的 `Resilience4j`、以及阿里开源的 `Sentinel`。 本篇主要以Hystrix进行服务保障的学习。 <!--more--> --- ## 概述 ### 服务雪崩 在微服务架构中,系统往往会被拆分成多个服务小单元,再通过 `HTTP`或 `RPC`进行远程调用。  如果某个服务单元由于**网络抖动**、**执行逻辑耗时**等问题,造成**延迟**或**异常(超时、失败)**,其**上游**的整个微服务链路都会陷入**等待**。  如果这种状态持续一段时间,就会引起**任务积压**,最后恶化成整个服务链**故障**。  这种因**下游**服务的故障,导致**上游**服务一起跟着故障的现象,就称为**服务雪崩**。 ### 服务容错 针对服务雪崩的情况下,需要进行**服务容错**处理,也可以叫**服务保障**。常用的手段主要为**限流**和**开关**。 **限流**: 故障前,限制调用耗时、并发小等服务的频率,防止因请求任务积压而出现自身雪崩。(耗时的查库/过长的系统调用链/三方限频率的API等) **开关**: 故障后,通过**关闭**调用故障服务,从而避免雪崩。前提是关闭后业务逻辑可以继续走下去,业务数据的完整性不会被破坏。(宁愿失败,也不愿长时间等待) 开关还分为**手动开关**和**自动开关**。自动开关又称为**熔断器**(断路器),后续会具体讲解。 ### 雪崩场景及应对策略 * **硬件故障** **场景**:服务器宕机、机房断电<br>**应对**:多机房容灾、异地多活 * **流量激增** **场景**:异常流量、重试加大流量、秒杀流量激增<br>**应对**:服务自动扩容、流量控制(限流、关闭重试) * **缓存穿透** **场景**:应用重启导致缓存失效、短时间大批量缓存失效,导致缓存不命中而直击后端服务,造成后端服务超负荷运行,引起服务不可用<br>**应对**:缓存预加载、缓存异步加载 * **程序BUG** **场景**:程序逻辑错误导致内存泄漏、JVM长时间FullGC<br>**应对**:及时施放资源 * **同步等待** **场景**:服务间采用同步调用模式,同步等待造成的资源耗尽<br>**应对**:资源隔离(不同服务采用不同线程池)、MQ解耦、熔断器模式结合超时机制 ## 关于Hystrix Hystrix中文名为**豪猪**,其设计目的也正是为了实现自我保护和容错处理。 例如,服务端中有30个服务单元,每个服务单元都有99.99%的正常运行时间,可以预期如下所示: ```java 99.99^30 = 99.7%(正常运行时间) 0.3% * 10w = 3w(失败请求) ``` 相当于每月仍可能有两小时的故障停机时间。 以下翻译自[官方文档](https://github.com/Netflix/Hystrix/wiki): ### Hystrix设计目标 * 保护和控制通过第三方客户端库访问的依赖项(通常是通过网络访问)的延迟和失败。 * 阻止复杂分布式系统中的级联故障。 * 快速失败且迅速恢复。 * 回退,且尽可能优雅降级。 * 提供近乎实时的监控、报警和操作控制。 ### 流程详解  1. **构造一个 `HystrixCommand`或 `HystrixObservableCommand`对象** 2. **执行命令(四种方法)** 3. **是否开启结果缓存** 4. **是否开启请求熔断** 5. **线程/队列/信号量是否已满** 6. **`HystrixCommand.run()`或 `HystrixObservableCommand.construct()`** 7. **计算熔断器的链路健康值** 8. **失败回退逻辑 `fallback()`** 9. **成功结果返回** #### 1.构造一个 `HystrixCommand`或 `HystrixObservableCommand`对象 使用 Hystrix 的第一步是创建一个 `HystrixCommand` 或 `HystrixObservableCommand` 对象来表示你需要发给依赖服务的请求,可以向构造器传递任意参数。 若只期望依赖服务每次返回单一的回应,则: `HystrixCommand command = new HystrixCommand(arg1, arg2);` 若期望依赖服务返回一个 `Observable`,并应用『Observer』模式监听依赖服务的回应,则: `HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);` #### 2.执行命令(四种方法) Hystrix 命令提供四种方式(`HystrixCommand` 支持所有**四种**方式,而 `HystrixObservableCommand` 仅支持**后两种**方式)来执行你包装的请求: * `execute`: 阻塞,直到收到调用的返回值(或者抛出异常)。 * `queue`: 返回一个Future,可以通过Future来获取调用的返回值。 * `observe`: 监听一个调用返回的Observable对象。 * `toObservable`: 返回一个Observable,当监听该Observable后hystrix命令将会执行并返回结果。 ```java K value = command.execute(); Future<K> fValue = command.queue(); Observable<K> ohValue = command.observe(); // hot observable(注:调用observe()方法时,请求立即发出) Observable<K> ocValue = command.toObservable(); // cold observable(注:只有在返回的ocValue上调用subscribe时,才会发出请求) ``` 具体从官方Demo的 `CommandHelloWorld`分析下四个方法: **(1)**`toObservable()`方法返回一个**未订阅**的Observe对象,只有在返回的ocValue上调用subscribe时,才会发出请求。 **(2)**`observe()`方法可从下面代码得知:在其调用 `toObservable()` 方法的基础上,向Observable 注册 `rx.subjects.ReplaySubject` 发起订阅。 ```java public Observable<R> observe() { // 使用ReplaySubject来缓冲需急切订阅的Observe ReplaySubject<R> subject = ReplaySubject.create(); // 订阅一个 Observable 并提供一个 Observer 来实现函数来处理 Observable 发出的项目以及它发出的任何错误或完成通知。 final Subscription sourceSubscription = toObservable().subscribe(subject); // 在执行已经开始时返回可以稍后订阅的ReplaySubject实例 return subject.doOnUnsubscribe(new Action0() { @Override public void call() { sourceSubscription.unsubscribe(); } }); } ``` **`补充`** [ReactiveX](https://mcxiaoke.gitbooks.io/rxdocs/content/Intro.html),简称Rx,它是微软开源的一个函数库,让开发者可以利用可观察序列和LINQ风格查询操作符来编写异步和基于事件的程序,使用Rx,开发者可以用Observables表示异步数据流,用LINQ操作符查询异步数据流, 用Schedulers参数化异步数据流的并发处理,Rx可以这样定义:`Rx = Observables + LINQ + Schedulers`。 简单来说,Rx扩展了观察者模式用于支持数据和事件序列,添加了一些操作符,它让你可以声明式的组合这些序列,而无需关注底层的实现:如线程、同步、线程安全、并发数据结构和非阻塞IO。而Rx的Observable模型让你可以像使用集合数据一样操作异步事件流,对异步事件流使用各种简单、可组合的操作。 Subject可以看成是一个桥梁或者代理,在某些ReactiveX实现中(如RxJava),它同时充当了Observer和Observable的角色。具体四种种类的分析和用法可以看这里[Subject](https://mcxiaoke.gitbooks.io/rxdocs/content/Subject.html) **(3)**`queue()`方法中 `final Future<R> delegate = toObservable().toBlocking().toFuture();`先将 Observable 转换成阻塞的 `BlockingObservable`,在返回可获得 `run()` 抽象方法执行结果的 `Future`。 而 `run()`方法又由子类实现,执行**正常的业务逻辑**。 **(4)**`execute()`方法是在调用 `queue()`方法的基础上,再调用了 `Future.get()`方法,**同步**返回 `run()`方法的执行结果。 **`补充`** `Future`代表的是异步执行的结果,当异步执行结束之后,返回的结果将会保存在Future中。 使用场景一般为:计算密集场景、处理大数据量、远程方法调用等。  **综上所述**,在内部实现中,`execute()` 是同步调用,内部会调用 `queue().get()` 方法。`queue()` 内部会调用 `toObservable().toBlocking().toFuture()`。也就是说,`HystrixCommand` 内部均通过RxJava的 `Observable` 的实现来执行请求。 #### 3.是否开启结果缓存 在 `toObservable()` 方法里,如果请求**结果缓存**这个特性被**启用**,并且**缓存命中**,则缓存的回应会立即通过一个 **`Observable`对象**的形式返回。如果缓存未命中,则返回**订阅了执行命令的 `Observable`**的 `ReplySubject` 对象缓存执行结果。 **原理**:`ReplySubject` 能够**重放**执行结果,从而实现缓存的功效。 **好处**:不同请求路径上(不同线程)针对同一个依赖服务进行的重复请求(有同一个缓存Key),不会真实请求多次,而是走缓存。 官方Demo中的 `CommandUsingRequestCache`、`CommandUsingRequestCacheInvalidation`分别讲解了简单的 `asKey`使用和复杂的 `get-set-get`用例。 ```java public class CommandUsingRequestCache extends HystrixCommand<Boolean> { private final int value; protected CommandUsingRequestCache(int value) { super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); this.value = value; } ... } ``` 启用缓存会在 `construct()` 或 ` run()` 方法执行之前先从请求缓存中获取数据,这样不仅可以保持获取数据的一致性,还能避免不必要的线程执行,减小系统开销。 源码的详情分析可以看芋艿艿的[执行结果缓存](https://www.iocoder.cn/Hystrix/command-execute-result-cache/) #### 4.是否开启请求熔断 运行hystrix命令时,会判断是否熔断,如果已经熔断,hystrix将不会执行命令,而是直接执行**失败回退逻辑** `fallback()`。等熔断关闭了,再执行命令。 #### 5.线程/队列/信号量是否已满 如果线程或队列(非线程池模式下是信号量)已满,将不会执行命令,而是直接执行**失败回退逻辑** `fallback()`。 #### 6.`HystrixCommand.run()`或 `HystrixObservableCommand.construct()` * **`HystrixCommand.run()`**: 返回回应或者抛出异常。 * **`HystrixObservableCommand.construct()`**: 返回 Observable 对象,并在回应到达时通知 observers,或者回调 onError 方法通知出现异常。 若 run() 或者 construct() 方法耗时超过了给命令设置的**超时**阈值,执行请求的线程将抛出 `TimeoutException`(若命令本身并不在其调用线程内执行,则单独的定时器线程会抛出该异常)。在这种情况下,Hystrix 将会执行失败回退逻辑,并且会忽略最终(若执行命令的线程没有被中断)返回的回应。 若命令本身并不抛出异常,并正常返回回应,Hystrix 在添加一些日志和监控数据采集之后,直接返回回应。Hystrix 在使用 run() 方法时,Hystrix 内部还是会生成一个 Observable 对象,并返回单个请求,产生一个 `onCompleted` 通知;而在 Hystrix 使用 construct() 时,会直接返回由 construct() 产生的 Observable 对象 #### 7.计算熔断器的链路健康值 Hystrix 会将请求成功,失败,被拒绝或超时信息报告给熔断器,熔断器维护一些用于统计数据用的计数器。 这些计数器产生的统计数据使得熔断器在特定的时刻,能短路某个依赖服务的后续请求,直到恢复期结束,若恢复期结束根据统计数据熔断器判定线路仍然未恢复健康,熔断器会再次关闭线路。 #### 8.失败回退逻辑 `fallback()` * `HystrixCommand` : `HystrixCommand.getFallback()`会执行用户实现的getFallback() 方法返回回应; * `HystrixObservableCommand`: 通过实现 `HystrixObservableCommand.resumeWithFallback()`,将用户实现的resumeWithFallback()返回的Observable对象直接返回。 若没有实现失败回退方法,或者失败回退方法抛出异常,Hystrix 内部还是会生成一个 Observable 对象,但它不会产生任何回应,并通过 `onError` 通知立即中止请求。Hystrix 默认会通过 onError 通知调用者发生了何种异常。 不同方法产生的行为也不一样: * **`execute()`**: 抛出异常 * **`queue()`**: 成功返回 Future 对象,但其 get() 方法被调用时,会抛出异常 * **`observe()`**: 返回 Observable 对象,当你订阅它的时候,会立即调用 subscriber 的 onError 方法中止请求 * **`toObservable()`**: 同上 **触发失败回退逻辑情况分析:** 根据上文分析的四种方式最终都会走向 `toObservable()`方法,该方法内部调用了 `applyHystrixSemantics()`去获得执行Observable。而异常的类型判断也都在该方法以及其子方法 `executeCommandAndObserve()#handleFallback`中。 共有六种异常: ```java private Observable<R> applyHystrixSemantics(final AbstractCommand<R> _cmd) { // 在ExecutionHook上执行 executionHook.onStart(_cmd); // 是否可执行(熔断开关是否开启) if (circuitBreaker.attemptExecution()) { // 获取信号量 final TryableSemaphore executionSemaphore = getExecutionSemaphore(); final AtomicBoolean semaphoreHasBeenReleased = new AtomicBoolean(false); final Action0 singleSemaphoreRelease = new Action0() { @Override public void call() { if (semaphoreHasBeenReleased.compareAndSet(false, true)) { executionSemaphore.release(); } } }; final Action1<Throwable> markExceptionThrown = new Action1<Throwable>() { @Override public void call(Throwable t) { eventNotifier.markEvent(HystrixEventType.EXCEPTION_THROWN, commandKey); } }; // 信号量 获得 if (executionSemaphore.tryAcquire()) { try { // 标记 executionResult 调用开始时间,用于跟踪userThreadExecutionTime executionResult = executionResult.setInvocationStartTime(System.currentTimeMillis()); // 获得执行的Observable,其它异常在这里判断 return executeCommandAndObserve(_cmd) .doOnError(markExceptionThrown) .doOnTerminate(singleSemaphoreRelease) .doOnUnsubscribe(singleSemaphoreRelease); } catch (RuntimeException e) { return Observable.error(e); } } else { // 二、信号量获取失败 return handleSemaphoreRejectionViaFallback(); } } else { // 一、链路处于熔断开启状态,不可执行 return handleShortCircuitViaFallback(); } } ``` ```java /** * 围绕 run() Observable 装饰了“Hystrix”功能。 * * @return R */ private Observable<R> executeCommandAndObserve(final AbstractCommand<R> _cmd) { final HystrixRequestContext currentRequestContext = HystrixRequestContext.getContextForCurrentThread(); ... final Func1<Throwable, Observable<R>> handleFallback = new Func1<Throwable, Observable<R>>() { @Override public Observable<R> call(Throwable t) { // 当尝试调用正常逻辑失败时,调用此方法重新打开断路器。使用 CAS 方式,修改断路器状态( HALF_OPEN => OPEN )。 // 设置断路器打开时间为当前时间。这样,#attemptExecution() 过一段时间,可以再次尝试执行正常逻辑。 circuitBreaker.markNonSuccess(); // 获取异常类型,返回 退回逻辑的Observable Exception e = getExceptionFromThrowable(t); executionResult = executionResult.setExecutionException(e); // 三、线程池提交任务拒绝 if (e instanceof RejectedExecutionException) { return handleThreadPoolRejectionViaFallback(e); // 四、执行命令超时,失败回退逻辑使用的是 HystrixTimer 的线程池 } else if (t instanceof HystrixTimeoutException) { return handleTimeoutViaFallback(); // 五、Hystrix请求出错,和 hystrix-javanica 子项目相关 } else if (t instanceof HystrixBadRequestException) { return handleBadRequestByEmittingError(e); } else { // 将 ExecutionHook 中的 HystrixBadRequestException 视为普通的 HystrixBadRequestException if (e instanceof HystrixBadRequestException) { eventNotifier.markEvent(HystrixEventType.BAD_REQUEST, commandKey); return Observable.error(e); } // 六、命令执行异常 return handleFailureViaFallback(e); } } }; ... } ``` 整体代码比较类似,最终都是调用 `getFallbackOrThrowException()` 方法,获得**回退逻辑 Observable**或者**异常 Observable**。 失败回退逻辑,无超时时间,使用要小心。失败回退逻辑包含了通用的回应信息,这些回应从内存缓存中或者其他固定逻辑中得到,而不应有任何的网络依赖。如果一定要在失败回退逻辑中包含网络请求,必须将这些网络请求包装在另一个 `HystrixCommand` 或 `HystrixObservableCommand` 中。 #### 9.成功结果返回  * **`execute()`**: 产生一个 Future 对象,行为同 .queue() 产生的 Future 对象一样,接着调用其 get() 方法,生成由内部产生的 Observable 对象返回的回应 * **`queue()`**: 将内部产生的 Observable 对象转换(Decorator模式)成 BlockingObservable 对象,以产生并返回 Future 对象 * **`observe()`**: 产生 Observable 对象后,立即订阅(ReplaySubject)以使命令得以执行(异步),返回该 Observable 对象,当你调用其 subscribe 方法时,重放产生的回应信息和通知给用户提供的订阅者 * **`toObservable()`**: 返回 Observable 对象,你必须调用其 subscribe 方法,以使命令得以执行。 ### 熔断器 `HystrixCircuitBreaker` HystrixCircuitBreaker 有三种状态 : * `CLOSED`: 关闭 * `OPEN`: 打开(直接调用回退逻辑) * `HALF_OPEN`: 半开 HystrixCircuitBreaker 状态变迁如下图 :  **红线**:初始时,断路器处于 `CLOSED` 状态,链路处于健康状态。当满足如下条件,断路器从 CLOSED 变成 OPEN 状态: (1)周期(可配,`HystrixCommandProperties.default_metricsRollingStatisticalWindow = 10000 ms`)内,总请求数超过一定量(可配,`HystrixCommandProperties.circuitBreakerRequestVolumeThreshold = 20`) 。 (2)错误请求占总请求数超过一定比例(可配,`HystrixCommandProperties.circuitBreakerErrorThresholdPercentage = 50%`)。 **绿线**:断路器处于 `OPEN` 状态,命令执行时,若当前时间超过断路器开启时间一定时间(`HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds = 5000 ms`),断路器变成 `HALF_OPEN` 状态,尝试调用正常逻辑,根据执行是否成功,打开或关闭熔断器**蓝线**。 ### 线程隔离对比  线程池和信号量都支持熔断和限流。相比线程池,信号量不需要线程切换,因此避免了不必要的开销。但是信号量不支持异步,也不支持超时,也就是说当所请求的服务不可用时,信号量会控制超过限制的请求立即返回,但是已经持有信号量的线程只能等待服务响应或从超时中返回,即可能出现长时间等待。线程池模式下,当超过指定时间未响应的服务,Hystrix会通过响应中断的方式通知线程立即结束并返回。 Last modification:August 22, 2022 © Allow specification reprint Like 0 喵ฅฅ
One comment
牛呀牛呀