一. Hystrix产生的背景
- 问题:分布式系统中经常会出现某个基础服务不可用造成整个系统不可用的情况, 这种现象被称为服务雪崩效应.
- 解决:Hystrix是Netflix的一个帮助解决分布式服务系统交互时超时处理和容错的类库,它具有降级和熔断的保护能力,可以优雅的解决上述问题。
二. 服务雪崩效应
2.1 服务雪崩效应的定义
- 服务雪崩效应:一种因 服务提供者 的不可用导致 服务调用者 的不可用,并将不可用 逐渐放大 的过程。
- 图示
说明:A为服务提供者, B为A的服务调用者, C和D是B的服务调用者。当A的不可用,引起B的不可用,并将不可用逐渐放大C和D时, 服务雪崩就形成了。
2.2 服务雪崩效应形成的原因
1. 原因概述
- 将服务雪崩的参与者简化为 服务提供者 和 服务调用者, 并将服务雪崩产生的过程分为以下三个阶段:
- 服务提供者不可用
- 重试加大流量
- 服务调用者不可用
2. 原因详述
(1)服务提供者不可用的原因
- 硬件故障:硬件损坏造成服务器宕机,或网络硬件故障造成服务器提供者网络不可访问。
- 程序Bug
- 缓存击穿
- 一般发生在缓存应用重启,所有缓存被清空,以及短时间内大量缓存失效时。
- 大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用。
- 用户大量请求:服务提供者超负荷运行。
(2)重试加大流量的原因
- 用户重试:在服务提供者不可用后, 用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单。
- 代码逻辑重试:服务调用端会存在大量服务异常后的重试逻辑。
(3)服务调用者不可用的原因
- 同步等待造成的资源耗尽:当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源。一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了。
2.3 服务雪崩的应对策略
1. 策略概述
- 针对造成服务雪崩的不同原因, 可以使用不同的应对策略:
- 流量控制
- 改进缓存模式
- 服务自动扩容
- 服务调用者降级服务
2. 策略详述
(1)流量控制
- 网关限流:目前一线互联网公司大量采用Nginx+Lua的网关进行流量控制
- 用户交互限流
- 采用加载动画,提高用户的忍耐等待时间.
- 提交按钮添加强制等待时间机制.
####(2)改进缓存模式
- 缓存预加载
- 同步改为异步刷新
(3)服务自动扩容
(4)服务调用者降级服务
- 资源隔离:对调用服务的线程池进行隔离.
- 对依赖服务进行分类:将依赖服务分为强依赖和若依赖.
- 强依赖服务不可用会导致当前业务中止;
- 弱依赖服务不可用不会导致当前业务的中止.
- 不可用服务的调用快速失败:一般通过 超时机制, 熔断器 和熔断后的 降级方法 来实现。
三. Hystrix的原理
3.1 Hystrix的设计原则
1. 资源隔离
-
在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如:
商品详情展示服务 会依赖 商品服务, 价格服务, 商品评论服务. 如图所示,三个依赖服务会共享商品详情服务的线程池。
-
如果其中的商品评论服务不可用, 就会出现线程池里所有线程都因等待响应而被阻塞, 从而造成服务雪崩。如图所示:
-
Hystrix的解决方法:通过将每个依赖服务分配独立的线程池进行资源隔离, 从而避免服务雪崩。
- 如下图所示, 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用.
- 如下图所示, 当商品评论服务不可用时, 即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用.
2. 熔断器模式
(1)概述
- 熔断器模式定义了熔断器开关相互转换的逻辑。
- 熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的.
- 服务的健康状况 = 请求失败数 / 请求总数
- 熔断器的开关能保证服务调用者在调用异常服务时, 快速返回结果, 避免大量的同步等待. 并且熔断器能在一段时间后继续侦测请求执行结果, 提供恢复服务调用的可能.
(2)熔断器模式的状态转换
- 当熔断器开关关闭时, 请求被允许通过熔断器。如果当前健康状况低于设定阈值, 开关继续保持关闭;如果当前健康状况低于设定阈值, 开关则切换为打开状态.
- 当熔断器开关打开时, 请求被禁止通过.
- 当熔断器开关处于打开状态, 经过一段时间后, 熔断器会自动进入半开状态, 这时熔断器只允许一个请求通过. 当该请求调用成功时, 熔断器恢复到关闭状态. 若该请求失败, 熔断器继续保持打开状态, 接下来的请求被禁止通过.
3. 命令模式
(1)概述
- Hystrix使用命令模式(继承HystrixCommand类)来包裹具体的服务调用逻辑(run方法), 并在命令模式中添加了服务调用失败后的降级逻辑(getFallback).
- 在Command的构造方法中可以定义当前服务线程池和熔断器的相关参数
- 在使用了Command模式构建了服务对象之后, 服务便拥有了熔断器和线程池的功能
- 图示
(2)示例代码
public class Service1HystrixCommand extends HystrixCommand<String> {
private final String name;
public Service1HystrixCommand(String name){
//最少配置:指定命令组名(CommandGroup)
//super(HystrixCommandGroupKey.Factory.asKey("ServiceGroup"));
supper(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;
}
// 依赖逻辑封装在run()方法中
@Override
protected Response run(){
return "Hello " + name + "--" + Thread.currentThread().getId();
}
// 超时、异常后执行该方法
@Override
protected Response getFallback(){
return "fallback";
}
}
3.2 Hystrix服务调用的内部逻辑
1. 内部逻辑图
2. 解释
- 构建Hystrix的Command对象, 调用执行方法。
- Hystrix检查当前服务的熔断器开关是否开启。
- 若熔断器开关开启, 则执行降级服务getFallback方法,并将执行结果上报Metrics。
- 若熔断器开关关闭, 则Hystrix检查当前服务的线程池是否能接收新的请求。执行3。
- 判断线程池是否已满。
- 若线程池接受请求, 则Hystrix开始执行服务调用具体逻辑run方法。执行4。
- 若线程池已满, 则执行降级服务getFallback方法,并将执行结果上报Metrics;
- 判断服务是否执行成功。
- 若服务执行成功,则执行5。
- 若服务执行失败, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况;
- 判断服务执行是否超时。
- 若服务执行成功, 返回正常结果.
- 若服务执行超时, 则执行降级服务getFallback方法, 并将执行结果上报Metrics更新服务健康状况;
- 若服务降级方法getFallback执行成功, 则返回降级结果.
- 若服务降级方法getFallback执行失败, 则抛出异常
3.3 Hystrix Metrics的实现
Hystrix的Metrics中保存了当前服务的健康状况, 包括服务调用总次数和服务调用失败次数等. 根据Metrics的计数, 熔断器从而能计算出当前服务的调用失败率, 用来和设定的阈值比较从而决定熔断器的状态切换逻辑。
四. Hystrix的使用
4.1 添加Hystrix依赖
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.5.12</version>
</dependency>
4.2 通过继承HystrixCommand来封装依赖调用
public class CommandHelloWorld extends HystrixCommand<String> {
private final String name;
public CommandHelloWorld(String name) {
super(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"));
this.name = name;
}
// 在这里调用依赖
@Override
protected String run() throws Exception {
return "Hello " + name + "--" + Thread.currentThread().getId();
}
// 超时、异常后执行该方法
@Override
protected String getFallback() {
return "fallback";
}
}
- run方法:run方法体中进行依赖调用
- getFallback方法:当run方法中的依赖调用在设置的时间内超时、异常(除了HystrixBadRequestException)的频率超过阈值,后续对这个依赖的调用将直接执行getFallback方法,待冷却一段时间后,对这个依赖的调用会重新进入run方法执行。
4.3 执行封装的依赖调用
1. 同步执行
-
调用execute方法:即为同步执行,当前线程将一直阻塞,直到获取结果。
-
示例代码
public void testSynchronous() { CommandHelloWorld commandHelloWorld = new CommandHelloWorld("jack"); System.out.print(commandHelloWorld.execute() + "--" + Thread.currentThread().getId()); }
-
说明
- 依赖调用线程和主线程不是同一个,实现了线程隔离。
- HystrixCommand默认的调用超时时间是1000毫秒
2. 异步执行
- 调用queue方法:即为异步执行,不阻塞当前线程,返回一个Future对象。
- 示例代码
public void testAsynchronous() throws Exception {
CommandHelloWorld commandHelloWorld = new CommandHelloWorld("jack");
Future<String> future = commandHelloWorld.queue();
System.out.println(future.get() + "--" + Thread.currentThread().getId());
}
五. Hystrix的属性配置
5.1 概述
- HystrixCommand有一个常用的构造方法:HystrixCommand(HystrixCommand.Setter setter)
5.2 HystrixCommand.Setter的属性
1. 属性概述
protected final HystrixCommandGroupKey groupKey;
protected HystrixCommandKey commandKey;
protected HystrixThreadPoolKey threadPoolKey;
protected com.netflix.hystrix.HystrixCommandProperties.Setter commandPropertiesDefaults;
protected com.netflix.hystrix.HystrixThreadPoolProperties.Setter threadPoolPropertiesDefaults;
2. 属性讲解
- HystrixCommandGroupKey
- 表示该命令属于哪一个组,HystrixCommandGroupKey就是用于定义分组名称。
- 每个HystrixCommand必须要配置一个分组名。
- HystrixCommandKey
- Hystrix使用单例模式存储HystrixCommand,HystrixCommandKey用于定义该命令的名称 。
- 如果没有定义这个名字,Hystrix会使用其类名作为其名字。
- HystrixThreadPoolKey
- 它表示HystrixCommand所在的线程池/信号量。
- 如果该参数不设置,则使用HystrixCommandGroupKey作为HystrixThreadPoolKey,这种情况下同一个HystrixCommandGroupKey下的依赖调用共用同一个线程池。
- HystrixCommandProperties
- HystrixCommand的属性配置,它可以设置熔断器是否可用、熔断器熔断的错误百分比、依赖调用超时时间等。
- 它有一些默认的配置参数,如熔断器熔断的错误百分比默认值是50%、依赖调用超时时间默认值是1000毫秒。
- HystrixThreadPoolProperties
- 它是线程池的属性配置,可以通过它设置核心线程数大小、最大线程数、任务队列大小等,
- 它也有一些默认的配置参数。
3. 关系
- CommandKey,依赖的名字。每个CommandKey代表一个依赖抽象,相同的依赖要使用相同的CommandKey名称。依赖隔离的根本就是对相同CommandKey的依赖做隔离。
- CommandGroup:依赖分组,用于对依赖进行分组。
- ThreadPoolKey:线程池/信号。当对同一业务依赖做隔离时使用CommandGroup做区分,但是对同一依赖的不同远程调用,例如一个是redis,一个是http,可以使用HystrixThreadPoolKey做隔离区分。即虽然在业务上都是相同的组,但是需要在资源上做隔离时,可以使用HystrixThreadPoolKey区分。
- 关系:将多个服务分为多个依赖分组(CommandGroup),每个分组有一个依赖名字(CommandKey),每个依赖里可以再使用ThreadPoolKey做区分。
5.3 示例
public class CustomeCommand extends HystrixCommand<String> {
protected CustomeCommand(String input) {
//设置HystrixCommand的属性
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("CustomeGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("CustomeKey"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("CustomeThreadPool"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)) //隔离方式
.withCircuitBreakerErrorThresholdPercentage(60)//熔断器关闭到打开阈值
.withCircuitBreakerSleepWindowInMilliseconds(3000);//熔断器打开到关闭的时间窗长度
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
.withCoreSize(10))
);
}
@Override
protected String run() throws Exception {
//访问真正的服务
return "hello World!";
}
//服务降级
@Override
protected String getFallback() {
return "exeucute Falled";
}
//请求缓存
@Override
protected String getCacheKey() {
return "";
}
}
六. Hystrix的隔离方式:ThreadPoolKey表示(线程池/信号量)
Hystrix支持线程隔离和信号量隔离
6.1 线程隔离
- 概述:不同的依赖调用分配到不同的线程池中执行,使用线程对依赖调用进行隔离,上述的示例代码就是使用线程隔离。
- 优点
- 隔离性能好
- 可设置短路机制。即依赖调用失败后执行getFallback()或依赖调用熔断后,一段时间内对该依赖的调用将直接返回失败。
- 缺点:线程切换的性能损耗
6.2 信号量隔离
- 概述:可实现对依赖调用最高并发请求数的限制,每次依赖调用都会先判断信号量是否达到阈值,如果达到极限值则拒绝调用。
- 优点:不用新启线程
- 缺点:每次都需要获取信号量。
参考
防雪崩利器:熔断器 Hystrix 的原理与使用
Hystrix实现分布式系统中的故障容错
Hystrix使用
Hystrix的原理与使用