12、Hystrix的高级用法

122 阅读9分钟

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

12、Hystrix的高级用法

6、断路器关闭

断路器打开后,在一段时间内,命令不会再执行(一直触发回退),这段时间称为休眠期,休眠期的默认值为5秒, 休眠期结束之后,Hystrix会尝试性执行一次命令,此时断路器状态不是开启,也不是关闭,而是一个半开的状态,**如果这一次命令执行成功,则会关闭断路器并且清空链路的健康信息,**如果执行失败,断路器会继续保持打开的状态。 断路器的打开与关闭测试,如下代码:

public class CloseTest {

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

        // 设置休眠期,断路器打开后,这段时间不会再执行命令,默认值为 5 秒,此处设置为 3 秒
        ConfigurationManager.getConfigInstance().setProperty(
                "hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds", 3000);
        // 该值决定是否执行超时
        boolean isTimeout = true;

        for (int i = 0; i < 10; i++) {// 发送10次请求,一开始均为失败,则会打开断路器
            TestCommand c = new TestCommand(isTimeout);// 总会超时
            c.execute();
            // 输出健康状态等信息
            HealthCounts hc = c.getMetrics().getHealthCounts();
            System.out.println("断路器状态:" + c.isCircuitBreakerOpen() + ", 请求数量:"
                    + hc.getTotalRequests());
            if (c.isCircuitBreakerOpen()) {
                // 如果断路器打开, 让下一次循环成功执行命令
                isTimeout = false;//
                System.out.println("============  断路器打开了,等待休眠期结束 ===========");
                Thread.sleep(6000);// 等待休眠期结束,6s之后尝试性发一次请求
            }
        }
    }

    /**
     * 模拟超时的命令
     */
    static class TestCommand extends HystrixCommand<String> {

        private boolean isTimeout;

        public TestCommand(boolean isTimeout) {
            super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                    .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                                    .withExecutionTimeoutInMilliseconds(500)));// 500毫秒没有响应,则打开断路器
            this.isTimeout = isTimeout;
        }

        @Override
        protected String run() throws Exception {
            // 让外部决定是否超时
            if (isTimeout) {
                // 模拟处理超时
                Thread.sleep(800);// 方法执行时间为800毫秒
            } else {
                Thread.sleep(200);
            }
            return "";
        }

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

}

代码清单中配置了休眠期为 3秒,循环 10 次,创建 10 个命令并执行 在执行完第4 个命令后,断路器会被打开,此时我们等待休眠期结束,让下 次循环的命令执行成功。 代码清单中使用了 一个布尔值来决定是否执行成功,第5 次命令会执行成功,此时 路器将会被关闭,剩下的命令全部都可以正常执行。在循环体中,使用了 HealthCounts 象,该对象用于记录链路的健康信息。如果断路器关闭(链路恢复健康), Health Counts 面的健康信息将会被重置。

执行结果:

断路器状态:false, 请求数量:0
断路器状态:false, 请求数量:1
断路器状态:false, 请求数量:2
断路器状态:true, 请求数量:3
============  断路器打开了,等待休眠期结束 ===========
断路器状态:false, 请求数量:0
断路器状态:false, 请求数量:0
断路器状态:false, 请求数量:0
断路器状态:false, 请求数量:3
断路器状态:false, 请求数量:3
断路器状态:false, 请求数量:5

7、隔离机制

为什么要做线程隔离 比如我们现在有3个业务调用分别是查询订单、查询商品、查询用户,且这三个业务请求都是依赖第三方服务-订单服务、商品服务、用户服务。三个服务均是通过RPC调用。当查询订单服务,假如线程阻塞了,这个时候后续有大量的查询订单请求过来,那么容器中的线程数量则会持续增加直致CPU资源耗尽到100%,整个服务对外不可用,集群环境下就是雪崩。如下图

参考链接:www.jianshu.com/p/df1525d58…

image.png

命令的真正执行,除了断路器要关闭外,还需要再过一关:执行命令的线程池或者信号量是否满载。如果满载,命令就不会执行,而是直接触发回退,这样的机制,在控制命令的执行上,实现了错误的隔离。 Hystrix 提供了两种隔离策略。

  • THREAD :默认值,由线程池来决定命令的执行,如线程池满载,则不会执行命令。 Hystrix 使用了 ThreadPoolExecutor 来控制线程池行为,线程池的默认大小为10。
  • SEMAPHORE :由信号量来决定命令的执行,当请求的并发数高于阔值时,就不再执行命令。相对于线程策略,信号量策略开销更小,但是该策略不支持超时以及异步,除非对调用的服务有足够的信任,否则不建议使用该策略进行隔离。

使用代码测试线程隔离与信号隔离两个策略,编写共用的命令类:

/**
 * 测试隔离策略的命令类
 */
public class MyCommand extends HystrixCommand<String> {

    public int index;

    public MyCommand(int index) {
        super(Setter.withGroupKey(HystrixCommandGroupKey.Factory
                .asKey("ExampleGroup")));
        this.index = index;
    }

    @Override
    protected String run() throws Exception {
        Thread.sleep(500);
        System.out.println("run(),当前索引:" + index);
        return "success";
    }

    @Override
    protected String getFallback() {
        System.out.println("getFallback(),当前索引:" + index);
        return "fallback";
    }

}

测试线程隔离: 在使用线程隔离策略的运行类中 配置了线程池大小为 3,进行6次循环,意味着有3次命令将会触发回退,运行后可看到效果。

当用户请求服务A和服务I的时候,tomcat的线程(图中蓝色箭头标注)会将请求的任务交给服务A和服务I的内部线程池里面的线程(图中橘色箭头标注)来执行,tomcat的线程就可以去干别的事情去了,当服务A和服务I自己线程池里面的线程执行完任务之后,就会将调用的结果返回给tomcat的线程,从而实现资源的隔离,当有大量并发的时候,服务内部的线程池的数量就决定了整个服务的并发度,例如服务A的线程池大小为10个,当同时有12请求时,只会允许10个任务在执行,其他的任务被放在线程池队列中,或者是直接走降级服务,此时,如果服务A挂了,就不会造成大量的tomcat线程被服务A拖死,服务I依然能够提供服务。整个系统不会受太大的影响。

原文:blog.csdn.net/liuchuanhon…

public class ThreadIso {
    /**
     * 测试线程隔离
     */
    public static void main(String[] args) throws Exception {
        // 设置线程池(超过3,认为是线程池满载)
        ConfigurationManager.getConfigInstance().setProperty(
                "hystrix.threadpool.default.coreSize", 3);

        for (int i = 0; i < 6; i++) {
            MyCommand c = new MyCommand(i);
            c.queue();//异步执行
        }

        Thread.sleep(5000);
    }

}

运行结果: getFallback(),当前索引:3 getFallback(),当前索引:4 getFallback(),当前索引:5 run(),当前索引:0 run(),当前索引:2 run(),当前索引:1

测试信号量隔离: 信号量的资源隔离只是起到一个开关的作用,例如,服务X的信号量大小为10,那么同时只允许10个tomcat的线程(此处是tomcat的线程,而不是服务X的独立线程池里面的线程)来访问服务X,其他的请求就会被拒绝,从而达到限流保护的作用。

注意代码中的 3个配置项,指定使用信号量作为隔离策略,分别设置了命令执行的最大并发数,以及执行回退的最大并发数。运行代码如下,最终只有两个命令正常执行,其余命令都会触发回退,可见信号量隔离的相关配置生效。

    /**
     * 测试信号量隔离
     */
    public static void main1(String[] args) throws Exception {
        // 配置使用信号量的策略进行隔离
        ConfigurationManager.getConfigInstance().setProperty(
                "hystrix.command.default.execution.isolation.strategy",
                HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE);

        // 设置最大并发数,默认为 10 本例配置为 2
        ConfigurationManager.getConfigInstance().setProperty(
                "hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests", 2);

        // 设置执行回退方法的最大并发数,默认为 10 本例配置为 20
        ConfigurationManager.getConfigInstance().setProperty(
                "hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests", 20);

        //此处是tomcat的线程,而不是服务X的独立线程池里面的线程
        for (int i = 0; i < 6; i++) {
            final int index = i;
            Thread t = new Thread() {
                public void run() {
                    MyCommand c = new MyCommand(index);
                    c.execute();
                }
            };
            t.start();
        }
        // 如果使用独立Hystrix的线程池里的线程,信号量控制不起作用都会 执行成功
//        for (int i = 0; i < 6; i++) {
//            MyCommand c = new MyCommand(i);
//            c.queue();//异步执行
//        }
        Thread.sleep(5000);
    }

执行结果: getFallback(),当前索引:2 getFallback(),当前索引:0 getFallback(),当前索引:3 getFallback(),当前索引:5 run(),当前索引:4 run(),当前索引:1

test线程池隔信号量隔离
线程与调用线程非相同线程与调用线程相同(jetty线程)
开销排队、调度、上下文开销等无线程切换,开销低
异步支持不支持
并发支持支持(最大线程池大小)支持(最大信号量上限)

总结 当请求的服务网络开销比较大的时候,或者是请求比较耗时的时候,我们最好是使用线程隔离策略,这样的话,可以保证大量的容器(tomcat)线程可用,不会由于服务原因,一直处于阻塞或等待状态,快速失败返回。而当我们请求缓存这些服务的时候,我们可以使用信号量隔离策略,因为这类服务的返回通常会非常的快,不会占用容器线程太长时间,而且也减少了线程切换的一些开销,提高了缓存服务的效率。

8、合并请求

合并请求、请求缓存,在一次请求的过程中才能实现,因此需要先初始化请求上下文 根据前面小节的介绍可知,默认情况下,会为命令分配线程池来执行命令实例,线程 池会消耗一定的性能。对于一些同类型的请求 (URL 相同,参数不同) , Hystrix 提供了合 并请求的功能,在一次请求的过程中,可以将一个时间段内的相同请求(参数不同),收集 到同一个命令中执行,这样就节省了线程的开销,减少了网络连接,从而提升了执行的性能。 这个功能有点像数据库的批处理功能。 实现合并请求的功能,至少包含以下3个条件

  • 需要有一个执行请求的命令,将全部参数进行整理,然后调用外部服务。
  • 需要有一个合并处理器,用于收集请求,以及处理结果。
  • 外部接口提供支持,例如外部的服务提供了/person/ {personName}的服务用于查找一 个Person ,如果合并请求,外部还需要提供一个/persons 的服务,用于查找多个Person。

接下来 实现一个简单的查找逻辑。假设有 以下场景: 客户端多次调用查找单个 Person的 Web 服务,而服务端提供了一个新的服务,可以传入多个名字,查找井返回多个 Person 实例 ,此时,可以考虑使用合并请求。编写一个命令类,用于收集请求参数以及调用服务, 请见代码清单如下:

/**
 * 合并请求测试类
 */
public class CollapseTest {

    /**
     * 编写命令类,用于收集请求参数以及调用服务
     */
    static class CollapserCommand extends HystrixCommand<Map<String, Person>> {

        // 请求集合,第一个类型是单个请求返回的数据类型,第二是请求参数的类型
        Collection<CollapsedRequest<Person, String>> requests;

        private CollapserCommand(Collection<CollapsedRequest<Person, String>> requests) {
            super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")));
            this.requests = requests;
        }

        @Override
        protected Map<String, Person> run() throws Exception {
            System.out.println("收集参数后执行命令,参数数量:" + requests.size());

            // 处理参数
            List<String> personNames = new ArrayList<>();

            for (CollapsedRequest<Person, String> request : requests) {
                personNames.add(request.getArgument());
            }
            // 调用服务(此处模拟调用),根据名称获取Person的Map
            Map<String, Person> result = callService(personNames);
            return result;
        }

        // 模拟服务返回
        private Map<String, Person> callService(List<String> personNames) {

            Map<String, Person> result = new HashMap<>();
            for (String personName : personNames) {
                Person p = new Person();
                p.id = UUID.randomUUID().toString();
                p.name = personName;
                p.age = new Random().nextInt(30);

                result.put(personName, p);
            }
            return result;
        }
    }

    static class Person {
        String id;
        String name;
        Integer age;

        public String toString() {
            return "id: " + id + ", name: " + name + ", age: " + age;
        }
    }

    /**
     * 合并处理器  -- 将请求进行合并
     * 第一个类型为批处理返回的结果类型
     * 第二个为单个请求返回的结果类型
     * 第三个是请求参数类型
     */
    static class MyHystrixCollapser extends HystrixCollapser<Map<String, Person>, Person, String> {

        String personName;

        public MyHystrixCollapser(String personName) {
            this.personName = personName;
        }

        @Override
        public String getRequestArgument() {
            return personName;
        }

        @Override
        protected HystrixCommand<Map<String, Person>> createCommand(Collection<CollapsedRequest<Person, String>> requests) {
            return new CollapserCommand(requests);
        }

        @Override
        protected void mapResponseToRequests(Map<String, Person> batchResponse, Collection<CollapsedRequest<Person, String>> requests) {
            // 让结果与请求进行关联
            for (CollapsedRequest<Person, String> request : requests) {
                // 获取单个响应返回的结果
                Person singleResult = batchResponse.get(request.getArgument());
                // 关联到请求中
                request.setResponse(singleResult);
            }
        }
    }
    
    public static void main(String[] args) throws Exception {
        // 收集 1 秒内发生的请求,合并为一个命令执行
        ConfigurationManager.getConfigInstance().setProperty("hystrix.collapser.default.timerDelayinMilliseconds",1000);

        // 请求上下文
        HystrixRequestContext context = HystrixRequestContext.initializeContext();

        // 创建请求合并处理器
        MyHystrixCollapser c1 = new MyHystrixCollapser("Angus");
        MyHystrixCollapser c2 = new MyHystrixCollapser("Crazyit");
        MyHystrixCollapser c3 = new MyHystrixCollapser("Sune");
        MyHystrixCollapser c4 = new MyHystrixCollapser("Paris");

        // 异步执行
        Future<Person> f1 = c1.queue();
        Future<Person> f2 = c2.queue();
        Future<Person> f3 = c3.queue();
        Future<Person> f4 = c4.queue();

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());

        context.shutdown();
    }

}

在命令类 CollapserCommand 中, 维护着 CollapsedRequest 集合,一个CollapsedReuest实例表示一个请求,该类指定的第一个类型为“单请求返回的类型” 第二个类型为请求的参数类型。例如, CollapsedRequest<Person, String >,表示单次请求将会以 String 作为参数,返回一个 Person 实例。 代码清单中的粗体部分模拟调用查询多个 Person 的服务。

合并处理器中实现了父类 的3个方法, getRequestArgument 用于返回请求的参数 ,本例需要根据名称查询 Person,因此该参数为字符串:createCommand 返回实际执行的命令 (查找多个Person的批处理命令) ; mapResponseToRequests 方法将会在返回结果后执行, 可在该方法中设置结果与请求之间的关联。

运行类设置了“时间段”,在1秒内执行的请求将会被合并到一起执行,该“时间段” 的默认值为10毫秒。

执行结果:

收集参数后执行命令,参数数量:4
id: 79aa8267-7e9d-4285-be51-284469e2e4ab, name: Angus, age: 5
id: a0f9d22c-b03c-4aba-9b5b-e4c2389322bf, name: Crazyit, age: 9
id: f40490fa-fe5d-4dae-9510-d3c10c1676bc, name: Sune, age: 2
id: c6f2c140-f643-4978-946e-e8355335a69d, name: Paris, age: 27

虽然合并请求后只执行了一个命令,只启动了一个线程,只进行了一次网络请求,但是在收集请求、合并请求、处理结果的过程中仍然会耗费一定的时间。 总的来说,一般情况下,合并请求进行批处理,比发送多个请求快,对于一些服务的 URL 相同 、参数不同 的请求,笔者推荐使用合并请求的功能

9、请求缓存

Hystrix 支持缓存功能,如果在一次请求的过程中,多个地方调用同一个接口,可以考虑使用缓存。打开后,下一次的命令不会执行,直接到缓存中获取响应并返回,具体可参4 节介绍的运作流程 。开启缓存较为简单,在命令中重写父类的 getCacheKey即可。 代码清单:测试开启缓存和清空缓存。

/**
 * 测试缓存的打开与关闭
 */
public class CacheMain {
    public static void main(String[] args) {
        // 初始化请求上下文,告知:这是一次请求
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
        // 定义一个key
        String cacheKey = "cache-key";

        // 创建命令,开始执行
        CacheCommand cacheCommand1 = new CacheCommand(cacheKey);
        CacheCommand cacheCommand2 = new CacheCommand(cacheKey);
        CacheCommand cacheCommand3 = new CacheCommand(cacheKey);
        // 从结果可以发现,run只执行了一次
        cacheCommand1.execute();
        cacheCommand2.execute();
        cacheCommand3.execute();
        System.out.println("命令cacheCommand1是否读取缓存:" + cacheCommand1.isResponseFromCache());
        System.out.println("命令cacheCommand2是否读取缓存:" + cacheCommand2.isResponseFromCache());
        System.out.println("命令cacheCommand3是否读取缓存:" + cacheCommand3.isResponseFromCache());

        // 清空缓存
        HystrixRequestCache cache = HystrixRequestCache.getInstance(HystrixCommandKey.Factory.asKey("MyCommoandKey"),
                HystrixConcurrencyStrategyDefault.getInstance());
        cache.clear(cacheKey);

        CacheCommand cacheCommand4 = new CacheCommand(cacheKey);
        cacheCommand4.execute();
        System.out.println("命令cacheCommand4是否读取缓存:" + cacheCommand4.isResponseFromCache());
        // 关闭
        context.shutdown();
    }

    static class CacheCommand extends HystrixCommand<String> {

        public String cacheKey;

        public CacheCommand(String cacheKey) {
            super(Setter.withGroupKey(
                    HystrixCommandGroupKey.Factory.asKey("GroupKey"))
                    .andCommandKey(HystrixCommandKey.Factory.asKey("MyCommoandKey")));
            this.cacheKey = cacheKey;
        }

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

        @Override
        protected String getFallback() {
            System.out.println("getFallback()");
            return "fallback";
        }

        @Override
        protected String getCacheKey() {
            return this.cacheKey;
        }
    }
}

在代码清单的运行类中,使用了同一个缓存的 key 来创建命令实例。注意粗体代码,使用HystrixRequestCache 来清空缓存。获取 HystrixRequestCache 实例需要传入 CommandKey, 因此在命令的构造器中,也设置了 CommandKey 。

根据输出结果可知,命令实际上只执行了两次: c2与c3 这两个命令执行时都读取了缓存,而 c4 在执行前清空了缓存。 运行结果如下:

执行run()方法
命令cacheCommand1是否读取缓存:false
命令cacheCommand2是否读取缓存:true
命令cacheCommand3是否读取缓存:true
执行run()方法
命令cacheCommand4是否读取缓存:false

合并请求、请求缓存,在一次请求的过程中才能实现,因此需要先初始化请求上下文