为什么要做线程隔离
比如我们现在有3个业务调用分别是查询订单、查询商品、查询用户,且这三个业务请求都是依赖第三方服务-订单服务、商品服务、用户服务。三个服务均是通过RPC调用。当查询订单服务,假如线程阻塞了,这个时候后续有大量的查询订单请求过来,那么容器中的线程数量则会持续增加直致CPU资源耗尽到100%,整个服务对外不可用,集群环境下就是雪崩。
Hystrix是如何通过线程池实现线程隔离的
Hystrix通过命令模式,将每个类型的业务请求封装成对应的命令请求,比如查询订单->订单Command,查询商品->商品Command,查询用户->用户Command。每个类型的Command对应一个线程池。创建好的线程池是被放入到ConcurrentHashMap中
1.依赖隔离概述
依赖隔离是Hystrix的核心目的。依赖隔离其实就是资源隔离,把对依赖使用的资源隔离起来,统一控制和调度。那为什么需要把资源隔离起来呢?主要有以下几点:
1.合理分配资源,把给资源分配的控制权交给用户,某一个依赖的故障不会影响到其他的依赖调用,访问资源也不受影响。
2.可以方便的指定调用策略,比如超时异常,熔断处理。
3.对依赖限制资源也是对下游依赖起到一个保护作用,避免大量的并发请求在依赖服务有问题的时候造成依赖服务瘫痪或者更糟的雪崩效应。
4.对依赖调用进行封装有利于对调用的监控和分析,类似于hystrix-dashboard的使用。
Hystrix提供了两种依赖隔离方式:线程池隔离 和 信号量隔离。如下图,线程池隔离,Hystrix可以为每一个依赖建立一个线程池,使之和其他依赖的使用资源隔离,同时限制他们的并发访问和阻塞扩张。每个依赖可以根据权重分配资源(这里主要是线程),每一部分的依赖出现了问题,也不会影响其他依赖的使用资源。
如果简单的使用异步线程来实现依赖调用会有如下问题:1、线程的创建和销毁;2、线程上下文空间的切换,用户态和内核态的切换带来的性能损耗。
使用线程池的方式可以解决第一种问题,但是第二个问题计算开销是不能避免的。Netflix在使用过程中详细评估了使用异步线程和同步线程带来的性能差异,结果表明在99%的情况下,异步线程带来的几毫秒延迟的完全可以接受的。
优点:
缺点:
Hystrix使用Command模式对依赖调用进行封装。当我们写一个调用继承HystrixCommand的时候,可以指定一个名称Command Name。如果不指定Hystrix将会使用getClass().getSimpleName()来默认获取。如果要指定,可以使用如下代码,使用HystrixCommandKey.Factory帮助类在构造函数中指定。
public HelloWorldCommand(String name)
{
//定义命令组 和 方法调用超时时间
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorldCommand")));
this.name = name;
}
而Command Group可以把一组Command归为一组。如上例代码,可以使用
HystrixCommandGroupKey.Factory.asKey来指定Command Group。一般情况下,逻辑上是同一类型的会放在同一个Command Group中。比如,获取用户相关信息的依赖可以放在一起。
虽然Hystrix可以为每个依赖建立一个线程池,但是如果依赖成千上万,建立那么多线程池肯定是不可能的。所以默认情况下,Hystrix会为每一个Command Group建立一个线程池。
Hystrix可以指定创建或关联上一个线程池,每一个线程池都有一个Key。这个线程池就是线程隔离的关键,所有的监控、缓存、调用等等都来自于这个线程池。可以通过如下代码指定线程池:
public HelloWorldCommand(String name)
{
//定义命令组 和 方法调用超时时间
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorldCommand"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));
this.name = name;
}
上文说到,默认情况下,每一个Command Group会自动创建一个线程池。那什么时候我们需要单独指定线程池呢?因为线程池主要的目的是隔离,所以当有一些依赖在一个Command Group中,但是又有隔离的必要的时候,比如一个依赖的超时会用满所有的线程池线程,而不应该影响其他的依赖。
Command模式:Hystrix中大量使用rxJAVA来实现Command模式。所有自定义的Command,不管继承于HystrixObservableCommand还是HystrixCommand,最终都继承于AbstractCommand。Thread Pool,Command Group,Command Key都在AbstractCommand这里实现。
线程池的创建和管理:Hystrix的线程池在
HystrixConcurrencyStrategy初始化,线程池是由ThreadPoolExecutor实现的。每个线程池默认初始化10个线程。Hystrix有个静态类Factory,创建的线程池会被存储在Factory中的ConcurrentHashMap中。ConcurrentHashMap的Key则是上文说到的CommandGroupKey或者指定的ThreadPoolKey。每次命令执行的时候,都会根据ThreadPoolKey去找到对应的线程池。线程池拥有一个继承于rxjava中Scheduler的HystrixContextScheduler,用于在执行命令的时候,把命令在这个线程池上调度执行。
命令的执行:以execute()方法为例,Hystrix通过toObservable()来构造命令,构造过程中,定义了整个命令执行过程中的stage(未开始、执行中、完成执行、执行异常等等)的回调和处理方法。在
executeCommandWithSpecifiedIsolation()方法中,使用exjava的subscribeOn方法,传入上文提到的HystrixContextScheduler对象,通过HystrixContextScheduler的ThreadPoolScheduler把命令submit到ThreadPoolExecutor中去执行。
对于那些本来延迟就比较小的请求(例如访问本地缓存成功率很高的请求)来说,线程池带来的开销是非常高的,这时,你可以考虑采用其他方法,例如非阻塞信号量(不支持超时),来实现依赖服务的隔离,使用信号量的开销很小。但绝大多数情况下,Netflix 更偏向于使用线程池来隔离依赖服务,因为其带来的额外开销可以接受,并且能支持包括超时在内的所有功能。