Hystrix的原理与使用
转载自:https://segmentfault.com/a/1190000005988895
http://blog.csdn.net/xiaoyu411502/article/details/50601687
Netflix的 Hystrix 是一个帮助解决分布式系统交互时超时处理和容错的类库, 它同样拥有保护系统的能力.
Hystrix的设计原则包括:资源隔离、熔断器、命令模式。
资源隔离
货船为了进行防止漏水和火灾的扩散,会将货仓分隔为多个,这种资源隔离减少风险的方式被称为:Bulkheads(舱壁隔离模式)。
Hystrix将同样的模式运用到了服务调用者上,在一个高度服务化的系统中,一个业务逻辑通常会依赖多个服务,比如:商品详情展示服务会依赖商品服务,价格服务,商品评论服务。调用三个依赖服务会共享商品详情服务的线程池。如果其中的商品评论服务不可用,就会出现线程池里所有线程都因等待响应而被阻塞,从而造成服务雪崩。Hystrix通过将每个依赖服务分配独立的线程池进行资源隔离,从而避免服务雪崩。
熔断器模式
熔断器模式定义了熔断器开关相互转换的逻辑。
服务的健康状况 = 请求失败数 / 请求总数。熔断器开关由关闭到打开的状态转换是通过当前服务健康状况和设定阈值比较决定的。
当熔断器开关关闭时,请求被允许通过熔断器。
如果当前健康状况高于设定阈值,开关继续保持关闭。如果当前健康状况低于设定阈值,开关则切换为打开状态。当熔断器开关打开时,请求被禁止通过。当熔断器开关处于打开状态,经过一段时间后,熔断器会自动进入半开状态,这时熔断器只允许一个请求通过。当该请求调用成功时,熔断器恢复到关闭状态。若该请求失败,熔断器继续保持打开状态,接下来的请求被禁止通过。
熔断器的开关能保证服务调用者在调用异常服务时,快速返回结果,避免大量的同步等待,并且熔断器能在一段时间后继续侦测请求执行结果,提供恢复服务调用的可能。
命令模式
Hystrix使用命令模式(继承HystrixCommand类)来包裹具体的服务调用逻辑(run方法),并在命令模式中添加了服务调用失败后的降级逻辑(getFallback)。
同时在Command的构造方法中可以定义当前服务线程池和熔断器的相关参数。因此在使用了Command模式构建了服务对象之后,服务便拥有了熔断器和线程池的功能。
示例
使用命令模式封装依赖逻辑:
-
public class HelloWorldCommand extends HystrixCommand<String> {
-
private final String name;
-
-
public HelloWorldCommand(String name) {
-
// //最少配置:指定命令组名(CommandGroup)
-
// super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
-
-
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
-
.andCommandKey(HystrixCommandKey.Factory.asKey("query"))
-
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ExampleThreadPool"))
-
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(20))//服务线程池数量
-
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
-
.withCircuitBreakerErrorThresholdPercentage(60) //熔断器关闭到打开阈值
-
.withCircuitBreakerSleepWindowInMilliseconds(3000)//熔断器打开到关闭的时间窗长度
-
));
-
this.name = name;
-
}
-
-
-
protected String run() {
-
// 依赖逻辑封装在run()方法中
-
return "Hello " + name + ", thread: " + Thread.currentThread().getName();
-
}
-
-
//调用实例
-
public static void main(String[] args) throws Exception {
-
//每个Command对象只能调用一次,不可以重复调用
-
HelloWorldCommand helloWorldCommand = new HelloWorldCommand("Synchronous-hystrix");
-
-
//使用execute()同步调用代码,效果等同于:helloWorldCommand.queue().get();
-
String result = helloWorldCommand.execute();
-
System.out.println("result = " + result);
-
-
helloWorldCommand = new HelloWorldCommand("Asynchronous-hystrix");
-
//异步调用,可自由控制获取结果时机,
-
Future<String> future = helloWorldCommand.queue();
-
//get操作不能超过command定义的超时时间,默认:1秒
-
result = future.get(100, TimeUnit.MILLISECONDS);
-
System.out.println("result = " + result);
-
System.out.println("mainThread = " + Thread.currentThread().getName());
-
}
-
}
运行结果:
-
result = Hello Synchronous-hystrix, thread: hystrix-ExampleThreadPool-1
-
result = Hello Asynchronous-hystrix, thread: hystrix-ExampleThreadPool-2
-
mainThread = main
注意上面有几个参数:
CommandKey,依赖命名,每个CommandKey代表一个依赖抽象,相同的依赖要使用相同的CommandKey名称。依赖隔离的根本就是对相同CommandKey的依赖做隔离。
CommandGroup,依赖分组,命令分组用于对依赖操作分组,便于统计,汇总等。CommandGroup是每个命令最少配置的必选参数,在不指定ThreadPoolKey的情况下,字面值用于对不同依赖的线程池/信号区分。
ThreadPoolKey,线程池/信号,当对同一业务依赖做隔离时使用CommandGroup做区分,但是对同一依赖的不同远程调用,例如一个是redis,一个是http,可以使用HystrixThreadPoolKey做隔离区分。即最然在业务上都是相同的组,但是需要在资源上做隔离时,是可以使用HystrixThreadPoolKey区分。
注册异步事件回调执行:
-
//注册观察者事件拦截
-
Observable<String> fs = new HelloWorldCommand("World").observe();
-
//注册结果回调事件
-
fs.subscribe(new Action1<String>() {
-
-
public void call(String result) {
-
//执行结果处理,result 为HelloWorldCommand返回的结果
-
//用户对结果做二次处理.
-
System.out.println("call : " + result);
-
}
-
});
-
-
//注册完整执行生命周期事件
-
fs.subscribe(new Observer<String>() {
-
-
public void onCompleted() {
-
// onNext/onError完成之后最后回调
-
System.out.println("execute onCompleted");
-
}
-
-
-
public void onError(Throwable e) {
-
// 当产生异常时回调
-
System.out.println("onError " + e.getMessage());
-
e.printStackTrace();
-
}
-
-
-
public void onNext(String v) {
-
// 获取结果后回调
-
System.out.println("onNext: " + v);
-
}
-
});
运行结果:
-
call : Hello World, thread: hystrix-ExampleThreadPool-3
-
onNext: Hello World, thread: hystrix-ExampleThreadPool-3
-
execute onCompleted
使用Fallback() 提供降级策略:
-
//重载HystrixCommand 的getFallback方法实现逻辑
-
public class HelloWorldCommand extends HystrixCommand<String> {
-
private final String name;
-
public HelloWorldCommand(String name) {
-
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HelloWorldGroup"))
-
/*依赖超时时间,500毫秒*/
-
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter().withExecutionTimeoutInMilliseconds(500)));
-
this.name = name;
-
}
-
-
protected String getFallback() {
-
return "exeucute Falled";
-
}
-
-
protected String run() throws Exception {
-
//sleep 1 秒,调用会超时
-
TimeUnit.MILLISECONDS.sleep(1000);
-
return "Hello " + name +" thread:" + Thread.currentThread().getName();
-
}
-
public static void main(String[] args) throws Exception{
-
HelloWorldCommand command = new HelloWorldCommand("test-Fallback");
-
String result = command.execute();
-
}
-
}
运行结果:
getFallback executed
除了HystrixBadRequestException异常之外,所有从run()方法抛出的异常都算作失败,并触发降级getFallback()和断路器逻辑。HystrixBadRequestException用在非法参数或非系统故障异常等不应触发回退逻辑的场景。
Hystrix的内部处理逻辑
下图为Hystrix服务调用的内部逻辑:
Hystrix Metrics的实现
Hystrix的Metrics中保存了当前服务的健康状况,包括服务调用总次数和服务调用失败次数等。根据Metrics的计数,熔断器从而能计算出当前服务的调用失败率,用来和设定的阈值比较从而决定熔断器的状态切换逻辑。因此Metrics的实现非常重要。
Hystrix使用RxJava的Observable.window()实现滑动窗口。RxJava的window使用后台线程创建新桶,避免了并发创建桶的问题。同时RxJava的单线程无锁特性也保证了计数变更时的线程安全。从而使代码更加简洁。以下为使用RxJava的window方法实现的一个简易滑动窗口Metrics,短短几行代码便能完成统计功能,足以证明RxJava的强大:
-
-
public void timeWindowTest() throws Exception{
-
Observable<Integer> source = Observable.interval(50, TimeUnit.MILLISECONDS).map(i -> RandomUtils.nextInt(2));
-
source.window(1, TimeUnit.SECONDS).subscribe(window -> {
-
int[] metrics = new int[2];
-
window.subscribe(i -> metrics[i]++,
-
InternalObservableUtils.ERROR_NOT_IMPLEMENTED,
-
() -> System.out.println("窗口Metrics:" + JSON.toJSONString(metrics)));
-
});
-
TimeUnit.SECONDS.sleep(3);
-
}
补充:
-
//构造setter
-
HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey(group);
-
HystrixThreadPoolKey threadPoolKey = HystrixThreadPoolKey.Factory.asKey(group);
-
HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey(service);
-
HytrixBaseCommand.Setter setter = HytrixBaseCommand.Setter.withGroupKey(groupKey) //分组名
-
.andCommandKey(commandKey) //依赖名
-
.andThreadPoolKey(threadPoolKey); //执行线程池名
-
//构造command
-
HystrixCommand<String> command = new HystrixCommand<String>(setter) {
-
protected String run() throws Exception {
-
if(time>0)//模拟长时间操作
-
Thread.sleep(time);
-
if(isException) //模拟异常情况
-
throw new RuntimeException("exception in run");
-
return service+ ":return";
-
}
-
-
protected String getFallback() {
-
return service+":fallback";
-
}
-
};
-
//阻塞执行,获取结果
-
String rel=command.execute();
-
return rel;
使用hystrix不仅仅有HystrixCommand,还有HystrixObservableCommand,两者的区别请参考:http://www.jianshu.com/p/b9af028efebb