在复杂的分布式系统架构中,每个服务都有很多的依赖服务,而每个依赖服务都可能会故障。如果服务没有和自己的依赖服务进行隔离,那么可能某一个依赖服务的故障就会拖垮当前这个服务
Hystrix设计原则:
- 阻止任何一个依赖服务耗尽所有的资源,比如tomcat中的所有线程资源
- 避免请求排队和积压,采用限流和fail fast来控制故障
- 提供fallback降级机制来应对故障
- 使用资源隔离技术,比如bulkhead(舱壁隔离技术),swimlane(泳道技术),circuit breaker(短路技术),来限制任何一个依赖服务的故障的影响
- 通过近实时的统计/监控/报警功能,来提高故障发现的速度
- 通过近实时的属性和配置热修改功能,来提高故障处理和恢复的速度
- 保护依赖服务调用的所有故障情况,而不仅仅只是网络故障情况
如何实现:
(1)通过HystrixCommand或者HystrixObservableCommand来封装对外部依赖的访问请求,这个访问请求一般会运行在独立的线程中,资源隔离
具体实现代码:
- 一次获取一条数据
public class CommandHelloWorld extends HystrixCommand<String> {
private final String name;
public CommandHelloWorld(String name) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
@Override
protected String run() {
//调用远程服务的逻辑代码
return "Hello " + name + "!";
}
}
不要因为某一个依赖服务的故障,导致耗尽了缓存服务中的所有的线程资源去执行
- 一次获取批量数据
public class ObservableCommandHelloWorld extends HystrixObservableCommand<String> {
private final String name;
public ObservableCommandHelloWorld(String name) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
@Override
protected Observable<String> construct() {
return Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> observer) {
try {
if (!observer.isUnsubscribed()) {
//onNext的会调用执行command时的onNext方法
observer.onNext("Hello " + name + "!");
observer.onNext("Hi " + name + "!");
observer.onCompleted();
}
} catch (Exception e) {
observer.onError(e);
}
}
} ).subscribeOn(Schedulers.io());
}
}
上述为线程池的隔离,如果使用信号量的隔离需要修改成如下:
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)));
command的四种调用方式
同步:
new CommandHelloWorld(“World”).execute()
new ObservableCommandHelloWorld(“World”).toBlocking().toFuture().get()
如果observable command只会返回一条数据,那么可以调用上面的模式,去同步执行,返回一条数据异步:
new CommandHelloWorld(“World”).queue()
new ObservableCommandHelloWorld(“World”).toBlocking().toFuture()
对command调用queue(),仅仅将command放入线程池的一个等待队列,就立即返回,拿到一个Future对象,后面可以做一些其他的事情,然后过一段时间对future调用get()方法获取数据
- 对于ObservableCommand有两种执行方式
//这种方法会立即调用执行
Observable<String> fWorld = new CommandHelloWorld("World").observe();
//会延迟调用执行,等到下面subscribe的时候才会执行
//Observable<String> fWorld = new ObservableCommandHelloWorld("World").toObservable();
assertEquals("Hello World!", fWorld.toBlocking().single());
fWorld.subscribe(new Observer<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onNext(String v) {
System.out.println("onNext: " + v);
}
});
(2)对于超出我们设定阈值的服务调用,直接进行超时,不允许其耗费过长时间阻塞住。这个超时时间默认是99.5%的访问时间,但是一般我们可以自己设置一下
(3)为每一个依赖服务维护一个独立的线程池,或者是semaphore,当线程池已满时,直接拒绝对这个服务的调用
(4)对依赖服务的调用的成功次数,失败次数,拒绝次数,超时次数,进行统计
(5)如果对一个依赖服务的调用失败次数超过了一定的阈值,自动进行熔断,在一定时间内对该服务的调用直接降级,一段时间后再自动尝试恢复
(6)当一个服务调用出现失败,被拒绝,超时,短路等异常情况时,自动调用fallback降级机制
(7)对属性和配置的修改提供近实时的支持
核心概念:
线程池和信号量隔离区别:
对于线程池和信号量做资源隔离、限流和容量的限制,默认的容量都是10,区别在于,线程池隔离技术是用自己的线程去执行调用的,而信号量的隔离是直接让tomcat的线程去调用依赖服务的。线程池隔离适合大多数场景,例如堆外部依赖服务的网络请求和timeout等问题;而信号量隔离适合对内部一些比较复杂的业务逻辑处理。
command threadpoll、group和key区别:- command key,代表了一类command,一般来说,代表了底层的依赖服务的一个接口
- command group,代表了某一个底层的依赖服务,合理,一个依赖服务可能会暴露出来多个接口,每个接口就是一个command key;command group,在逻辑上去组织起来一堆command key的调用,统计信息,成功次数,timeout超时次数,失败次数,可以看到某一个服务整体的一些访问情况;command group,一般来说,推荐是根据一个服务去划分出一个线程池,command key默认都是属于同一个线程池的
设置线程池大小:
//一般来说,用这个默认的10个线程大小就够了
HystrixThreadPoolProperties.Setter()
.withCoreSize(int value)
- 队列容量修改:
控制queue满后reject的threshold,因为maxQueueSize不允许热修改,因此提供这个参数可以热修改,控制队列的最大大小。HystrixCommand在提交到线程池之前,其实会先进入一个队列中,这个队列满了之后,才会reject
//默认是5
HystrixThreadPoolProperties.Setter()
.withQueueSizeRejectionThreshold(int value)
- 当使用信号量隔离策略的时候,允许控制并发量来限流:
这个并发量的设置,跟线程池大小的设置,应该是类似的,但是基于信号量的话,性能会好很多,而且hystrix框架本身的开销会小很多。默认值是10,设置的小一些,否则因为信号量是基于调用线程去执行command的,而且不能从timeout中抽离,因此一旦设置的太大,而且有延时发生,可能瞬间导致tomcat本身的线程资源本占满
//默认是10
.withExecutionIsolationSemaphoreMaxConcurrentRequests(int value)