Hystrix内部存在一套精细、高效和简洁的流程,该流程为实现hystrix的服务降级、故障隔离、访问限流和对被依赖服务运行信息的维护奠定了坚实的基础。
hystrix整个工作流程涉及到熔断器、缓冲、线程池等组件。这些组件分别实现了故障隔离、访问合并、服务降级和访问限流等功能,正是这些组件高效密切的配合确保了服务的稳定运行。
下面是hystrix的整体工作流程图:
整个流程可以大致归纳为如下几个步骤:
HytrixCommand和HystrixObservableCommand包装了对外部依赖访问的逻辑,整个流程的第一个步骤就是实例化HystrixCommand或则HystrixObservableCommand对象。在构造这两个Command对象时,可以通过构造方法传递任何执行过程中需要的参数。
如果对外部依赖调用只返回一个结果值,那么可以实例化一个HystrixCommand对象
HystrixCommand command = new HystrixCommand(arg1, arg2);
如果在调用外部依赖时需要返回多个结果值时,可以实例化一个HystrixObservableCommand对象
HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);
Hystrix API提供了四个触发流程的方法供开发者调用,下面四个方法中前两个方法仅适用于HystrixCommand类。
其中execute()方法和queue()方法是HystrixCommand类特有的,适用于外部依赖只返回一个结果的情况下。execute()方法的实现是通过调用HystrixCommand的queue()方法获得Future,调用Future的get()方法获取最终的返回值。而无论是HystrixCommand的execute()方法还是queue()方法,最终都是通过调用toObservable()方法来实现的。只不过是HystrixCommand这两个方法返回的结果只有一个值。
如果开启了Hystrix请求结果缓存开关并且缓存了历史调用返回的结果,那么hystrix会直接将缓存的结果返回。在使用HystrixCommand和HystrixObservableCommand类封装依赖请求逻辑时,可以通过重载getCacheKey()实现结果的缓存。通过实现同类请求结果的缓存,可以在同一个请求Context中有效降低对外部依赖的实际调用次数。
以下是在两个线程中对外部依赖http请求的流程示意图:
结果缓存的好处:
如果流程中判断到依赖调用的结果并没有缓存,那么接下来的流程需要判断熔断器的开启状态。在熔断器开启的情况下,整个流程将转入调用次数控制环节,在熔断器关闭的情况下将直接调用服务降级方法返回默认结果。HystrixCircuitBreaker是Hystrix中实现熔断器功能的核心类,HystrixCommand和HystrixObservableCommand通过与HystrixCircuitBreaker交互实现了熔断器功能的开启和关闭。
下面是HystrixCircuitBreaker和Command的交互流程和内部开关控制逻辑:
熔断器开关控制条件:
在满足以上两个条件时,熔断器打开熔断开关,之后所有对外部依赖调用都将被直接断开。在开关打开时长超过试探窗口期后,熔断器将尝试放行部分外部依赖的调用,根据试探的结果决定重新开启或则关闭熔断开关。
Hystrix引入了ThreadPool和semaphore两种方式实现对同一类外部依赖调用次数的控制。如果对同一类外部依赖调用的塞满ThreadPool的队列或则超过semaphore配置的阈值,接下来的依赖调用将直接断开,通过调用降级方法返回默认值。ThreadPool和semaphore都可以实现限流功能,相对于semaphore方式,ThreadPool还实现了故障的隔离。
Hystrix为每一类外部依赖调用提供不同的ThreadPool,每一类外部依赖调用都在独立的线程中执行,任何一类依赖调用出现问题也不会影响到其他依赖调用的正常执行。通过为每次外部依赖调用分配独立的线程执行,使得外部依赖服务的故障、调用超时都不会阻塞调用线程从而有效的隔离了故障的影响。
基于以下原因我们可以考虑采用ThreadPool隔离故障:
由于线程池的引入,对外部依赖的调用增加了任务排队、任务调度和上下文切换的开销。这些开销随着外部依赖调用流量的增长会变得更大,所以需要根据外部依赖的业务特点决定是否采用 ThreadPool机制。
下面是Netflix耗时测试结果:
使用Semaphore可以控制对外部依赖并发调用的数量,Semaphore省去了任务排队、任务调度和执行线程上下文切换的开销。但是采用Semaphore机制后,调用外部依赖将在请求线程中执行,这样外部依赖调用的延时、故障都将影响服务本身。通常在外部依赖耗时小、可靠性较高的情况下选择 Semaphore机制,规避采用ThreadPool机制带来明显的耗时负担。
HystrixCommand和HystrixObservableCommand在两处地方使用了Semaphore机制:
HystrixCommand和HystrixObservableCommand封装了外部依赖调用。通过重写HystrixCommand的run()方法和HystrixObservableCommand类的construct()方法实现外部依赖的调用。
如果run()方法或则construct()方法在执行外部依赖请求出现超时,那么请求线程或则调用线程将抛出TimeOutException。Hystrix接下来直接返回调用降级方法获得的值而不管超时后外部依赖实际返回结果。除了通过抛出一个InterruptException外没有更好的方式中断外部调用线程的执行,所以建议调用外部依赖逻辑添加对InterruptException的处理以便在必要时中断请求线程的执行,而目前很多Http Client的实现并没有很好的支持该中断的相应,所以在使用Http Client发送http请求时需要合理设置超时时间以免长时间阻塞执行线程。
如果外部依赖调用正常返回,那么Hystrix metrics将进行必要的数据统计,这些统计数据关系到HystrixCircuitBreaker的正确运转。
HystrixCircuitBreaker通过维护一系列的counter记录外部依赖请求的执行情况。在外部依赖请求超时、失败、成功时,这些信息都将被记录到熔断器内。熔断器根据维护的这些信息,在符合触发条件下开启熔断功能,在条件合适的时候关闭熔断开关。
外部依赖调用成功时将通过Observable对象返回结果值,鉴于构造Command对象的不同和调用执行方法的不同,结果值可能经过一些列的转化过程,不过从本质上讲都是通过Observable对象返回结果值。
外部依赖结果值返回流程图:
Hystrix在调用外部依赖出现异常、调用超时、熔断器开关打开、并发调用流量达到ThreadPool(Semaphore)的阈值时触发服务降级。HystrixCommand通过重载getFallback()方法或则HystrixObservableCommand的resumeWithFallback()方法实现服务的降级。通过getFallback()方法只能返回一个结果值,而通过resumeWithFallback()方法返回的Observable对象可以给请求线程返回多个结果值。
一般降级方法最好不要有网络请求这类耗时不稳定的操作,如果必须实现这类操作建议对降级逻辑实行同样的隔离控制策略。