11、Hystrix的基础使用

112 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情

11、Hystrix的基础使用

1、命令执行

在前面的例子中,使用了 execute 方法执行命令,一个命令对象可以使用以下方法来执行命令。

  • toObservable :返回一个最原始的可观察的实例 ( Observable ) , Observable是Rx.Java的类,使用该对象可以观察命令的执行过程,并且将执行信息传递给订阅者。
  • observe :调用 toObservable 方法,获得一个原始的 Observable 实例后, 使用ReplaySubject 作为原始 Observable 的订阅者。
  • queue :通过 toObservable 方法获取原始的 Observable 实例,再调用 Observable的toBlocking 方法得到 BlockingObservable 实例,最后调用 BlockingObservable 的toFuture 方法返回 Future 实例,调用 Future 的get 方法得到执行结果。
  • execute :调用 queue的 get 方法返回命令的执行结果,该方法同步执行。

以上4个方法,除 execute 方法外,其他方法均为异步执行 observe toObservable方法的区别在于, toObservable 被调用后,命令不会立即执行,只有当返回的 Observable实例被订阅后,才会真正执行命令。而在 observe 法的实现中,会调用 toObservable 得到Observable 实例,再对其进行订阅,因此调用 observe 方法后会立即执行命令(异步)。

public class RunTest {
    public static void main(String[] args) throws Exception {
        // 使用execute方法
        RunCommand c1 = new RunCommand("使用 execute 方法执行命令");
        c1.execute();
        // 使用queue 方法
        RunCommand c2 = new RunCommand("使用 queue 方法执行命令");
        c2.queue();
        //使用 observe 方法
        RunCommand c3 = new RunCommand("使用 observe 方法执行命令");
        c3.observe();
        // 使用 toObservable 方法
        RunCommand c4 = new RunCommand("使用 toObservable 方法执行命令");
        //调用 toObservable 方法后,命令不会马上执行
        // 只返回一个可观察的对象,并不会马上执行
        // 只有当Observable真正被订阅之后,才会执行
        Observable<String> ob = c4.toObservable();
        //进行订阅,此时会执行命令
        ob.subscribe(new Observer<String>() {
            // 命令执行完成之后
            @Override
            public void onCompleted() {
                System.out.println("       命令执行完成");
            }

            // 命令有错误
            @Override
            public void onError(Throwable e) {

            }

            // 命令执行返回
            @Override
            public void onNext(String s) {
                System.out.println("   命令执行结果:" + s);
            }
        });
        Thread.sleep(100);
    }


    // 测试命令
    static class RunCommand extends HystrixCommand<String> {

        String msg;

        public RunCommand(String msg) {
            super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
            this.msg = msg;
        }

        @Override
        protected String run() throws Exception {
            System.out.println(msg);
            return "success";
        }
    }
}

执行main方法,结果:

使用 execute 方法执行命令
使用 queue 方法执行命令
使用 observe 方法执行命令
使用 toObservable 方法执行命令
   命令执行结果:success
       命令执行完成 

2、属性配置

为一个命令设置执行的超时时间:默认超时时间 1 秒

public class MyTimeOutConfig extends HystrixCommand<String> {

    public MyTimeOutConfig() {
        // 超时时间设置成 500 毫秒
        super(Setter.withGroupKey(
                HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                .andCommandPropertiesDefaults(
                        HystrixCommandProperties.Setter()
                                .withExecutionTimeoutInMilliseconds(500)));
    }

    @Override
    protected String run() throws Exception {
        // 手动延迟 800 毫秒
        Thread.sleep(800);
        System.out.println("执行命令...");
        return "Success";
    }

    // 回退方法
    @Override
    protected String getFallback() {
        System.out.println("执行回退...");
        // return super.getFallback();
        return "Fallback...";
    }


    public static void main(String[] args) {
        MyTimeOutConfig c = new MyTimeOutConfig();
        String result = c.execute();
        System.out.println(result);

        /**
         * 执行回退...
         * Disconnected from the target VM, address: '127.0.0.1:57108', transport: 'socket'
         * Fallback...
         */
    }

}

设置全局超时时间:

/**
 * 设置全局超时时间
 */
public class PropertyMain {
    public static void main(String[] args) {
        ConfigurationManager
                .getConfigInstance()
                .setProperty(
                        "hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds",
                        2000);
        PropertyCommand c = new PropertyCommand();
        String result = c.execute();
        System.out.println(result);     // fallback 
    }

    static class PropertyCommand extends HystrixCommand<String> {
        public PropertyCommand() {
            super(Setter.withGroupKey(HystrixCommandGroupKey.Factory
                    .asKey("ExampleGroup")));
        }

        @Override
        protected String run() throws Exception {
            Thread.sleep(3500);
            return "success";
        }

        @Override
        protected String getFallback() {
            return "fallback";
        }

    }
}

除了超时的配置外,还需要了解一下命令的相关名称,可以为命令设置以下名称。

  • 命令组名称( GroupKey ):必须提供命令组名称,默认情况下,全局维护的线程池Map 以该值作为 key ,该 Map的value 为执行命令的线程池。
  • 命令名称( CommandKey ):可选参数。
  • 线程池名称( ThreadPoolKey ):指定了线程的 key 后,全局维护的线程池 Map 将以该值作为 key 以下的代码片断分别设置以上的 3个Key:

Hystrix配置众多,更多请参考下面链接: github.com/Netflix/Hys…

/**
 * 配置命令组名称,命令名称,线程池名称
 */
public class KeyCommand extends HystrixCommand<String> {

    public KeyCommand() {
        // 设置组名,每一个命令,Hystrix底层都会分配一个线程池去执行命令,它会使用一个map来维护这些线程池
        // 如果不提供线程池名称,则默认是使用组名作为线程池的key:Map<String,Pool>
        // group标识,一个group使用一个线程池
        super(Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey("GroupKey"))
                .andCommandKey(HystrixCommandKey.Factory.asKey("CommandKey"))
                .andThreadPoolKey(
                        HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey")));
    }

    @Override
    protected String run() throws Exception {
        return null;
    }

}

3、回退

由上面流程图可知,至少有3种情况触发回退(fallback)

  • 断路器被打开
  • 线程池、队列、信号量满载
  • 实际执行命令失败(超时也属于失败)
/**
 * 回退测试
 */
public class FallbackTest {

    public static void main(String[] args) {
        //断路器被强制打开    -- 全局配置
        ConfigurationManager.getConfigInstance().setProperty("hystrix.command.default.circuitBreaker.forceOpen", "true");

        FallbackCommand c = new FallbackCommand();
        String result = c.execute();
        System.out.println(result);

        FallbackCommand2 c2 = new FallbackCommand2();
        String result2 = c2.execute();
        System.out.println(result2);
    }

    static class FallbackCommand extends HystrixCommand<String> {

        public FallbackCommand() {
            // 设置组名、打开断路器(不推荐代码打开断路器,实际开发中是实现特定逻辑才打开断路器,如频繁请求同一个服务均失败才打开)
            super(Setter.withGroupKey(
                    HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                    .andThreadPoolKey(
                            HystrixThreadPoolKey.Factory.asKey("ThreadPoolKey"))
                    .andCommandPropertiesDefaults(
                            HystrixCommandProperties.Setter()
                                    .withCircuitBreakerForceOpen(false)));// 强制关闭断路器   -- 只对当前命令起作用
        }
        @Override
        protected String run() throws Exception {
            return "命令1:success";
        }
        @Override
        protected String getFallback() {
            return "命令1:fallback";
        }
    }

    static class FallbackCommand2 extends HystrixCommand<String> {
        public FallbackCommand2() {
            super(Setter.withGroupKey(
                    HystrixCommandGroupKey.Factory.asKey("ExampleGroup")));
        }
        @Override
        protected String run() throws Exception {
            return "命令2: success";
        }
        @Override
        protected String getFallback() {
            return "命令2:fallback";
        }
    }
}

执行结果: 命令1:success 命令2:fallback 如果让断路器打开,需要符合一定的条件。本例为了简单起见,在代码清单中使用了 配置管理类( ConfigurationManager )将断路器强制打开与关闭。 在打开断路器后, FallbackCommand 总会执行回退(getFallback )方法将断路器关闭,命令执行正常。 如果断路器被打开,而命令中没有提供回退方法,将抛出以下异常: com.netflix.hystrix.exception.HystrixRuntimeException : FallbackCommand short - circuited and no fallback available. 另外,需要注意的是,命令执行后,不管是否会触发回退,都会去计算整个链路的 健康状况,根据健康状况来判断是否要打开断路器。 如果命令仅仅失败了一次,是不足以打开断路器的,关于断路器的逻辑将在后面章节讲述。

4、回退的模式

Hystrix的回退机制比较灵活,可以在A命令中执行B中回退,如果B也执行失败,同样会触发B命令的回退,这样就形成一种链式的命令执行 如下面代码所示:

/**
     * 一种回退模式  -- 链式
     */
    static class CommandA extends HystrixCommand<String> {
        //省略其他代码
        protected String run() throws Exception {
            throw new RuntimeException();
        }

        protected String getFallback() {
            return new CommandB().execute();
        }

    }

还有更复杂的例子,例如银行转账。 假设一个转账命令包含调用A银行扣款、B银行加款两个命令,其中一个命令失败后,再执行转账命令回退 要做到如下图所示的多命令只执行一次回退的效果,CommandA和CommandB,不能有回退方法,如果CommandA命令执行失败,并且该命令有回退方法,此时将不会执行MainCommand的回退方法

image.png

5、断路器开启

断路器一旦开启,就会直接调用回退方法,不再执行命令,而且也不会更新链路的健 康状况。断路器的开启要满足两个条件:

  • 整个链路达到一定的阀值,默认情况下,10秒内产生超过20次请求,则符合第一个条件
  • 满足第一个条件的情况下,如果请求的错误百分比大于阀值,则会开启断路器,默认为50%(如:10秒内发送30次请求,其中15次是失败的,则会开启断路器)

Hystrix 的逻辑是先判断是否满足第一个条件,再判断是否满足第二个条件。如果两个 条件都满足,则开启断路器。断路器开启的测试代码请见代码清单 6-8。

/**
 * 测试到达断路器阈值,打开断路器
 */
public class OpenTest {

    public static void main(String[] args) {
        // 断路器配置(此处配置成:10s内大于10次请求,错误百分比为 50%)
        ConfigurationManager.getConfigInstance().setProperty(
                "hystrix.command.default.metrics.rollingStats.timeInMilliseconds", 10000);
        ConfigurationManager.getConfigInstance().setProperty(
                "hystrix.command.default.circuitBreaker.requestVolumeThreshold", 10);
        ConfigurationManager.getConfigInstance().setProperty(
                "hystrix.command.default.circuitBreaker.errorThresholdPercentage", 50);

        // 循环遍历发送请求,发送15次请求(满足第一个要求)
        for (int i = 0; i < 15; i++) {
            ErrorCommand c = new ErrorCommand();
            String execute = c.execute();
            // 输出断路器状态
            System.out.println("执行第" + i + "次请求,断路器的状态:" + c.isCircuitBreakerOpen());
            System.out.println(execute);
        }
    }

    /**
     * 模拟超时的命令
     */
    static class ErrorCommand extends HystrixCommand<String> {
        public ErrorCommand() {
            // 如果超过500毫秒无响应就执行回退
            super(Setter.withGroupKey(
                    HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                    .andCommandPropertiesDefaults(
                            HystrixCommandProperties.Setter()
                                    .withExecutionTimeoutInMilliseconds(500)));
        }

        @Override
        protected String run() throws Exception {
            // 方法执行需要800毫秒,也就是说每次方法执行均是无响应的
            Thread.sleep(800);
            return "";
        }

        @Override
        protected String getFallback() {
            return "fallback";
        }
    }


}

注意代码清单 6-8 中的三个配置,第一个配置了数据统计的时间,第二个配置了请求 的阙值,第三个配置了错误百分比。如果在 10 秒内,有大于 10 个请求发生,并且请求的 错误率超过 50% ,则开启断路器。 在命令类 MyCommand 中,设置了命令执行的超时时间为 500 毫秒,命令执行需要 800豪 秒,换言之,该命令总会超时,命令模拟了现实环境中所依赖的服务瘫痪 (超时响应) 的情况。 由结果可知,前面10个命令没有开启断路器,而到了第11命令,断路器被打开,命令直接执行fallback方法, 所有的命令都是同步执行,不是异步 执行结果:

执行第1次请求,断路器的状态:false
fallback
执行第2次请求,断路器的状态:false
fallback
执行第3次请求,断路器的状态:false
fallback
执行第4次请求,断路器的状态:false
fallback
执行第5次请求,断路器的状态:false
fallback
执行第6次请求,断路器的状态:false
fallback
执行第7次请求,断路器的状态:false
fallback
执行第8次请求,断路器的状态:false
fallback
执行第9次请求,断路器的状态:false
fallback
执行第10次请求,断路器的状态:false
fallback
执行第11次请求,断路器的状态:true
fallback
执行第12次请求,断路器的状态:true
fallback
执行第13次请求,断路器的状态:true
Disconnected from the target VM, address: '127.0.0.1:61209', transport: 'socket'
fallback
执行第14次请求,断路器的状态:true
fallback
执行第15次请求,断路器的状态:true
fallback