在一个分布式系统中,服务之间都是相互调用的,比如,
商品详情展示服务会依赖商品服务,价格服务,商品评论服务,如下图所示:

调用三个依赖服务会共享商品详情服务的线程池,如果其中的商品评论服务因不可用导致线程阻塞,这个时候后续的大量商品详情请求过来了,那么线程池里所有线程都因等待响应而被阻塞,整个商品详情服务对外不可用,从而造成服务雪崩,如图所示:

一、资源隔离之线程隔离
1、简介
货船为了进行防止漏水和火灾的扩散,会将货仓分隔为多个,如下图所示:

这种采用多个隔仓的来减少货船风险的方式被水密隔舱。
Hystrix将同样的方法运用到了服务调用者上。
Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离,从而避免服务雪崩。
如下图所示,当商品评论服务不可用时,即使商品服务独立分配的20个线程全部处于同步等待状态,也不会影响其他依赖服务的调用,如下图所示:

2、Hystrix是如何通过线程池实现线程隔离的
Hystrix通过
命令模式,将每个类型的业务请求封装成对应的命令请求,比如 商品服务->商品服务Command,价格服务 -> 价格服务Command,商品评论服务 -> 商品评论服务Command。每个类型的Command对应一个线程池。创建好的线程池是被放入到ConcurrentHashMap中。具体流程如下图:

3、Command执行方式
Command执行方式一共四种,分别如下:
- execute():以同步堵塞方式执行 run()。调用 execute() 后,hystrix先创建一个新线程运行run(),接着调用程序要在 execute() 调用处一直堵塞着,直到 run() 运行完成。
- queue():以异步非堵塞方式执行 run() 。调用 queue() 就直接返回一个 Future 对象,同时hystrix创建一个新线程运行 run(),调用程序通过 Future.get() 拿到 run() 的返回结果,而Future.get() 是堵塞执行的。
- observe():立即执行,即事件subscribe()完成注册前执行 run()/construct() 。
- 第一步是事件注册前,先调用 observe() 自动触发执行 run()/construct()(如果继承的是HystrixCommand,hystrix将创建新线程非堵塞执行run();如果继承的是HystrixObservableCommand,将以调用程序线程堵塞执行construct()),
- 第二步是从 observe() 返回后调用程序调用 subscribe() 完成事件注册,如果 run()/construct() 执行成功则触发 onNext() 和 onCompleted() ,如果执行异常则触发 onError() 。
- toObservable():延时执行,即事件subscribe()完成事件注册后执行 run()/construct() 。
- 第一步是事件注册前,调用 toObservable() 就直接返回一个 Observable<String> 对象,
- 第二步调用 subscribe() 完成事件注册后自动触发执行 run()/construct()(如果继承的是HystrixCommand,hystrix将创建新线程非堵塞执行 run() ,调用程序不必等待 run() ;如果继承的是HystrixObservableCommand,将以调用程序线程堵塞执行 construct(),调用程序等待construct()执行完才能继续往下走),如果 run()/construct() 执行成功则触发 onNext() 和 onCompleted() ,如果执行异常则触发 onError() 。
注:
execute()和queue()是HystrixCommand中的方法,observe()和toObservable()是HystrixObservableCommand 中的方法。
其中HystrixCommand是用来获取一条数据的;HystrixObservableCommand是用来获取多条数据的。从底层实现来讲,HystrixCommand其实也是利用Observable实现的(如果我们看Hystrix的源码的话,可以发现里面大量使用了RxJava),虽然HystrixCommand只返回单个的结果,但HystrixCommand的queue方法实际上是调用了
toObservable().toBlocking().toFuture(),而execute方法实际上是调用了queue().get()。
4、如何应用到实际代码中
public class GetProductInfoCommand extends HystrixCommand<ProductInfo>{
private Long productId;
public GetProductInfoCommand(Long productId) {
super(HystrixCommandGroupKey.Factory.asKey("GetProductInfoCommandGroup"));
this.productId=productId;
}
@Override
protected ProductInfo run() throws Exception {
String url = "http://127.0.0.1:8082/getProductInfo?productId="+productId;
String response = HttpClientUtils.sendGetRequest(url);
return JSONObject.parseObject(response,ProductInfo.class);
}
}
//使用
HystrixCommand<ProductInfo> command = new GetProductInfoCommand(productId);
ProductInfo productInfo=command.execute();
|
public class GetProductInfosCommand extends HystrixObservableCommand<ProductInfo> {
private String[] productIds;
public GetProductInfosCommand(String[] productIds) {
super(HystrixCommandGroupKey.Factory.asKey("GetProductInfoGroup"));
this.productIds = productIds;
}
@Override
protected Observable<ProductInfo> construct() {
return Observable.create(new Observable.OnSubscribe<ProductInfo>() {
public void call(Subscriber<? super ProductInfo> observer) {
try {
for(String productId : productIds) {
String url = "http://127.0.0.1:8082/getProductInfo?productId=" + productId;
String response = HttpClientUtils.sendGetRequest(url);
ProductInfo productInfo = JSONObject.parseObject(response, ProductInfo.class);
observer.onNext(productInfo);
}
observer.onCompleted();
} catch (Exception e) {
observer.onError(e);
}
}
}).subscribeOn(Schedulers.io());
}
}
//使用
HystrixObservableCommand<ProductInfo> getProductInfosCommand =
new GetProductInfosCommand(productIds.split(","));
Observable<ProductInfo> observable = getProductInfosCommand.observe();
//observable = getProductInfosCommand.toObservable(); // 还没有执行
observable.subscribe(new Observer<ProductInfo>() { // 等到调用subscribe然后才会执行
public void onCompleted() {
System.out.println("获取完了所有的商品数据");
}
public void onError(Throwable e) {
e.printStackTrace();
}
public void onNext(ProductInfo productInfo) {
System.out.println(productInfo);
}
});
|
5、线程池小结
执行依赖代码的线程与请求线程(比如 Tomcat 线程)分离,请求线程可以自由控制离开的时间,这也是通常说的异步编程,Hystrix是结合RxJava来实现的异步编程。通过设置线程池大小来控制并发访问量,当线程饱和的时候可以打拒绝服务,防止依赖问题扩散。

线程池隔离的优点:
(1)任何一个依赖服务都可以被隔离在自己的线程池内,即使自己的线程池资源填满了,也不会影响任何其他的服务调用
(2)服务可以随时引入一个新的依赖服务,因为即使这个新的依赖服务有问题,也不会影响其他任何服务的调用
(3)当一个故障的依赖服务重新变好的时候,可以通过清理掉线程池,瞬间恢复该服务的调用,而如果是tomcat线程池被占满,再恢复就很麻烦
(4)如果一个client调用库配置有问题,线程池的健康状况随时会报告,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机
(5)如果一个服务本身发生了修改,需要重新调整配置,此时线程池的健康状况也可以随时发现,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机
(6)除了隔离优势外,hystrix拥有专门的线程池可提供内置的并发功能,使得可以在同步调用之上构建异步的外观模式,这样就可以很方便的做异步编程(Hystrix引入了Rxjava异步框架)。
简单来说,最大的好处,就是资源隔离,确保说,任何一个依赖服务故障,不会拖垮当前的这个服务。
尽管线程池提供了线程隔离,我们的客户端底层代码也必须要有超时设置,不能无限制的阻塞以致线程池一直饱和。
线程池隔离的缺点:
线程池的主要缺点就是它增加了CPU的开销,每个业务请求(被包装成命令)在执行的时候,会涉及到请求排队,调度和上下文切换。不过Netflix公司内部认为线程隔离开销足够小,不会产生重大的成本或性能的影响。
Netflix API每天使用线程隔离处理10亿次Hystrix Command执行。 每个API实例都有40多个线程池,每个线程池中有5-20个线程(大多数设置为10个)。
对于不依赖网络访问的服务,比如只依赖内存缓存这种情况下,就不适合用线程池隔离技术,而是采用信号量隔离。
二、资源隔离之信号量
1、线程池和信号量的区别
当我们依赖的服务是极低延迟的,比如访问内存缓存,就没有必要使用线程池的方式,那样的话开销得不偿失,而是推荐使用信号量这种方式。下面这张图说明了线程池隔离和信号量隔离的主要区别:线程池方式下业务请求线程和执行依赖的服务的线程不是同一个线程;信号量方式下业务请求线程和执行依赖的线程是同一个线程。

2、如何使用信号量来隔离线程
将属性 execution.isolation.strategy 设置为 SEMAPHORE,象这样 ExecutionalsolationStrategy.SEMAPHORE,则Hystrix使用信号量而不是默认的线程池来做隔离。
public class GetCityNameCommand extends HystrixCommand<String> {
private Long cityId;
public GetCityNameCommand(Long cityId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GetCityNameGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.
withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)
));
this.cityId = cityId;
}
@Override
protected String run() throws Exception {
return LocationCache.getCityName(cityId);
}
}
|
3、信号量小结
信号量隔离的方式是限制了总的并发数,每一次请求过来,请求线程和调用依赖服务的线程是同一个线程,那么如果不涉及远程RPC调用(没有网络开销)则使用信号量来隔离,更为轻量,开销更小。
三、二者的比较
线程池隔离
|
信号量隔离
| |
线程
|
与调用线程非相同线程
|
与调用线程相同(jetty线程)
|
开销
|
排队、调度、上下文开销等
|
无线程切换,开销低
|
异步
|
支持
|
不支持
|
并发支持
|
支持(最大线程池大小)
|
支持(最大信号量上限)
|
四、总结
当请求的服务网络开销比较大的时候,或者是请求比较耗时的时候,我们最好是使用线程隔离策略,这样的话,可以保证大量的容器(tomcat)线程可用,不会由于服务原因,一直处于阻塞或等待状态,快速失败返回。
而当我们请求缓存这些服务的时候,我们可以使用信号量隔离策略,因为这类服务的返回通常会非常的快,不会占用容器线程太长时间,而且也减少了线程切换的一些开销,提高了缓存服务的效率。