Hystrix实现原理

在了解hystrix的工作原理之前,我们先来了解一下命令模式

命令模式

命令模式的定义: 将请求封装成一个对象,从而让用户使用不同的请求把客户端参数化,以及支持可撤销和恢复的功能。

命令模式常用的对象

Command:请求封装成的对象,该对象是命令模式的主角。也就是说将请求方法封装成一个命令对象,通过操作命令对象来操作请求方法。在命令模式是有若干个请求的,需要将这些请求封装成一条条命令对象,客户端只需要调用不同的命令就可以达到将请求参数化的目的。将一条条请求封装成一条条命定对象之后,客户端发起的就是一个个命令对象了,而不是原来的请求方法!

Receiver:有命令,当然有命令的接收者对象:如果有只有命令,没有接受者,那不就是光棍司令了?没有电视机或者电脑主机,你对着电视机遥控器或者电脑键盘狂按有什么用?Receiver对象的主要作用就是收到命令后执行对应的操作。对于点击遥控器发起的命令来说,电视机就是这个Receiver对象,比如按了待机键,电视机收到命令后就执行了待机操作,进入待机状态。

Client: 但是有一个问题摆在眼前,命令对象现在已经有了,但是谁来负责创建命令呢?这里就引出了客户端Client对象,再命令模式中命令是有客户端来创建的。打个比方来说,操作遥控器的那个人,就是扮演的客户端的角色。人按下遥控器的不同按键,来创建一条条命令。

Invoker:现在创建命令的对象Client也已经露脸了,它负责创建一条条命令,那么谁来使用或者调度这个命令呢?--命令的使用者就是Invoker对象了,还是拿人,遥控器,电视机来做比喻,遥控器就是这个Invoker对象,遥控器负责使用客户端创建的命令对象。该Invoker对象负责要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。

 以上内容摘自:

设计模式之命令模式_菜鸟博客-CSDN博客_命令模式

在下面这些情况下应考虑使用命令模式。

• 使用命令模式作为 “ 回调(CallBack) "在面向对象系统中的替代。"CallBack"讲的

便是先将 一个函数登记上, 然后在以后调用此函数。

• 需要在不同的时间指定请求、 将请求排队。 一个命令对象和原先的请求发出者可以

有不同的生命期。 换言之, 原先的请求发出者可能已经不在了, 而命令对象本身仍

然是活动的。这时命令的接收者可以是在本地, 也可以在网络的另外一个地址。命

令对象可以在序列化之后传送到另外一台机器上去。

• 系统需要支持命令的撤销。命令对象可以把状态存储起来, 等到 客户端需要撤销命

令所产生的效果时, 可以调用undo()方法, 把命令所产生的效果撤销掉。命令对

象还可以提供redo()方法, 以供客户端在需要时再重新实施命令效果。

• 如果要将系统中所有的数据更新到日志里,以便在系统 崩溃时,可以根据日志读回

所有的数据更新命令, 重新 调用 Execute()方法 一条一条执行 这些命令, 从而恢

复系统在崩溃前所做的数据更新。

Netflix hystrix实现原理

工作流程图

上图摘自官网:

https://raw.githubusercontent.com/wiki/Netflix/Hystrix/images/hystrix-command-flow-chart.png

官网介绍如下:How it Works · Netflix/Hystrix Wiki · GitHub

1. 创建HystrixCommand或HystrixObservableCommand对象

首先,构建一个HystrixCommand或是HystrixObservableCommand对象,用来表示对依赖服务的操作请求, 同时传递所有需要的参数。 从其命名中我们就能知道它采用了 “命令模式” 来实现对服务调用操作的封装。 而这两个 Command 对象分别针对不同的应用场景。

  1. HystrixCommand: 用在依赖的服务返回单个操作结果的时候。
  2. HystrixObservableCommand: 用在依赖的服务返回多个操作结果的时候

2. 命令执行

从图中我们可以看到一共存在4种命令的执行方式,而Hystrix在执行时会根据创建的Command对象以及具体的情况来选择一个执行。

HystrixComrnand实现了下面两个执行方式:

execute (): 同步执行,从依赖的服务返回一个单一的结果对象,或是在发生错误的时候抛出异常。

queue (): 异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。

R value = command. execute() ;

Future<R> fValue = command. queue() ;

HystrixObservableCommand实现了另外两种执行方式:

observe () : 返回Observable对象,它代表了操作的多个结果,它是一个Hot

Observable。

toObservable(): 同样会返回Observable对象,也代表了操作的多个结果,

但它返回的是 一个Cold Observable。

Observable<R> ohValue = command.observe(};

Observable<R> ocValue = command. toObservable(};

在Hystrix的底层实现中大量地使用了RxJava, 在这里简单介绍一下RxJava的观察者-订阅者模式。

上面我们所提到的Observable对象就是RxJava中的核心内容之一,可以把它理解

为 “事件源” 或是 “被观察者”, 与其对应的Subscriber对象,可以理解为 “ 订阅者”

或是 “观察者”。 这两个对象是RxJava响应式编程的重要组成部分。

• Observable用来向订阅者Subscriber对象发布事件,Subscriber对象则在接收到事件后对其进行处理, 而在这里所指的事件 通常就是对依赖 服务的调用。

• 一个Observable可以发出多个事件, 直到结束或是 发生异常。

• Observable 对象每发出一个事件,就会调用对应观察者Subscriber对象的onNext ()方法。

• 每一个Observable的执行,最后一定会通过调用 Subscriber. onCompleted () 或者Subscriber.onError()来结束该事件的操作流。

事件源 observable 提到了两个不同的概念: HotObservable 和 ColdObservable, 分别对应了上面 command. observe ()和command.toObservable() 的返回对象。其中 HotObservable,它不论 ”事件源 ” 是否有 “订阅者”, 都会在创建后对事件进行发布,所以对于HotObservable 的每一个 “订阅者” 都有可能是从 “ 事件源 ” 的中途开始的, 并可能只是看到了整个操作的局部过程。而 Cold Observable 在没有 “订阅者” 的时候并不会发布事件, 而是进行等待, 直到有“订阅者”之后才发布事件,所以对于ColdObservable 的订阅者,它可以保证从一开始看到整个操作的全部过程。

3. 结果是否被缓存

若当前命令的请求缓存功能是被启用的, 并且该命令缓存命中, 那么缓存的结果会立即以Observable 对象的形式 返回。

 4. 断路器是否打开

在命令结果没有缓存命中的时候, Hystrix在执行命令前需要检查断路器是否为打开状态:

  • 如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑
  • 如果断路器是关闭的, 那么Hystrix跳到第5步,检查是否有可用资源来 执行命令。

5. 线程池/请求队列/信号量是否占满

如果与命令相关的线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满, 那么Hystrix也不会执行命令,而是转接到fallback处理逻辑。

需要注意的是,这里Hystrix所判断的线程池并非容器的线程池,而是每个依赖服务的专有线程池。

6. HystrixObservableCommand.construct()或HystrixCommand.run()

  • HystrixCommand.run(): 返回一个单一 的结果,或者抛出异常。
  • HystrixObservableCommand.construct(): 返回一个Observable对象来发射多个结果,或通过onError发送错误通知。

7.  计算断路器的状态

Hystrix会将“成功”、“失败”、“拒绝”、“超时” 等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行 “熔断/短路”,直到恢复期结束。若在恢复期结束后,根据统计数据判断如果还是未达到健康指标,就再次 “熔断/短路”。

8. fallback处理

当命令执行失败的时候, Hystrix会进入fallback尝试回退处理, 我们通常也称该操作为 “ 服务降级”。而能够引起服务降级处理的清况有下面几种:

• 第4步, 当前命令处于 “熔断/短路” 状态, 断路器是打开的时候。

• 第5步, 当前命令的线程池、请求队列或者信号量被占满的时候。

• 第6步, HystrixObservableCommand.construct()或HystrixCommand.run()抛出异常的时候。

9. 返回成功的响应

当Hystrix命令执行成功之后,它会将处理结果直接返回或是以Observable 的形式返回。

 

断路器原理

断路器在 HystrixCommand 和 HystrixObservableCommand 执行过程中起到了非常重要的作用,它是 Hystrix 的核心部件。那么断路器是如何决策熔断和记录信息的呢?

我们先来看看断路器 HystrixCircuitBreaker 的定义

public interface HystrixCircuitBreaker {
    public boolean allowRequest();

    public boolean isOpen();

    void markSuccess();

    public static class Factory {...}

    static class HystrixCircuitBreakerImpl implements HystrixCircuitBreaker {...}

    static class NoOpCircuitBreaker implements HystrixCircuitBreaker {...}
}

 

HystrixCircuitBreaker 主要定义了断路器的三个抽象方法

  • allowRequest():每个Hystrix命令的请求都通过它判断是否被执行。
  • isOpen(): 返回当前断路器是否打开。
  • markSuccess(): 用来闭合断路器。

另外有三个静态类

静态类 Factory 中维护了一个 Hystrix 命令与 HystrixCircuitBreaker 的关系集合: ConcurrentHashMap<String, HystrixCircuitBreaker> circuitBreakersByCommand, 其中 String 类型的 key 通过 HystrixCommandKey 定义,每一个 Hystrix 命令需要有一个 key 来标识, 同时一个 Hystrix 命令也会在该集合中找到它对应的断路器 HystrixCircuitBreaker 实例。

静态类NoOpCircuitBreaker定义了一个空的断路器实现,它允许所有请求,并且断路器状态始终是闭合的

静态类HystrixCircuitBreakerImpl 是断路器接口HystrixCircuitBreaker的实现类,在该类中定义了断路器的4个核心对象。

private final HystrixCommandProperties properties;
private final HystrixCommandMetrics metrics;

private AtomicBoolean circuitOpen = new AtomicBoolean(false);

private AtomicLong circuitOpenedOrLastTestedTime = new AtomicLong();

HystrixCommandProperties properties:断路器对应HystrixCommand实例的属性对象

HystrixCommandMetrics metrics:用于让HystrixCommand记录各类度量指标的对象。

AtomicBoolean circuitOpen:断路器是否打开的标志,默认为false

AtomicLong circuitOpenedOrLastTestedTime:断路器打开或上一次测试的时间戳。

HystrixCircuitBreakerImpl 对 HystrixCircuitBreaker接口的各个方法实现如下所示:

1.isOpen函数

public boolean isOpen() {
    if (circuitOpen.get()) {
        return true;
    }

    HealthCounts health = metrics.getHealthCounts();

    if (health.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
        return false;
    }

    if (health.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
        return false;
    } else {
        // our failure rate is too high, trip the circuit
        if (circuitOpen.compareAndSet(false, true)) {
            // if the previousValue was false then we want to set the currentTime
            circuitOpenedOrLastTestedTime.set(System.currentTimeMillis());
            return true;
        } else {
            return true;
        }
    }
}

        isOpen (): 判断断路器的打开/关闭状态。

        如果断路器打开标识为true, 则直接返回true, 表示断路器处于打开状态。否则,就从度量指标对象 metrics 中获取 HealthCounts 统计对象做进一步判断(该对象记录了 一个滚动时间窗内的请求信息快照,默认时间窗为10秒)。

        如果它的请求总数(QPS)在预设的阙值范围内就返回 false, 表示断路器处于未打开状态。该阙值的配置参数为circuitBreakerRequestVolumeThreshold,默认值为20。

        如果错误百分比在阈值范围内就返回 false, 表示断路器处于未打开状态。该阙值的配置参数为 circuitBreakerErrorThresholdPercentage, 默认值为50。

        如果上面的两个条件都不满足,则将断路器设置为打开状态 (熔断/短路)。 同时,如果是从关闭状态切换到打开状态的话,就将当前时间记录到上面提到的circuitOpenedOrLastTestedTirne 对象中。

2.  allowRequest函数

public boolean allowRequest() {
    if (properties.circuitBreakerForceOpen().get()) {
        return false;
    }
    if (properties.circuitBreakerForceClosed().get()) {
        isOpen();
        return true;
    }
    return !isOpen() || allowSingleTest();
}

        allowRequest(): 判断请求是否被允许。先根据配置对象properties中断路器判断强制打开或关闭属性是否被设置。如果强制打开,就直接返回false,拒绝请求。如果强制关闭,它会允许所有请求,但是同时也会调用 isOpen ()来执行断路器的计算逻辑, 用来模拟断路器打开/关闭的行为。 在默认情况下,断路器并不会进入这两个强制打开或关闭的分支中去,而是通过!isOpen () I I allowSingleTest ()来判断是否允许请求访问。 isOpen()之前已经介绍过,用来判断和计算当前断路器是否打开,如果是断开状态就允许请求。

public boolean allowSingleTest() {
    long timeCircuitOpenedOrWasLastTested = circuitOpenedOrLastTestedTime.get();
    if (circuitOpen.get() && System.currentTimeMillis() > timeCircuitOpenedOrWasLastTested + properties.circuitBreakerSleepWindowInMilliseconds().get()) {
        if (circuitOpenedOrLastTestedTime.compareAndSet(timeCircuitOpenedOrWasLastTested, System.currentTimeMillis())) {
            return true;
        }
    }
    return false;
}

   allowSingleTest()判断在断路器打开状态时,是否过了指定休眠时间(circuitBreakerSleepWindowInMilliseconds),如果过了休眠时间,则再次允许请求尝试访问,此时断路器处于‘半开’状态,若此时请求继续失败, 断路器又进入打开状态, 并继续等待下一个休眠窗口过去之后再次尝试;若请求成功, 则将断路器重新置于关闭状态。所以通过 allowSingleTest()与isOpen ()方法的配合,实现了断路器打开和关闭状态的切换。

3. markSuccess函数

public void markSuccess() {
    if (circuitOpen.get()) {
        if (circuitOpen.compareAndSet(true, false)) {
            metrics.resetStream();
        }
    }
}

该函数用来在 “半开路” 状态时使用。若Hystrix 命令调用成功,通过调用它将打开的断路器关闭, 并重置度量指标对象。

Netflix Hystrix断路器官方原理图如下:

 

依赖隔离

        Hystrix 使用舱壁模式来隔离彼此的依赖关系并限制对其中任何一个的并发访问。Hystrix 则使用该模式实现线程池的隔离,它会为每一个依赖服务创建一个独立的线程池,即通过对依赖服务实现线程池隔离。

线程和线程池

        客户端(库、网络调用等)在单独的线程上执行。这将它们与调用线程(Tomcat 线程池)隔离,以便调用者可以“离开”耗时过长的依赖调用。

        Hystrix 使用单独的、每个依赖项的线程池作为约束任何给定依赖项的一种方式,因此底层执行的延迟只会使该池中的可用线程饱和。

实现对依赖服务的线程池隔离的好处有:

  1. 应用自身得到完全保护,不会受不可控的依赖服务影响
  2. 可以有效降低接入新服务的风险
  3. 当失败的客户端再次恢复健康时,线程池将被清理,应用程序立即恢复健康的性能,而不是整个 Tomcat 容器不堪重负时的长时间恢复。
  4. 如果客户端库配置错误,线程池的健康状况将迅速证明这一点(通过增加的错误、延迟、超时、拒绝等)并且您可以处理它(通常通过动态属性实时)而不影响应用程序功能。
  5. 如果客户端服务更改性能特征(这种情况经常发生,足以成为一个问题)进而导致需要调整属性(增加/减少超时、更改重试等),这再次通过线程池指标(错误、延迟)变得可见、超时、拒绝),并且可以在不影响其他客户端、请求或用户的情况下进行处理。
  6. 除了隔离的好处之外,拥有专用线程池还提供了内置的并发性,可以利用它在同步客户端库之上构建异步外观(类似于 Netflix API 如何在 Hystrix 命令之上构建反应式、完全异步的 Java API)。

 

线程池的缺点

线程池的主要缺点是它们增加了计算开销。每个命令执行都涉及在单独线程上运行命令所涉及的排队、调度和上下文切换。

在设计这个系统时,Netflix 决定接受这种开销的成本以换取它提供的好处,并认为它足够小,不会对成本或性能产生重大影响。

线程成本

Hystrix 测量在子线程上执行construct()orrun()方法时的延迟以及在父线程上的总端到端时间。通过这种方式,您可以看到 Hystrix 开销(线程、指标、日志记录、断路器等)的成本。

Netflix API 使用线程隔离每天处理 10 多亿次 Hystrix 命令执行。每个 API 实例有 40 多个线程池,每个线程池有 5-20 个线程(大多数设置为 10 个)。

下图表示HystrixCommand在单个 API 实例上以每秒 60 次请求执行的情况(每台服务器每秒线程执行总数约为 350 次):

在中位数(和更低),拥有一个单独的线程没有成本。

在第 90个百分位数处,拥有单独线程的成本为 3 毫秒。

在第 99个百分位数处,拥有单独线程的成本为 9 毫秒。但是请注意,成本的增加远小于单独线程(网络请求)的执行时间增加,后者从 2 跳到 28,而成本从 0 跳到 9。

对于大多数 Netflix 用例而言,对于此类电路而言,这种开销在第 90个百分点或更高的位置上被认为是可以接受的,因为这样可以获得弹性的好处。

信号量

您可以使用信号量(或计数器)来限制对任何给定依赖项的并发调用数量,而不是使用线程池/队列大小。这允许 Hystrix 在不使用线程池的情况下减轻负载,但它不允许超时和走开。如果您信任客户端并且只想要减载,则可以使用此方法。

HystrixCommand并HystrixObservableCommand在 2 个地方支持信号量:

回退(Execution):当 Hystrix 检索回退时,它总是在调用 Tomcat 线程上执行此操作。

执行(Execution):如果将该属性设置为execution.isolation.strategy,SEMAPHORE则 Hystrix 将使用信号量而不是线程来限制调用该命令的并发父线程的数量。

您可以通过定义可以执行多少并发线程的动态属性来配置信号量的这两种用途。您应该使用与调整线程池大小时使用的类似计算来调整它们的大小(在亚毫秒时间内返回的内存中调用可以执行超过 5000rps,信号量仅为 1 或 2……但默认值为 10)。

以上内容主要整理自官网和《Spring Cloud微服务实战》

 

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐