Hystrix 原理分析

认识Hystrix

Hystrix是Netflix公司开源的一款容错框架。 它可以完成以下几件事情:

  • 资源隔离,包括线程池隔离和信号量隔离,避免某个依赖出现问题会影响到其他依赖。
  • 断路器,当请求失败率达到一定的阈值时,会打开断路器开关,直接拒绝后续的请求,并且具有弹性机制,在后端服务恢复后,会自动关闭断路器开关。
  • 降级回退,当断路器开关被打开,服务调用超时/异常,或者资源不足(线程、信号量)会进入指定的fallback降级方法。
  • 请求结果缓存,hystrix实现了一个内部缓存机制,可以将请求结果进行缓存,那么对于相同的请求则会直接走缓存而不用请求后端服务。
  • 请求合并, 可以实现将一段时间内的请求合并,然后只对后端服务发送一次请求。

为什么要做线程隔离

在复杂的分布式系统中通常有很多依赖,如果一个应用不能对来自依赖故障进行隔离,那么应用本身就处于被拖垮的风险中。在一个高流量的网站中,某一个单一后端一旦发生延迟,将会在数秒内导致所有的应用资源被耗尽,这也就是我们常说的雪崩效应。

比如我们现在有3个业务调用分别是查询订单、查询商品、查询用户,且这三个业务请求都是依赖第三方服务-订单服务、商品服务、用户服务。三个服务均是通过RPC调用。当查询订单服务,假如线程阻塞了,这个时候后续有大量的查询订单请求过来,那么容器中的线程数量则会持续增加直致CPU资源耗尽到100%,整个服务对外不可用,集群环境下就是雪崩

订单服务不可用,导致请求积压,系统资源消耗殆尽,影响其他服务调用

 

容错限流的原理

对于基本的容错限流模式,主要有以下几点需要考量:

  • 主动超时:在调用依赖时尽快的超时,可以设置比较短的超时时间,比如2s,防止长时间的等待。
  • 限流:限制最大并发数。
  • 熔断:错误数达到阈值时,类似于保险丝熔断。
  • 隔离:隔离不同的依赖调用
  • 服务降级:资源不足时进行服务降级

断路器模式

实现流程为:当断路器的开关为关闭时(对应图中的绿色),每次请求进来都是成功的,当后端服务出现问题,请求出现的错误数达到一定的阈值,则会触发断路器为打开状态(对应图中的红色),在断路器为打开状态时,进来的所有请求都会被拒绝,当然也不是一直会拒绝请求,而是弹性的,过了特定的时间后,断路器会进入半打开状态(对应图中的黄色),这是会让一部分请求通过进行尝试,如果尝试还是有问题,则继续进入打开状态,如果尝试没有问题了,则会进入关闭状态。
 

舱壁隔离模式

舱壁隔离模式可以对资源进行隔离,类似于船的船舱都是被隔离开来的,当其中一个或者几个船舱出现问题,比如漏水,是不会影响到其他的船舱的,从而实现一种资源隔离的效果。

容错理念

  • 凡是依赖都有可能会失败。
  • 凡是资源都有限制,比如CPU、Memory、Threads、Queue。
  • 网络并不可靠,可能存在网络抖动等其他问题。
  • 延迟是应用稳定的杀手,延迟会占据大量的资源。

Hystrix核心概念

资源隔离

资源隔离的思想参考上述的舱壁隔离模式,在hystrix中提供了两种资源隔离策略:线程池隔离、信号量隔离

线程池隔离:线程池隔离会为每一个依赖(每一个服务提供者)创建一个线程池来处理来自该依赖的请求,不同的依赖线程池相互隔离,就算依赖A出故障,导致线程池资源被耗尽,也不会影响其他依赖的线程池资源。

  • 优点:支持排队和超时,支持异步调用。
  • 缺点:线程的创建一个调度会造成一定的性能开销。
  • 适用场景:适合耗时较长的接口场景,比如接口处理逻辑复杂,且与第三方中间件有交互,因为线程池模式的请求线程与实际转发线程不是同一个,所以可以保证容器有足够的线程来处理新的请求。

 Hystrix通过命令模式,将每个类型的业务请求封装成对应的命令请求,比如查询订单->订单Command,查询商品->商品Command,查询用户->用户Command。每个类型的Command对应一个线程池。创建好的线程池是被放入到ConcurrentHashMap中,比如查询订单:
 

final static ConcurrentHashMap<String, HystrixThreadPool> threadPools
 = new ConcurrentHashMap<String, HystrixThreadPool>();
threadPools.put(“hystrix-order”, 
new HystrixThreadPoolDefault(threadPoolKey, propertiesBuilder));

当第二次查询订单请求过来的时候,则可以直接从Map中获取该线程池。具体流程如下图:

信号量隔离模式: 初始化信号量currentCount=0,每进来一个请求需要先将currentCount自增,再判断currentCount的值是否小于系统最大信号量,小于则继续执行,大于则直接返回,拒绝请求。

代码如下:

public boolean tryAcquire() {
    int currentCount = this.count.incrementAndGet();
    if (currentCount > (Integer)this.numberOfPermits.get()) {
        this.count.decrementAndGet();
        return false;
    } else {
        return true;
    }
}
  • 优点:轻量,无额外的开销,只是一个简单的计数器
  • 缺点:不支持任务排队和主动超时;不支持异步调用
  • 适用场景:适合能快速响应的接口场景,不适合一些耗时较长的接口场景,因为信号量模式下的请求线程与转发处理线程是同一个,如果接口耗时过长有可能会占满容器的线程数。
隔离方式是否支持超时是否支持熔断

隔离原理

是否异步调用

资源消耗

线程池隔离支持,可直接返回支持,当线程池到达maxSize后,再请求会触发fallback接口进行熔断每个服务单独用线程池,请求线程与转发处理线程不是同一个可以是异步,也可以是同步。看调用的方法大,大量线程的上下文切换,容易造成机器负载高
信号量隔离不支持,如果阻塞,只能通过调用协议(如:socket超时才能返回)支持,当信号量达到maxConcurrentRequests后。再请求会触发fallback通过信号量的计数器,请求线程与转发处理线程是同一个同步调用,不支持异步小,只是个计数器

断路器

断路器工作原理如下:

Hystrix是基于滚筒式来处理,每一秒会产生一个buckets,每产生一个新的buckets就会移除一个最老的buckets,默认是10秒一个窗口。buckets在内存中就是一种数据结构,每个buckets会记录Metrics的相关数据,比如成功、失败、超时、拒绝。

当一个HystrixCommand进来后,会先通过allowRequest()方法判断是否允许通过该次请求,allowRequest()方法会通过isOpen判断断路器是否打开。断路器关闭,则允许通过该次请求;断路器打开,则会判断是否过了睡眠周期。没有过睡眠周期则返回false,拒绝通过该次请求,过了睡眠周期则会尝试放行。

isOpen()方法会按照(failure) / (success+failure)公式计算出失败率,如果失败率大于阈值,则会触发熔断。公式中的成功、失败的数据就来源于每10秒中一个窗口的滚筒数据。

对于一个依赖调用,要么调用成功,要么调用失败(包括异常、超时、拒绝),这些调用结果都会记录到buckets中。对于调用成功结果来说,还会判断断路器开关是否打开,如果是打开状态的话,则会关闭断路器并重置相关的计数器。

降级回退

 降级,通常指事务高峰期,为了保证核心服务正常运行,需要停掉一些不太重要的业务,或者某些服务不可用时,执行备用逻辑从故障服务中快速失败或快速返回,以保障主体业务不受影响。 Hystrix提供的降级主要是为了容错,保证当前服务不受依赖服务故障的影响,从而提高服务的健壮性。

1)哪些情况会进入降级逻辑

断路器打开
线程池/信号量资源不足
执行依赖调用超时
执行依赖调用异常
2)降级回退方式

(1)Fail Fast快速失败

快速失败是最普通的命令执行方法,命令没有重写降级逻辑。 如果命令执行发生任何类型的故障,它将直接抛出异常。

(2)Fail Fast无声失败

指在降级方法中通过返回null,空Map,空List或其他类似的响应来完成。

(3)FallBack:Static

指在降级方法中返回静态默认值。 这不会导致服务以“无声失败”的方式被删除,而是导致默认行为发生。如:应用根据命令执行返回true / false执行相应逻辑,但命令执行失败,则默认为true。

(4)FallBack:Stubbed

当命令返回一个包含多个字段的复合对象时,适合以Stubbed 的方式回退。

(5)FallBack:Cache via Network

有时,如果调用依赖服务失败,可以从缓存服务(如redis)中查询旧数据版本。由于又会发起远程调用,所以建议重新封装一个Command,使用不同的ThreadPoolKey,与主线程池进行隔离。

(6)Primary+Secondary with FallBack

有时系统具有两种行为- 主要和次要,或主要和故障转移。主要和次要逻辑涉及到不同的网络调用和业务逻辑,所以需要将主次逻辑封装在不同的Command中,使用线程池进行隔离。为了实现主从逻辑切换,可以将主次command封装在外观HystrixCommand的run方法中,并结合配置中心设置的开关切换主从逻辑。由于主次逻辑都是经过线程池隔离的HystrixCommand,因此外观HystrixCommand可以使用信号量隔离,而没有必要使用线程池隔离引入不必要的开销。
 

请求结果缓存

 

请求合并

Hystrix工作流程

对于一次依赖调用,会被封装在一个HystrixCommand对象中,调用的执行有四种种方式,一种是调用execute()方法同步调用,另一种是调用queue()方法进行异步调用

1、同步执行:​execute​,同步,直接返回结果,​该方式有注解方式的实现​

@HystrixCommand
    public String helloService(){
        return restTemplate.getForEntity("http://hello-service/hello",String.class).getBody();
    }

2、异步执行:​queue​,异步,返回Future,直到调用Future的get方法时,才会阻塞,直获取结果,在那之前都是在异步执行的,​该方式有注解方式的实现

@HystrixCommand
    public Future<String> queueService(){
        return new AsyncResult<String>(){
            public String invoke(){
                return restTemplate.getForEntity("http://hello-service/hello",String.class).getBody();
            }
        };
    }

以上两种,返回值不同,注意观察。

3、异步执行,observe方法返回一个Hot Observable,因为observe方法调用后HystrixCommand就执行了(异步),通过subscribe订阅返回的Observable获取处理结果。案例如下:

@Test
public void test03() throws InterruptedException {
    HystrixCommand<String> command = createCommand("this is test03");
    Observable<String> observe = command.observe();// observe直接执行run方法,称为Hot Observable
    System.out.println("observe之后,command就执行了");
    Thread.sleep(1000);
    observe.subscribe(new Action1<String>() {
        @Override
        public void call(String s) {
            System.out.println("通过订阅,获取执行结果:" + s);
        }
    });
}
// 控制台:
// observe之后,command就执行了
// HystrixCommand执行了!!!
// 通过订阅,获取执行结果:this is test03

4、HystrixCommand.toObservable,toObservable方法返回一个Cold Observable,因为HystrixCommand的run方法这里在调用toObservable不会立即执行,run方法需要订阅后执行。案例如下:

@Test
public void test04() throws InterruptedException {
    HystrixCommand<String> command = createCommand("this is test04");
    Observable<String> observe = command.toObservable();// toObservable不直接执行run方法
    System.out.println("未订阅,command不执行");
    Thread.sleep(1000);
    System.out.println("订阅后,command执行了");
    observe.subscribe();
    Thread.sleep(1000);
}
// 控制台:
// 未订阅,command不执行
// 订阅后,command执行了
// HystrixCommand执行了!!!

执行时会判断断路器开关是否打开,如果断路器打开,则进入getFallback()降级逻辑;如果断路器关闭,则判断线程池/信号量资源是否已满,如果资源满了,则进入getFallback()降级逻辑;如果没满,则执行run()方法。再判断执行run()方法是否超时,超时则进入getFallback()降级逻辑,run()方法执行失败,则进入getFallback()降级逻辑,执行成功则报告Metrics。Metrics中的数据包括执行成功、超时、失败等情况的数据,Hystrix会计算一个断路器的健康值,也就是失败率,当失败率超过阈值后则会触发断路器开关打开。

getFallback()逻辑为:如果没有实现fallback()方法,则直接抛出异常,另外fallback降级也是需要资源的,在fallback时需要获取一个针对fallback的信号量,只有获取成功才能fallback,获取信号量失败,则抛出异常,获取信号量成功,才会执行fallback方法并且会响应fallback方法中的内容。
 

 

————————————————
版权声明:本文为CSDN博主「大象999」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wqadxmm/article/details/119148924

 相关文章:Hystrix源码阅读(一)HystrixCommand的四种执行方式

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值