一、前言
1、分布式系统中经常会出现某个基础服务不可用造成整个系统不可用的情况,这种现象称为服务雪崩效应。为了应对服务雪崩,一种常见的做法是手动服务降级,而Hystrix的出现,为我们提供了另一种方式
二、服务雪崩效应的定义
1、服务雪崩是一种因服务提供者的不可用导致服务调用者的不可用,并将不可用逐渐放大的过程,如图所示:
A为服务提供者,B为A的服务调用者,C、D是B的调用者。A的不可用导致B的不可用逐步扩大导致C、D不可用,服务雪崩就形成了

三、使用Hystrix预防服务雪崩
1、Hystrix中文意思豪猪,因其背上长满了刺而有自我保护能力。Netflix的Hystrix是一个帮助解决分布式系统交互时候超时处理、容错的类库,它同样拥有保护系统的能力
Hystrix的设计原则包括:
-
资源隔离
-
熔断器
-
命令模式
2、资源隔离
货船为了防止漏水和火灾的扩散,会将货舱分割为多个,这种隔离减少风险的模式称为Bulkheads(舱壁隔离模式),Hystrix将同样的模式运用到了服务调用者身上
在一个高度服务化的系统中,我们实现一个业务逻辑通常会依赖多个服务。如商品详情展示服务依赖商品服务、价格服务、商品评论服务

调用三个依赖服务会共享商品详情服务的线程池,如果其中商品评论服务不可用,则会导致线程池里所有线程都因等待响应而被阻塞,从而造成服务雪崩

Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离,从而避免服务雪崩。如下,当商品评论服务不可用时,及时商品评论服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用


3、熔断器模式
熔断器模式定义了熔断器开关相互转换的模式

服务健康的状况=请求失败数/请求总数。熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阀值比较决定的。
【1】当熔断器开关关闭时,请求被允许通过熔断器。如果当前健康状况高于设定的阀值,开关继续保持关闭;如果健康状况低于阀值,开关则切换为打开状态。
【2】当熔断器开关打开时,请求被禁止通过。
【3】当熔断器开关处于打开状态,经过一段时间后,熔断器会自动进入半开状态。这时,熔断器只允许一个请求通过,当请求调用成功时,熔断器恢复到关闭状态;若请求失败,继续保持打开状态,接下来的请求被禁止。
熔断器开关能保证服务调用者在调用异常服务时,快速返回结果,避免大量的同步等待。并且熔断器能在一段时间后继续侦测请求执行结果,提供恢复服务调用的可能
4、命令模式
Hystrix使用命令模式(继承HystrixCommand类)来包裹具体的服务调用逻辑(run方法),并在命令模式中添加了服务调用失败后的降级逻辑(getFallback)。同时我们在Command的构造方法中可以定义当前服务线程池和熔断器的相关参数
package com.tuniu.scc.purchase.plan.manage.hystrix;import com.netflix.hystrix.HystrixCommand;import com.netflix.hystrix.HystrixCommandGroupKey;import com.netflix.hystrix.HystrixCommandKey;import com.netflix.hystrix.HystrixCommandProperties;import com.netflix.hystrix.HystrixThreadPoolKey;import com.netflix.hystrix.HystrixThreadPoolProperties;public class Service1HystrixCommand extends HystrixCommand<String> {private String name;public Service1HystrixCommand(String name) {super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ServiceGroup")).andCommandKey(HystrixCommandKey.Factory.asKey("servcie1query")).andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("service1ThreadPool")).andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(20))// 服务线程池数量.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withCircuitBreakerErrorThresholdPercentage(60)// 熔断器关闭到打开阈值.withCircuitBreakerSleepWindowInMilliseconds(3000)// 熔断器打开到关闭的时间窗长度));this.name = name;}@Overrideprotected String run() throws Exception {return "Hello " + name + "!";}@Overrideprotected String getFallback() {return name;}}
在使用了Command模式构建了服务对象后,服务便拥有了熔断器和线程池的功能呢

Hystrix内部处理逻辑

1、构建Hystrix的Command对象,调用执行方法
2、Hystrix检查当前服务器的熔断器开关是否开启。若开启,则执行降级服务getFallback方法
3、若熔断器开关关闭,则Hystrix检查当前服务的线程池是否能接收新请求,若超过线程池已满,则执行降级服务getFallback方法
4、若线程池接收请求,则Hystrix开始执行服务调用具体逻辑run方法
5、若服务执行失败,则执行降级服务getFallback方法,并将执行结果上报Mertics更新服务健康状况
6、若服务执行超时,则执行降级服务getFallback方法,
并将执行结果上报Mertics更新服务健康状况
7、若服务执行成功,返回
正常结果
8、若服务降级方法getFallback执行成功,则返回执行
降级结果
9、若服务降级方法getFallback执行失败,则抛出异常
5、Hystrix Metrics的实现
Hystrix的Metrics中保存了当前服务的健康状况,包括服务调用总次数和服务调用失败次数等。根据Metrics的计数,熔断器从而能计算出当前服务的调用失败率,用来和设定的阀值比较从而决定熔断器状态切换逻辑,因此Metrics的实现非常重要。
1.4之前的滑动窗口实现
Hystrix在这些版本中的使用自己定义的滑动窗口数据结构来记录当前时间窗的各种事件(成功,失败,超时,线程池拒绝等)的计数.
事件产生时, 数据结构根据当前时间确定使用旧桶还是创建新桶来计数, 并在桶中对计数器经行修改.
这些修改是多线程并发执行的, 代码中有不少加锁操作,逻辑较为复杂.
1.5之后的滑动窗口实现
Hystrix在这些版本中开始使用RxJava的Observable.window()实现滑动窗口.
RxJava的window使用后台线程创建新桶, 避免了并发创建桶的问题.
同时RxJava的单线程无锁特性也保证了计数变更时的线程安全. 从而使代码更加简洁.
以下为我使用RxJava的window方法实现的一个简易滑动窗口Metrics, 短短几行代码便能完成统计功能,足以证明RxJava的强大:
@Testpublic void timeWindowTest() throws Exception{
Observable<Integer> source = Observable.interval(50, TimeUnit.MILLISECONDS).map(i -> RandomUtils.nextInt(2));
source.window(1, TimeUnit.SECONDS).subscribe(window -> {
int[] metrics = new int[2];
window.subscribe(i -> metrics[i]++,
InternalObservableUtils.ERROR_NOT_IMPLEMENTED,
() -> System.out.println("窗口Metrics:" + JSON.toJSONString(metrics)));
});
TimeUnit.SECONDS.sleep(3);
}