Hystrix-介绍与使用(上)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiaoyi52/article/details/82844370

在博主另一篇文章-“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

猜你喜欢

转载自blog.csdn.net/xiaoyi52/article/details/82844370
今日推荐