在博主另一篇文章-“SpringCloud基础(2)”中简单介绍过Hystrix在SpringCloud中的作用。实际上Hystrix是Netflix的分布式套件之一,该框架目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix具备拥有回退机制和断路器功能的线程和信号隔离,请求缓存和请求打包,以及监控和配置等功能,即使不使用SpringCloud,在分布式系统中,Hystrix也是大有作为的。
依赖包如下:
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>${hystrix.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-metrics-event-stream</artifactId>
<version>${hystrix.version}</version>
</dependency>
Hystrix的版本是1.4.22
1 HelloWorld
直接看HelloWorld。
@RunWith(SpringJUnit4ClassRunner.class)
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() throws Exception {
return "Hello " + name + "!";
}
public static class UnitTest {
@Test
// 同步调用
public void testSynchronous() throws Exception {
HystrixCommand<String> hystrixCommand = new CommandHelloWorld("world");
String result = hystrixCommand.execute();
System.out.println(result);
}
}
}
构造一个HystrixCommand实例,调用execute方法同步执行获得结果。
事实上HystrixCommand的执行模式有三种,分别是同步执行,异步执行和反应执行。上面的例子是同步执行,下面是异步执行模式:
@Test
// 异步调用
public void testAsynchronous() throws Exception {
HystrixCommand<String> hystrixCommand = new CommandHelloWorld("world");
Future<String> future = hystrixCommand.queue();
String result = future.get();
System.out.println(result);
}
通过调用queue方法获得Future对象,然后通过get方法获取到异步返回的结果。
反应执行模式demo如下:
@Test
// 反应执行
public void testObservable() throws Exception {
Observable<String> observable = new CommandHelloWorld("world").observe();
// 同步阻塞
String blockingResult = observable.toBlocking().single();
System.out.println(blockingResult);
// 非阻塞
observable.subscribe(new Observer<String>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onNext(String s) {
System.out.println("onNext: " + s);
}
});
}
事实上反应执行模式一般不用上面这种方式,而是通过继承HystrixObservableCommand接口来实现。如下所示:
@RunWith(SpringJUnit4ClassRunner.class)
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() {
System.out.println("[construct] thread: " + Thread.currentThread().getName());
return Observable.create(new Observable.OnSubscribe<String>() {
@Override
public void call(Subscriber<? super String> observer) {
System.out.println("[construct-call] thread: " + Thread.currentThread().getName());
if (!observer.isUnsubscribed()) {
observer.onNext("Hello1" + " thread: " +Thread.currentThread().getName());
observer.onNext("Hello2" + " thread: " +Thread.currentThread().getName());
observer.onNext(name + " thread:" +Thread.currentThread().getName());
System.out.println("complete before-----" + "thread: " + Thread.currentThread().getName());
observer.onCompleted();
System.out.println("complete after------" + "thread: " + Thread.currentThread().getName() + "\r\n"
);
observer.onCompleted(); // 不会执行到
observer.onNext("abc"); // 不会执行到
}
}
});
}
public static class UnitTest {
@Test
// 反应执行
public void testObservableCommand() throws Exception {
HystrixObservableCommand<String> observableCommand = new ObservableCommandHelloWorld("observable world");
observableCommand.toObservable().subscribe(new Observer<String>() {
@Override
public void onCompleted() {
System.out.println("complate");
}
@Override
public void onError(Throwable throwable) {
System.out.println("error");
}
@Override
public void onNext(String s) {
System.out.println("next:" + s);
}
});
HystrixObservableCommand<String> observableCommand1 = new ObservableCommandHelloWorld("observable world1");
String result = observableCommand1.observe().toBlocking().first();
System.out.println(result);
}
}
}
输出结果如下:
[construct] thread: main
[construct-call] thread: main
next:Hello1 thread: main
next:Hello2 thread: main
next:observable world thread:main
complete before-----thread: main
complate
complete after------thread: main
[construct] thread: main
[construct-call] thread: main
complete before-----thread: main
complete after------thread: main
Hello1 thread: main
2 Command执行模式
从上面的例子可以看出,Hystrix一共有三种执行模式:同步执行、异步执行和反应模式执行。
其中同步执行和异步执行模式只需要继承HystrixCommand接口,而反应执行模式需要继承HystrixObservableCommand接口。
继承HystrixCommand需要覆写run方法,在该方法中完成命令逻辑。而继承HystrixObservableCommand接口需要通过覆写construct方法来实现命令逻辑。
前者的run()是由新创建的线程执行的,而construct()是由调用程序线程执行的。
前者一个实例只能向调用程序发送单条数据,比如上文例子中run()只能返回一个结果;后者一个实例可以顺序发送多条数据,如例子中顺序调用多个onNext(),实现了向调用程序发送多条数据。
HystrixCommand接口拥有全部三种执行模式,同步执行模式通过execute()实现,异步执行模式通过queue()实现,反应执行模式一般不会直接通过HystrixCommand调用。而是由HystrixObservableCommand执行,通过observe()和toObservable()实现。下面简单介绍一下四个方法。
execute():以同步阻塞方式执行run()。调用execute()后,hystrix先创建一个新线程运行run(),接着调用程序即主线程要在execute方法的调用处一直堵塞者,直到run()运行完成。
queue():以异步非堵塞方式执行run()。调用queue()后,先同步返回一个Future对象。同时hystrix创建一个新线程运行run(),主线程通过Future.get()拿到run方法的返回结果。当然,Future.get()也是阻塞执行的。实际上execute()内部就是调用的queue().get()。调用queue()之后立即调用get(),也即所谓的同步阻塞是阻塞在get()方法上的。而异步执行是将queue()和get()分开,调用完queue()之后可以不马上调用get(),而是可以做其他的事情,所以是异步的。
observe():事件注册前执行run()/construct()。只看HystrixObservableCommand的话,那就是construct方法了。调用observe()后,主线程执行construct();接着从observe()返回之后主线程调用subscribe方法完成事件注册,如果construct方法执行成功则触发onNext()和onCompleted(),执行异常则触发onError()。observe():事件注册前执行run()/construct()。只看HystrixObservableCommand的话,那就是construct方法了。调用observe()后,主线程执行construct();接着从observe()返回之后主线程调用subscribe方法完成事件注册,如果construct方法执行成功则触发onNext()和onCompleted(),执行异常则触发onError()。
toObservable():事件注册后执行run()/construct()。只看HystrixObservableCommand的话,那就是construct方法了。先调用toObservable()获得一个Observable对象,然后通过subscribe()完成事件注册,注册完成之后自动触发construct方法。construct()执行成功则触发onNext()和onCompleted(),执行异常则触发onError()。toObservable(): 事件注册后执行run()/construct()。只看HystrixObservableCommand的话,那就是construct方法了。先调用toObservable()获得一个Observable对象,然后通过subscribe()完成事件注册,注册完成之后自动触发construct方法。construct()执行成功则触发onNext()和onCompleted(),执行异常则触发onError()。
3 fallback降级
HystrixCommand的降级方法是getFallback,HystrixObservableCommand接口的降级方法是resumeWithFallback()。
先看一个demo:
public class CommandHelloFailure extends HystrixCommand<String>{
private static final Logger logger = LoggerFactory.getLogger(CommandHelloFailure.class);
private final String name;
public CommandHelloFailure(String name) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
@Override
protected String run() throws Exception {
logger.error("run thread#################" + Thread.currentThread().getName());
throw new RuntimeException("this command always fails");
}
@Override
protected String getFallback() {
logger.error("getFallback thread#################" + Thread.currentThread().getName());
return "Hello Failure " + name + "!";
}
public static class UnitTest {
@Test
public void testSynchronous() throws Exception {
HystrixCommand<String> hystrixCommand = new CommandHelloFailure("world");
String result = hystrixCommand.execute();
System.out.println(result);
}
}
}
当调用execute同步执行时,Hystrix会启动一个线程,来执行run(),在执行run()的过程中抛出了异常,于是自动触发getFallback(),进入降级逻辑。从run()转到getFallback()时,是同一个线程处理的。
当run()/construct()被触发执行过程中发生错误时,将转向执行降级函数。实际上有四种错误的情况将会触发服务降级。
非HystrixBadRequestException异常:除HystrixBadRequest之外的其他异常,都会导致服务降级。而HystrixBadRequestException异常由非法参数或非系统错误引起,不会触发fallback,也不会被计入熔断器统计指标。
run()/construct()运行超时:Hystrix命令有执行超时时间,当到达超时时间时就触发降级逻辑。
熔断器启动:当符合熔断条件(在指定时间窗口内满足指定数量的请求,且达到指定的失败率)时,将触发服务熔断,后续的请求将直接进行fallback。
线程池/信号量已满:当一个command的所有可用线程或信号量被占用完之后,之后接受到的请求将触发降级逻辑。
如下图表格所示:
失败类型 | 异常类别 | 异常原因 | 是否触发fallback |
---|---|---|---|
FAILURE(失败) | HystrixRuntimeException | 基础异常(用户控制的) | 是 |
TIMEOUT(超时) | HystrixRuntimeException | j.u.c.TimeoutException | 是 |
SHORT_CIRCUITED(熔断) | HystrixRuntimeException | j.l.RuntimeException | 是 |
THREAD_POOL_REJECTED(线程池已满) | HystrixRuntimeException | j.u.c.RejectedExecutionException | 是 |
SEMAPHORE_REJECTED(信号量已满) | HystrixRuntimeException | j.l.RuntimeException | 是 |
BAD_REQUEST | HystrixBadRequestException | 基础异常(用户控制的) | 否 |
4 隔离策略
上面提到导致降级逻辑执行的场景中,有一种情况是线程池满或者信号量已满,即当一个command的所有可用线程或信号量被占用完之后,之后接受到的请求将触发降级逻辑。
线程池和信号量正是hystrix提供的两种隔离策略。
4.1 线程池隔离
不同的服务使用不同的线程池,彼此间不受影响,达到隔离效果。线程池隔离把执行依赖代码的线程与请求线程(如tomcat线程)分离。以上文中HelloWorld为例,调用execute方法执行hystrix命令后,请求线程在此阻塞等待,然后由hystrix从线程池中取出一个线程来执行run(),直到返回结果。
4.2 信号量隔离
使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,当请求进来时先判断计数器的数值,若超过设置的最大线程个数则拒绝该请求,若不超过则通行,这时候计数器+1,请求返回成功后计数器-1。
线程池隔离与信号量隔离的最大区别在于:线程池隔离时执行依赖代码的线程是hystrix线程池中的新线程,而信号量隔离时仍然是请求线程。
Hystrix默认的隔离策略是线程池隔离。
5 基本概念
5.1 commandKey
不指定的话,默认取类名:getClass.getSimpleName()。也可以用过构造函数定义:
public CommandHelloWorld(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld")));
this.name = name;
}
5.2 commandGroupKey
一些指标如报表、警告、仪表板或团队/库的拥有者等是以groupKey为粒度进行统计的。它是每个命令最少配置的必选参数,因为下一个参数HystrixThreadPoolKey不指定的话,会默认使用groupKey作为HystrixThreadPoolKey的值。也就是说,如果不单独指定线程池key的话,会以groupKey作为线程池分配的粒度。
5.3 threadPoolKey
threadPoolKey是线程池隔离时线程池分配的粒度,如果不指定,则使用groupKey的值。如果要隔离不同的服务,则需要隔离的服务每个都需要指定单独的threadPoolKey。
5.4 threadPoolProperties
可以通过threadPoolProperties设置线程的属性,如核心线程数、最大线程数、任务队列大小等。
6 熔断机制
在指定时间内,满足指定数量的请求,且失败的比率达到指定值,则会触发熔断,熔断之后所有请求不再进入主逻辑,而是直接进入fallback。
下面看一个熔断demo。
public class CircuitBreakerTest {
public static int num = 0;
static HystrixCommand.Setter setter = HystrixCommand.Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("circuitBreakerTestGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("circuitBreakerTestCommand"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("circuitBreakerTestPool"))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(10)) // 配置线程池
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withCircuitBreakerEnabled(true)
.withCircuitBreakerRequestVolumeThreshold(10)
.withCircuitBreakerErrorThresholdPercentage(80));
// 未配置的值均取系统默认值
HystrixCommand<Object> hystrixCommand = new HystrixCommand<Object>(setter) {
@Override
protected Object run() throws Exception {
if (num % 2 == 0) {
return num + "";
} else {
int j = 0;
while (true) {
j++; // 死循环模拟超时
}
}
}
@Override
protected Object getFallback() {
return "CircuitBreaker fallback:" + num;
}
};
}
测试函数:
@Test
public void testCircuitBreaker() throws Exception {
for (int i = 0; i < 30; i++) {
CircuitBreakerTest.num = i;
CircuitBreakerTest circuitBreakerTest = new CircuitBreakerTest();
String result = (String) circuitBreakerTest.hystrixCommand.execute();
System.out.println(result);
}
}
输出如下:
0
CircuitBreaker fallback:1
2
CircuitBreaker fallback:3
4
CircuitBreaker fallback:5
6
CircuitBreaker fallback:7
8
CircuitBreaker fallback:9
10
CircuitBreaker fallback:11
12
CircuitBreaker fallback:13
14
CircuitBreaker fallback:15
16
CircuitBreaker fallback:17
18
CircuitBreaker fallback:19
CircuitBreaker fallback:20
CircuitBreaker fallback:21
CircuitBreaker fallback:22
CircuitBreaker fallback:23
CircuitBreaker fallback:24
CircuitBreaker fallback:25
CircuitBreaker fallback:26
CircuitBreaker fallback:27
CircuitBreaker fallback:28
CircuitBreaker fallback:29
这个例子中,熔断指标中时间窗口没有设置,取系统默认值10s,withCircuitBreakerRequestVolumeThreshold(10)指定时间窗口内统计的请求数量为10,withCircuitBreakerErrorThresholdPercentage(80)指定统计失败率为80%。
熔断器开启条件是10s内,有10个以上请求,而且失败率达到80%。
从理论上来讲,这个例子中只有当num为偶数,run()处理超时才会fallback,那么失败率应该是50%,达不到80%的失败率,熔断器不会开启。而实际输出结果却表明第20个以后的请求全部fallback,看起来似乎是熔断器已经开启了。
但实际并不是熔断器开启,而是因为核心线程被用完了。上面提到过,当run()执行过程中发生错误转而执行getFallback()时,处理线程是同一个线程,但是当run()执行超时(默认值1s)转而执行getFallback()时,此时是另外启动一个线程来处理getFallback()的,在本例中,run函数线程一直在死循环逻辑中没有释放,导致10个核心线程被占满,后面再进来的请求只能fallback了。可以在getFallback函数里面加上System.out.println(getExecutionException())来看每次进入fallback的原因。
本例中核心线程使用完之前fallback的原因是com.netflix.hystrix.exception.HystrixTimeoutException,从第20个请求开始fallback的原因是
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@453da22c rejected from java.util.concurrent.ThreadPoolExecutor@10b48321[Running, pool size = 10, active threads = 10, queued tasks = 0, completed tasks = 9]
如果将失败率调整到50%,那么前5个进入fallback的请求是因为超时,后面进入fallback的请求是因为熔断器开启。输出结果如下:
0
com.netflix.hystrix.exception.HystrixTimeoutException
CircuitBreaker fallback:1
2
com.netflix.hystrix.exception.HystrixTimeoutException
CircuitBreaker fallback:3
4
com.netflix.hystrix.exception.HystrixTimeoutException
CircuitBreaker fallback:5
6
com.netflix.hystrix.exception.HystrixTimeoutException
CircuitBreaker fallback:7
8
com.netflix.hystrix.exception.HystrixTimeoutException
CircuitBreaker fallback:9
java.lang.RuntimeException: Hystrix circuit short-circuited and is OPEN
CircuitBreaker fallback:10
java.lang.RuntimeException: Hystrix circuit short-circuited and is OPEN
CircuitBreaker fallback:11
java.lang.RuntimeException: Hystrix circuit short-circuited and is OPEN
CircuitBreaker fallback:12
java.lang.RuntimeException: Hystrix circuit short-circuited and is OPEN
CircuitBreaker fallback:13
java.lang.RuntimeException: Hystrix circuit short-circuited and is OPEN
...
Hystrix的结果缓存和请求合并的功能由于时间关系没有详细研究,在此略过。下篇中将介绍隔离与熔断在项目中的实际使用。
7 参考资料
1.https://www.jianshu.com/p/b9af028efebb
2.https://github.com/Netflix/Hystrix/wiki/How-To-Use