Spring Cloud Hystrix——原理分析

根据Netflix Hystrix流程图来详细了解:当一个请求调用了相关服务依赖之后Hystrix工作流程(即当访问了http://localhost:9000/ribbon-consumer 请求之后,在RIBBON-CONSUMER中是如何处理的)

工作流程

创建 HystrixCommand 或 HystrixObservableCommand 对象

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

  • HystrixCommand :用在依赖的服务返回单个操作结果的时候。
  • HystrixObservableCommand :用在依赖的服务返回多个操作结果的时候。
    ps:
    命令模式,将来自客户端的请求封装成一个对象,从而让你可以使用不同的请求对客户端进行参数化。它可以被用于实现“行为请求者” 与 “行为实现者” 的解耦,以便使两者可以适应变化。下面的示例是对命令模式的简单实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    // 接收者
    public class Receiver {
    public void action(){
    //真正的业务逻辑
    }
    }
    //抽象命令
    public interface Command {
    void excute();
    }
    //具体命令实现
    public class ConcreteCommand implements Command {
    private Receiver receiver;
    public ConcreteCommand(Receiver receiver) {
    this.receiver = receiver;
    }
    @Override
    public void excute() {
    this.receiver.action();
    }
    }
    //客户端调用
    public class Invoker {
    private Command command;
    public void setCommand(Command command) {
    this.command = command;
    }
    public void action (){
    command.excute();
    }
    }
    public class Client {
    public static void main(String[] args) {
    Receiver receiver = new Receiver();
    Command command = new ConcreteCommand(receiver);
    Invoker invoker = new Invoker();
    invoker.setCommand(command);
    invoker.action(); //客户端通过调用者来执行命令
    }
    }

从代码中,可以看到这样几个对象。

  • Receiver:接收者,它知道如何处理具体的业务逻辑。
  • Command:抽象命令,它定义了一个命令对象应具备的一系列命令操作,比如 execute()、undo()、redo() 等。当命令操作被调用的时候就会触发接收者去做具体命令对应的业务逻辑。
  • ConcreteCommand:具体的命令实现,在这里它绑定了命令操作与接收者之间的关系,execute() 命令的实现委托给了 Receiver 的 action() 函数。
  • Invoker:调用者,它持有一个命令对象,并且可以在需要的时候通过命令对象完成具体的业务逻辑。

    命令执行

    从图中可以看出有4中方式可以执行一个Hystrix命令。
  • execute():同步,阻塞方法,从依赖服务中返回一个单一的结果对象,或是在发生错误时抛出异常。其实就是调用了queue().get()。
  • queue():异步,非阻塞方法,直接返回Future对象,其中包含了服务执行结束时要返回的单一结果对象。可以先做自己的事情,做完再.get()
  • observe():订阅一个从依赖请求中返回的代表响应的Observable对象。
  • toObservable():返回一个Observable对象,只有当你订阅它时,它才会执行Hystrix命令并发射响应。
    同步调用方法execute()实际上就是调用queue().get()方法,queue()方法的调用的是toObservable().toBlocking().toFuture().也就是说,最终每一个HystrixCommand都是通过Observable来实现的,即使这些命令仅仅是返回一个简单的单个值。

    结果是否被缓存

    若当前命令的请求缓存功能已经开启,并且对于该请求的响应也在缓存中,那么缓存的结果会立即以 Observable 对象的形式返回。

    回路器是否打开

    当命令执行时,Hystrix会检查回路器是否被打开。
  • 如果回路器被打开(或者tripped),那么Hystrix就不会再执行命令,而是转接到fallback处理逻辑(对应第8步)。
  • 如果回路器关闭,那么将进入第5步,检查是否有足够的容量来执行任务。(其中容量包括线程池的容量,队列的容量等等)。

    线程池/请求队列/信号量是否已满

    如果与该命令相关的线程池、请求队列,或者信号量(不使用线程池的时候)已经被占满了,那么Hystrix就不会再执行命令,而是转接到fallback处理逻辑。
    注意:此处的线程池并非容器的线程池,而是每个依赖服务的专有线程池。Hystrix 为了保证不会因为某个依赖服务的问题影响到其他依赖服务而采用了 “舱壁模式” (Bulkhead Pattern)来隔离每个依赖的服务。

    HystrixObservableCommand.construct() 或 HystrixCommand.run()

    Hystrix 会根据编写的方法来决定采取什么样的方式去请求依赖服务。
  • HystrixCommand.run() :返回一个单一的结果,或者抛出异常。
  • HystrixObservableCommand.construct():返回一个 Observable 对象来发射多个结果,或通过 onError 发送错误通知。
    如果 run() 或 construct() 方法的执行时间超过了命令设置的超时阈值,当前处理线程会抛出 TimeoutException。这种情况下,也会跳转到 fallback 处理逻辑。

    计算回路器的健康度

    Hystrix记录了成功,失败,拒绝,超时四种报告
    这些报告用于决定回路器是否应该熔断,被熔断的点在恢复周期内无法被后来的请求访问到。

    fallback处理

    当命令执行失败的时候,Hystrix 会进入 fallback 尝试回退处理,我们通常也称为 “服务降级”。下面就是能够引发服务降级处理的几种情况:
  • 第4步,当前命令处于 “熔断 / 短路” 状态,回路器是打开的时候。
  • 第5步,当前命令的线程池、请求队列或者信号量被占满的时候。
  • 第6步,当construct()或者run()方法执行过程中抛出异常。
  • 命令执行超时
    写一个fallback方法,提供一个不需要网络依赖的通用响应,从内存缓存或者其他的静态逻辑获取数据。如果在fallback内必须需要网络的调用,那么该调用请求也必须被包装在HystrixCommand或者HystrixObservableCommand。
  • 如果你的命令是继承自HystrixCommand,那么可以通过实现HystrixCommand.getFallback()方法返回一个单个的fallback值。
  • 如果你的命令是继承自HystrixObservableCommand,那么可以通过实现HystrixObservableCommand.resumeWithFallback()方法返回一个Observable,并且该Observable能够发射出一个fallback值。
    Hystrix会把fallback方法返回的响应返回给调用者。
    如果你没有为你的命令实现fallback方法,那么当命令抛出异常时,Hystrix仍然会返回一个Observable,但是该Observable并不会发射任何的数据,并且会立即终止并调用onError()通知。通过这个onError通知,可以将造成该命令抛出异常的原因返回给调用者。

失败或不存在回退的结果将根据您如何调用Hystrix命令而有所不同:

  • execute():抛出一个异常。
  • queue():成功返回一个Future,但是如果调用get()方法,将会抛出一个异常。
  • observe():返回一个Observable,当你订阅它时,它将立即终止,并调用onError()方法。
  • toObservable():返回一个Observable,当你订阅它时,它将立即终止,并调用onError()方法。

    返回成功的响应

    如果Hystrix命令执行成功,它会将处理结果直接返回或是以Observable形式返回响应给调用者。根据你在第2步的调用方式不同,在返回Observablez之前可能会做一些转换。
    • execute():通过调用queue()来得到一个Future对象,然后调用get()方法来获取Future中包含的值。
    • queue():将Observable转换成BlockingObservable,在将BlockingObservable转换成一个Future。
    • observe():订阅返回的Observable,并且立即开始执行命令的逻辑,
    • toObservable():返回一个没有改变的Observable,你必须订阅它,它才能够开始执行命令的逻辑。

依赖隔离

舱壁模式(bulkhead pattern):一般情况我们都用一个线程池来管理所有线程,容易造成一个问题,粒度太粗,无法对线程进行分类管理,会导致局部问题影响全局。bulkhead pattern模式在于,采用多个线程池来管理线程,这样使得1个线程池资源出现问题时不会造成另一个线程池资源问题。尽量使问题最小化。
Hystrix采用舱壁模式来实现线程池的隔离,它会为每一个依赖服务创建一个独立的线程池,隔离相互之间的依赖关系,并限制对其中任何一个的并发访问。就算某个依赖服务出现延迟过高的情况,也不会拖慢其他的依赖服务。
Hystrix给了我们三种key来用于隔离。

  • CommandKey,针对相同的接口一般CommandKey值相同,目的是把HystrixCommand,HystrixCircuitBreaker,HytrixCommandMerics以及其他相关对象关联在一起,形成一个原子组。采用原生接口的话,默认值为类名;采用注解形式的话,默认值为方法名。
  • CommandGroupKey,对CommandKey分组,用于真正的隔离。相同CommandGroupKey会使用同一个线程池或者信号量。一般情况相同业务功能会使用相同的CommandGroupKey。
  • ThreadPoolKey,如果说CommandGroupKey只是逻辑隔离,那么ThreadPoolKey就是物理隔离,当没有设置ThreadPoolKey的时候,线程池或者信号量的划分按照CommandGroupKey,当设置了ThreadPoolKey,那么线程池和信号量的划分就按照ThreadPoolKey来处理,相同ThreadPoolKey采用同一个线程池或者信号量。

线程成本

在99%情况下,使用线程池隔离的延迟有9ms,对于大多数需求来说,这样的消耗是微乎其微的,更何况可为系统在稳定性的灵活性上带来巨大的提升。
对于非常低延迟请求(例如那些主要触发内存缓存的请求),开销可能太高,在这种情况下,可以使用另一种方法,如信号量,信号量可以控制单个依赖服务的并发度,信号量的开销远比线程池的开销小,但是它不允许设置超时和实现异步访问。所以,只有在依赖服务时足够可靠的情况下才使用信号量。