服务保障有SpringCloud集成的Hystrix(不再维护)、基于前者轻量级模块化的Resilience4j、以及阿里开源的Sentinel
本篇主要以Hystrix进行服务保障的学习。


概述

服务雪崩

在微服务架构中,系统往往会被拆分成多个服务小单元,再通过HTTPRPC进行远程调用。

如果某个服务单元由于网络抖动执行逻辑耗时等问题,造成延迟异常(超时、失败),其上游的整个微服务链路都会陷入等待

如果这种状态持续一段时间,就会引起任务积压,最后恶化成整个服务链故障

这种因下游服务的故障,导致上游服务一起跟着故障的现象,就称为服务雪崩

服务容错

针对服务雪崩的情况下,需要进行服务容错处理,也可以叫服务保障。常用的手段主要为限流开关

限流
故障前,限制调用耗时、并发小等服务的频率,防止因请求任务积压而出现自身雪崩。(耗时的查库/过长的系统调用链/三方限频率的API等)
开关
故障后,通过关闭调用故障服务,从而避免雪崩。前提是关闭后业务逻辑可以继续走下去,业务数据的完整性不会被破坏。(宁愿失败,也不愿长时间等待)
开关还分为手动开关自动开关。自动开关又称为熔断器(断路器),后续会具体讲解。

雪崩场景及应对策略

  • 硬件故障
    场景:服务器宕机、机房断电
    应对:多机房容灾、异地多活
  • 流量激增
    场景:异常流量、重试加大流量、秒杀流量激增
    应对:服务自动扩容、流量控制(限流、关闭重试)
  • 缓存穿透
    场景:应用重启导致缓存失效、短时间大批量缓存失效,导致缓存不命中而直击后端服务,造成后端服务超负荷运行,引起服务不可用
    应对:缓存预加载、缓存异步加载
  • 程序BUG
    场景:程序逻辑错误导致内存泄漏、JVM长时间FullGC
    应对:及时施放资源
  • 同步等待
    场景:服务间采用同步调用模式,同步等待造成的资源耗尽
    应对:资源隔离(不同服务采用不同线程池)、MQ解耦、熔断器模式结合超时机制

关于Hystrix

Hystrix中文名为豪猪,其设计目的也正是为了实现自我保护和容错处理。
例如,服务端中有30个服务单元,每个服务单元都有99.99%的正常运行时间,可以预期如下所示:

99.99^30 = 99.7%(正常运行时间)
0.3% * 10w = 3w(失败请求)

相当于每月仍可能有两小时的故障停机时间。

以下翻译自官方文档

Hystrix设计目标

  • 保护和控制通过第三方客户端库访问的依赖项(通常是通过网络访问)的延迟和失败。
  • 阻止复杂分布式系统中的级联故障。
  • 快速失败且迅速恢复。
  • 回退,且尽可能优雅降级。
  • 提供近乎实时的监控、报警和操作控制。

流程详解

流程图

  1. 构造一个HystrixCommandHystrixObservableCommand对象
  2. 执行命令(四种方法)
  3. 是否开启结果缓存
  4. 是否开启请求熔断
  5. 线程/队列/信号量是否已满
  6. HystrixCommand.run()HystrixObservableCommand.construct()
  7. 计算熔断器的链路健康值
  8. 失败回退逻辑fallback()
  9. 成功结果返回

1.构造一个HystrixCommandHystrixObservableCommand对象

使用 Hystrix 的第一步是创建一个 HystrixCommandHystrixObservableCommand 对象来表示你需要发给依赖服务的请求,可以向构造器传递任意参数。

若只期望依赖服务每次返回单一的回应,则:
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命令将会执行并返回结果。
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 发起订阅。

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,简称Rx,它是微软开源的一个函数库,让开发者可以利用可观察序列和LINQ风格查询操作符来编写异步和基于事件的程序,使用Rx,开发者可以用Observables表示异步数据流,用LINQ操作符查询异步数据流, 用Schedulers参数化异步数据流的并发处理,Rx可以这样定义:Rx = Observables + LINQ + Schedulers
简单来说,Rx扩展了观察者模式用于支持数据和事件序列,添加了一些操作符,它让你可以声明式的组合这些序列,而无需关注底层的实现:如线程、同步、线程安全、并发数据结构和非阻塞IO。而Rx的Observable模型让你可以像使用集合数据一样操作异步事件流,对异步事件流使用各种简单、可组合的操作。
Subject可以看成是一个桥梁或者代理,在某些ReactiveX实现中(如RxJava),它同时充当了Observer和Observable的角色。具体四种种类的分析和用法可以看这里Subject

(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对象的形式返回。如果缓存未命中,则返回订阅了执行命令的 ObservableReplySubject 对象缓存执行结果。
原理ReplySubject 能够重放执行结果,从而实现缓存的功效。
好处:不同请求路径上(不同线程)针对同一个依赖服务进行的重复请求(有同一个缓存Key),不会真实请求多次,而是走缓存。

官方Demo中的CommandUsingRequestCacheCommandUsingRequestCacheInvalidation分别讲解了简单的asKey使用和复杂的get-set-get用例。

public class CommandUsingRequestCache extends HystrixCommand<Boolean> {
    private final int value;
    protected CommandUsingRequestCache(int value) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.value = value;
    }
    ...
}

启用缓存会在 construct() run() 方法执行之前先从请求缓存中获取数据,这样不仅可以保持获取数据的一致性,还能避免不必要的线程执行,减小系统开销。
源码的详情分析可以看芋艿艿的执行结果缓存

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()

  • HystrixCommandHystrixCommand.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中。

共有六种异常:

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();
    }
}
/**
 * 围绕 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
失败回退逻辑,无超时时间,使用要小心。失败回退逻辑包含了通用的回应信息,这些回应从内存缓存中或者其他固定逻辑中得到,而不应有任何的网络依赖。如果一定要在失败回退逻辑中包含网络请求,必须将这些网络请求包装在另一个 HystrixCommandHystrixObservableCommand 中。

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:July 18th, 2021 at 09:28 pm
喵ฅฅ