一、为什么要引入Hystrix 的作用?
在微服务架构中,根据业务来拆分成一个个的服务,而服务与服务之间存在着依赖关系 (比如用户调商品, 商品调库存,库存调订单等等),在Spring Cloud中多个微服务之间可以用 RestTemplate+Ribbon 和 Feign 来调用。
在服务之间调用的链路上由于网络原因、资源繁忙或者自身的原因,服务并不能保证100%可用,如果单个服务出 现问题,调用这个服务就会出现线程阻塞,导致响应时间过长或不可用,此时若有大量的请求涌入,容器的线程资 源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重 后果,这就是服务故障的“雪崩”效应。
Hystrix通过隔离服务之间的访问,阻止他们之间的级联故障。
其四大作用:熔断,降级,隔离,监控
二、熔断的原理
默认条件:断路器开启或者关闭的条件
当满足一定的阀值的时候默认10秒内超过20个请求次数
当失败率达到一定的时候默认10秒内超过50%的请求失败
到达以上阀值断路器将会开启
当开启的时候所有请求都不会进行转发
一段时间之后默认是5秒这个时候断路器是半开状态会让其中一个请求进行转发。如果成功断路器会关闭若失败继续开启。
Closed状态:默认情况关闭,断路器关闭的,所有请求可以正常发起
Open状态:当我们触发熔断以后,开启断路器,处于Open状态,后续5s的所有请求都不会发送到服务器
Half-Open状态:每搁5s会尝试请求,如果5s之后,后续的请求过来后,它会发到远程服务器,如果发现能够正常接收请求,那断路器就变为Closed状态,如果请求结果失败就会继续返回到Open状态,尝试过程中就是Half-Open状态。
降级触发的三种条件:
- 熔断触发降级;
- 请求超时触发降级;
-
资源隔离
-平台隔离,部署隔离,业务隔离,服务隔离,资源隔离
CPU,内存,线程
三、隔离的两种情况(线程池隔离与信号量隔离)
上面这张图说明了线程池隔离和信号量隔离的主要区别
:线程池方式下业务请求线程和执行依赖的服务的线程不是同一个线程;信号量方式下业务请求线程和执行依赖服务的线程是同一个线程。
线程池:
Hystrix通过命令模式对发送请求的对象和执行请求的对象进行解耦,将不同类型的业务请求封装为对应的命令请求。如订单服务查询商品,查询商品请求->商品Command;商品服务查询库存,查询库存请求->库存Command。并且为每个类型的Command配置一个线程池,当第一次创建Command时,根据配置创建一个线程池,并放入ConcurrentHashMap,如商品Command:
final static ConcurrentHashMap<String, HystrixThreadPool> threadPools = new ConcurrentHashMap<String, HystrixThreadPool>();
...
if (!threadPools.containsKey(key)) {
threadPools.put(key, new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));
}
后续查询商品的请求创建Command时,将会重用已创建的线程池。线程池隔离之后的服务依赖关系:
通过将发送请求线程与执行请求的线程分离,可有效防止发生级联故障。当线程池或请求队列饱和时,Hystrix将拒绝服务,使得请求线程可以快速失败,从而避免依赖问题扩散。
***线程池的主要缺点***是增加了计算开销。每个命令的执行都在单独的线程完成,增加了排队、调度和上下文切换的开销。因此,要使用Hystrix,就必须接受它带来的开销,以换取它所提供的好处。
通常情况下,线程池引入的开销足够小,不会有重大的成本或性能影响。但对于一些访问延迟极低的服务,如只依赖内存缓存,线程池引入的开销就比较明显了,这时候使用线程池隔离技术就不适合了,我们需要考虑更轻量级的方式,如信号量隔离。
信号量隔离:
使用线程池时,发送请求的线程和执行依赖服务的线程不是同一个,而使用信号量时,发送请求的线程和执行依赖服务的线程是同一个,都是发起请求的线程。先看一个使用信号量隔离线程的示例:
public class QueryByOrderIdCommandSemaphore extends HystrixCommand<Integer> {
private final static Logger logger = LoggerFactory.getLogger(QueryByOrderIdCommandSemaphore.class);
private OrderServiceProvider orderServiceProvider;
public QueryByOrderIdCommandSemaphore(OrderServiceProvider orderServiceProvider) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("orderService"))
.andCommandKey(HystrixCommandKey.Factory.asKey("queryByOrderId"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(10)至少有10个请求,熔断器才进行错误率的计算
.withCircuitBreakerSleepWindowInMilliseconds(5000)//熔断器中断请求5秒后会进入半打开状态,放部分流量过去重试
.withCircuitBreakerErrorThresholdPercentage(50)//错误率达到50开启熔断保护
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
.withExecutionIsolationSemaphoreMaxConcurrentRequests(10)));//最大并发请求量
this.orderServiceProvider = orderServiceProvider;
}
@Override
protected Integer run() {
return orderServiceProvider.queryByOrderId();
}
@Override
protected Integer getFallback() {
return -1;
}
}
实现方式是使用一个
原子计数器或信号量
来记录当前有多少个线程在运行请求过来了先判断计数器的数值若超过设置的最大线程个数则丢弃该类型的新请求若不超过则执行计数操作请求来计数器+1。
由于Hystrix默认使用线程池做线程隔离,使用信号量隔离需要显示地将属性execution.isolation.strategy设置为ExecutionIsolationStrategy.SEMAPHORE,同时配置信号量个数,默认为10。客户端需向依赖服务发起请求时,首先要获取一个信号量才能真正发起调用,由于信号量的数量有限,当并发请求量超过信号量个数时,后续的请求都会直接拒绝,进入fallback流程。
信号量隔离主要是通过控制并发请求量,防止请求线程大面积阻塞,从而达到限流和防止雪崩的目的。
四、回退降级
降级,通常指务高峰期,为了保证核心服务正常运行,需要停掉一些不太重要的业务,或者某些服务不可用时,执行备用逻辑从故障服务中快速失败或快速返回,以保障主体业务不受影响。Hystrix提供的降级主要是为了容错,保证当前服务不受依赖服务故障的影响,从而提高服务的健壮性。要支持回退或降级处理
熔断触发降级
资源隔离触发降级
请求超时触发降级
主动降级
,大促的时候关闭非核心服务.
被动降级,
熔断降级、限流降级
当被AOP切面拦截的时候,判断是否处于熔断状态,判断是否被限流,线程池资源是否被耗尽
熔断的流程:
Hystrix的数据统计是采用滑动窗口
滑动窗口,流向控制技术
计数器
我们可以看到总共10s钟,每个熔断器默认维护了10个bucket,每秒一个bucket,当新的bucket被创建时,最旧的bucket会被抛弃。其中每个blucket维护了请求成功、失败、超时、拒绝的计数器,Hystrix负责收集并统计这些计数器。
五、熔断器的执行流程
整个流程可以大致归纳为如下几个步骤:
- 创建HystrixCommand或者HystrixObservableCommand对象
- 执行 Command
- 检查请求结果是否被缓存
- 检查是否开启了短路器
- 检查 线程池/队列/semaphore 是否已经满
- 执行 HystrixObservableCommand.construct() or HystrixCommand.run()
- 计算短路健康状况
- 调用fallback降级机制
- 返回依赖请求的真正结果