SpringCloud 源码系列(16)— 熔断器Hystrix 之 基础入门篇

1,551 阅读18分钟

专栏系列文章:SpringCloud系列专栏

系列文章:

SpringCloud 源码系列(1)— 注册中心Eureka 之 启动初始化

SpringCloud 源码系列(2)— 注册中心Eureka 之 服务注册、续约

SpringCloud 源码系列(3)— 注册中心Eureka 之 抓取注册表

SpringCloud 源码系列(4)— 注册中心Eureka 之 服务下线、故障、自我保护机制

SpringCloud 源码系列(5)— 注册中心Eureka 之 EurekaServer集群

SpringCloud 源码系列(6)— 注册中心Eureka 之 总结篇

SpringCloud 源码系列(7)— 负载均衡Ribbon 之 RestTemplate

SpringCloud 源码系列(8)— 负载均衡Ribbon 之 核心原理

SpringCloud 源码系列(9)— 负载均衡Ribbon 之 核心组件与配置

SpringCloud 源码系列(10)— 负载均衡Ribbon 之 HTTP客户端组件

SpringCloud 源码系列(11)— 负载均衡Ribbon 之 重试与总结篇

SpringCloud 源码系列(12)— 服务调用Feign 之 基础使用篇

SpringCloud 源码系列(13)— 服务调用Feign 之 扫描@FeignClient注解接口

SpringCloud 源码系列(14)— 服务调用Feign 之 构建@FeignClient接口动态代理

SpringCloud 源码系列(15)— 服务调用Feign 之 结合Ribbon进行负载均衡请求

熔断器 Hystrix

分布式系统高可用问题

在分布式系统中,服务与服务之间的依赖错综复杂,某些服务出现故障,可能导致依赖于它们的其他服务出现级联阻塞故障。

例如下图,某个请求要调用 Service-A,Service-A 又要调用 Service-B、Service-D,Service-B 又要再调用 Service-C,Service-C 又依赖于数据库、Redis等中间件。如果在调用 Service-C 时,由于 Service-C 本身业务问题或数据库宕机等情况,就可能会导致 Service-B 的工作线程全部占满导致不可用,进而又导致级联的 Service-A 资源耗尽不可用,这时就会导致整个系统出现大面积的延迟或瘫痪。

在高并发的情况下,单个服务的延迟会导致整个请求都处于延迟阻塞状态,最终的结果就是整个服务的线程资源消耗殆尽。服务的依赖性会导致依赖于该故障服务的其他服务也处于线程阻塞状态,最终导致这些服务的线程资源消耗殆尽,直到不可用,从而导致整个微服务系统都不可用,即雪崩效应。

例如,对于依赖 30 个服务的应用程序,每个服务的正常运行时间为 99.99%,对于单个服务来说,99.99% 的可用是非常完美的。有 99.99^30 = 99.7% 的可正常运行时间和 0.3% 的不可用时间,那么 10 亿次请求中就有 3000000 次失败,实际的情况可能比这更糟糕。

如果不设计整个系统的韧性,即使所有依赖关系表现良好,单个服务只有 0.01% 的不可用,由于整个系统的服务相互依赖,最终对整个系统的影响是非常大的。

Hystrix 简介

1、简介

Hystrix是由Netflix开源的一个针对分布式系统容错处理的开源组件,旨在隔离远程系统、服务和第三方库,阻止级联故障,在复杂的分布式系统中实现恢复能力,从而提高了整个分布式系统的弹性。

例如在上图中,如果在 Service-B 调用 Service-C 时,引入 Hystrix 进行资源隔离,Service-C 故障或调用超时就自动降级返回,从而隔离了 Service-C 对 Service-B 的级联影响,进而保证了整个系统的稳定性。

2、Hystrix的设计原则

总的来说Hystrix 的设计原则如下:

  • 防止单个服务的故障耗尽整个服务的Servlet容器(例如Tomcat)的线程资源。
  • 快速失败机制,如果某个服务出现了故障,则调用该服务的请求快速失败,而不是线程等待。
  • 提供回退(fallback)方案,在请求发生故障时,提供设定好的回退方案。
  • 使用熔断机制,防止故障扩散到其他服务。
  • 提供熔断器的监控组件Hystrix Dashboard,可以实时监控熔断器的状态。

3、官方文档

GitHub:github.com/Netflix/Hys…

官方文档:github.com/Netflix/Hys…

注意:Hystrix 不再开发新功能,目前处于维护模式,最新稳定版本是 1.5.18

使用前在 spring cloud 项目中引入 netflix 相关依赖:我本地使用的 spring-cloud-netflix-hystrix 版本为 2.2.5.RELEASE

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

4、参考书籍:

  • 《深入理解 Spring Cloud 与微服务构建(第 2 版)》
  • 《重新定义Spring Cloud实战》

Hystrix的工作机制

Hystrix的工作机制可以参考官方【How-it-Works】。下面这幅图展示了 Hystrix 的核心工作原理。

核心的原理如下:

  • ① 将对依赖服务的请求调用封装到 HystrixCommand 或者 HystrixObservableCommand 中,这个访问请求一般会在独立的线程中执行。区别在于:

    • HystrixCommand 是用来获取一条数据的。
    • HystrixObservableCommand 是设计用来获取多条数据的,返回类型是 Observable。
  • ② 执行请求,有四个方法可以调用:execute()、queue()、observe()、toObservable(),HystrixCommand 四个都可以调用,HystrixObservableCommand 只能调用 observe()toObservable()

  • ③ 如果这个命令启用了缓存,且缓存中存在,就直接返回缓存中的数据。

  • ④ 检查断路器是否打开了,如果断路器打开了就直接走快速失败的逻辑返回。

  • ⑤ 检查线程池(线程池隔离)或队列(信号量隔离)是否已满,满了就直接走快速失败的逻辑返回。

  • ⑥ 执行请求调用远程服务,如果执行执行失败或超时,就走降级逻辑返回。否则成功返回数据。

  • ⑦ 每次执行请求时,线程池是否拒绝、执行是否失败、超时等,都会进行统计,然后计算是否打开断路器。

    • 当某个接口的失败次数超过设定的阈值时,Hystrix 判定该 API 接口出现了故障,打开熔断器,这时请求该 API 接口会执行快速失败的逻辑,不执行业务逻辑,请求的线程不会处于阻塞状态。
    • 处于打开状态的熔断器,一段时间后会处于半打开状态,并放行一定数量的请求执行正常逻辑。剩余的请求会执行快速失败。
    • 若执行正常逻辑的请求失败了,则熔断器继续打开;若成功了,则将熔断器关闭。这样熔断器就具有了自我修复的能力。
  • ⑧ 执行快速失败的逻辑,也就是降级逻辑,如果降级逻辑执行成功,则返回;如果失败需要自行处理,或抛出异常。

HystrixCommand

关于 HystrixCommand 或者 HystrixObservableCommand 的用法官方文档【How To Use】已经介绍得很详细了,这里就跟着官方文档中提供的例子来了解下 HystrixCommand 的使用方式以及 Hystrix 的特性、配置等。

构建 HystrixCommand

1、构建 HystrixCommand

继承 HystrixCommand,将业务逻辑封装到 HystrixCommand 的 run() 方法中,在 getFallback() 方法中实现错误回调的逻辑。

class CommandHello extends HystrixCommand<String> {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final String name;
    private final long timeout;

    protected CommandHello(String group, String name, long timeout) {
        // 指定命令的分組,同一组使用同一个线程池
        super(HystrixCommandGroupKey.Factory.asKey(group));
        this.name = name;
        this.timeout = timeout;
    }

    // 要封装的业务请求
    @Override
    protected String run() throws Exception {
        logger.info("hystrix command execute");
        if (name == null) {
            throw new RuntimeException("data exception");
        }
        Thread.sleep(timeout); // 休眠
        return "hello " + name;
    }

    // 快速失败的降级逻辑
    @Override
    protected String getFallback() {
        logger.info("return fallback data");
        return "error";
    }
}

2、构建 HystrixObservableCommand

继承 HystrixObservableCommand,将业务逻辑封装到 HystrixObservableCommand 的 construct() 方法中,在 resumeWithFallback() 方法中实现错误回调的逻辑。

class CommandObservableHello extends HystrixObservableCommand<String> {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    protected CommandObservableHello(String group) {
        // 指定命令的分組,同一组使用同一个线程池
        super(HystrixCommandGroupKey.Factory.asKey(group));
    }

    @Override
    protected Observable<String> construct() {
        logger.info("hystrix command execute");
        return Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                // 发送多条数据
                subscriber.onNext("hello world");
                subscriber.onNext("hello hystrix");
                subscriber.onNext("hello command");
                subscriber.onCompleted();
            }
        });
    }

    // 快速失败的降级逻辑
    @Override
    protected Observable<String> resumeWithFallback() {
        logger.info("return fallback data");
        return Observable.just("error");
    }
}

执行 HystrixCommand

1、执行请求的方法

每次执行 HystrixCommand 都需要重新创建一个 HystrixCommand 命令,然后调用 execute()queue()observe()toObservable() 中的一个方法执行请求。HystrixCommand 可以调用四个方法,HystrixObservableCommand 只能调用 observe()、toObservable() 两个方法。

四个方法的区别在于:

  • execute():同步调用,直到返回单条结果,或者抛出异常
  • queue():异步调用,返回一个 Future,可以异步的做其它事情,后面可以通过 Future 获取单条结果
  • observe():返回一个订阅对象 Observable,立即执行。可以通过 Observable.toBlocking() 执行同步请求。
  • toObservable():返回一个 Observable,只有订阅后才会执行

进入 execute() 方法可以发现,execute() 调用了 queue().get();再进入 queue() 方法可以发现,queue() 实际又调用了 toObservable().toBlocking().toFuture(),也就是说,无论是哪种方式执行command,最终都是依赖 toObservable() 去执行的。

2、HystrixCommand 执行请求

可以看到 HystrixCommand 中的业务执行是在一个单独的线程中执行的,线程名称为 hystrix-ExampleGroup-1,这就实现了资源的隔离了。

/**
 * 运行结果:
 *
 * 14:56:03.699 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute
 * 14:56:04.206 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - execute result is: hello hystrix
 */
@Test
public void test_HystrixCommand_execute() {
    CommandHello command = new CommandHello("ExampleGroup", "hystrix", 500);
    // 同步执行
    String result = command.execute();
    logger.info("execute result is: {}", result);
}

/**
 * 运行结果:
 *
 * 14:56:19.269 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - do something...
 * 14:56:19.279 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute
 * 14:56:19.785 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - queue result is: hello hystrix
 */
@Test
public void test_HystrixCommand_queue() throws Exception {
    CommandHello command = new CommandHello("ExampleGroup", "hystrix", 500);
    // 异步执行,返回 Future
    Future<String> future = command.queue();
    logger.info("do something...");
    logger.info("queue result is: {}", future.get());
}

/**
 * 运行结果
 *
 * 14:59:56.748 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute
 * 14:59:57.252 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - observe result is: hello hystrix
 */
@Test
public void test_HystrixCommand_observe_single() {
    CommandHello command = new CommandHello("ExampleGroup", "hystrix", 500);
    Observable<String> observable = command.observe();
    // 获取请求结果,toBlocking() 是为了同步执行,不加 toBlocking() 就是异步执行
    String result = observable.toBlocking().single();
    logger.info("observe result is: {}", result);
}

/**
 * 运行结果:
 *
 * 15:00:58.921 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute
 * 15:00:59.424 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - subscribe result is: hello hystrix
 * 15:00:59.425 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - completed
 */
@Test
public void test_HystrixCommand_observe_subscribe() {
    CommandHello command = new CommandHello("ExampleGroup", "hystrix", 500);
    Observable<String> observable = command.observe();
    // 订阅结果处理
    observable.toBlocking().subscribe(new Subscriber<String>() {
        @Override
        public void onCompleted() {
            logger.info("completed");
        }

        @Override
        public void onError(Throwable e) {
            logger.info("error", e);
        }

        @Override
        public void onNext(String s) {
            logger.info("subscribe result is: {}", s);
        }
    });
}

3、HystrixObservableCommand 执行请求

HystrixObservableCommand 与 HystrixCommand 是类似的,区别在于 HystrixObservableCommand 的业务逻辑是封装到 construct() 方法中,且 HystrixObservableCommand 只能调用 observe()toObservable() 两个方法执行命令。

/**
 * 运行结果:
 *
 * 15:22:49.306 [main] INFO com.lyyzoo.hystrix.CommandObservableHello - hystrix command execute
 * 15:22:49.316 [main] INFO com.lyyzoo.hystrix.Demo02_HystrixObservableCommand - last result: hello command
 */
@Test
public void test_HystrixObservableCommand_observe() {
    CommandObservableHello command = new CommandObservableHello("ExampleGroup");

    String result = command.observe().toBlocking().last();
    logger.info("last result: {}", result);
}

/**
 * 运行结果:
 *
 * 15:23:08.685 [main] INFO com.lyyzoo.hystrix.CommandObservableHello - hystrix command execute
 * 15:23:08.691 [main] INFO com.lyyzoo.hystrix.Demo02_HystrixObservableCommand - result data: hello world
 * 15:23:08.691 [main] INFO com.lyyzoo.hystrix.Demo02_HystrixObservableCommand - result data: hello hystrix
 * 15:23:08.691 [main] INFO com.lyyzoo.hystrix.Demo02_HystrixObservableCommand - result data: hello command
 * 15:23:08.691 [main] INFO com.lyyzoo.hystrix.Demo02_HystrixObservableCommand - completed
 */
@Test
public void test_HystrixObservableCommand_observe_subscribe() {
    CommandObservableHello command = new CommandObservableHello("ExampleGroup");

    command.observe().subscribe(new Observer<String>() {
        @Override
        public void onCompleted() {
            logger.info("completed");
        }

        @Override
        public void onError(Throwable e) {
            logger.info("error");
        }

        @Override
        public void onNext(String o) {
            logger.info("result data: {}", o);
        }
    });
}

容错和降级回调

1、容错模式

HystrixCommand 执行有两种容错模式,fail-fastfail-silent

  • fail-fast:不给fallback降级逻辑,run() 方法报错后直接抛异常,抛出到主工作线程。
  • fail-silent:给一个fallback降级逻辑,run() 报错了会走fallback降级。

基本不会用 fail-fast 模式,一般来说都会实现降级逻辑,在抛出异常后做一些兜底的事情。

2、实现降级方法

HystrixCommand 可以通过 getFallback() 方法返回降级的数据,HystrixObservableCommand 可以通过 resumeWithFallback() 返回降级的数据。当快速失败、超时、断路器打开的时候,就可以进入降级回调方法中,例如从本地缓存直接返回数据,避免业务异常、阻塞等情况。

通过文档可以了解到,有5中情况会进入降级回调方法中,分别是 业务异常、业务超时、断路器打开、线程池拒绝、信号量拒绝

3、Examples:

/**
 * 运行结果:
 *
 * 14:44:43.199 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - hystrix command execute
 * 14:44:43.203 [hystrix-ExampleGroup-1] DEBUG com.netflix.hystrix.AbstractCommand - Error executing HystrixCommand.run(). Proceeding to fallback logic ...
 * java.lang.RuntimeException: data exception
 * 	at com.lyyzoo.hystrix.Demo01_HystrixCommand$CommandHello.run(Demo01_HystrixCommand.java:75)
 * 	at com.lyyzoo.hystrix.Demo01_HystrixCommand$CommandHello.run(Demo01_HystrixCommand.java:61)
 * 	at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:302)
 * 	........
 * 14:44:43.210 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - return fallback data
 * 14:44:43.214 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result is: error
 */
@Test
public void test_HystrixCommand_exception_fallback() {
    CommandHello command = new CommandHello("ExampleGroup", null, 500);
    // 抛出异常,返回降级逻辑中的数据
    String result = command.execute();
    logger.info("result is: {}", result);
}

/**
 * 运行结果:
 *
 * 14:51:27.114 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute
 * 14:51:28.113 [HystrixTimer-1] INFO com.lyyzoo.hystrix.CommandHello - return fallback data
 * 14:51:28.119 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result is: error
 */
@Test
public void test_HystrixCommand_timeout_fallback() {
    CommandHello command = new CommandHello("ExampleGroup", "hystrix", 1500);
    // 请求超时,返回降级逻辑中的数据
    String result = command.execute();
    logger.info("result is: {}", result);
}

4、多级降级

多级降级其实就是在降级逻辑中再嵌套一个 command,command 嵌套 command 的形式。比如先从 MySQL 获取数据,抛出异常时,降级从 redis 获取数据。另外,不同的降级策略建议使用不同的线程池,因为如果 command 调用远程服务时耗尽了线程池,降级 command 使用同样的线程池时就会被拒绝。

Examples:

可以看到,首先调用 CommandMySQL,run() 方法正常时就直接返回结果,抛出异常后就进入降级方法中,在降级方法中又用 CommandRedis 执行请求。

public class Demo03_HystrixCommand_MultiFallback {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 请求结果:
     
     * 22:28:46.313 [hystrix-MySqlPool-1] INFO com.lyyzoo.hystrix.CommandMySQL - get data from mysql
     * 22:28:46.319 [main] INFO com.lyyzoo.hystrix.Demo03_HystrixCommand_MultiFallback - result: mysql-number-1
     * 22:28:46.320 [hystrix-MySqlPool-2] INFO com.lyyzoo.hystrix.CommandMySQL - get data from mysql
     * 22:28:46.324 [hystrix-MySqlPool-2] DEBUG com.netflix.hystrix.AbstractCommand - Error executing HystrixCommand.run(). Proceeding to fallback logic ...
     * java.lang.RuntimeException: data not found in mysql
     * 	at com.lyyzoo.hystrix.CommandMySQL.run(Demo03_HystrixCommand_MultiFallback.java:50)
     * 	at com.lyyzoo.hystrix.CommandMySQL.run(Demo03_HystrixCommand_MultiFallback.java:29)
     * 	at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:302)
     * 	......
     * 22:28:46.332 [hystrix-MySqlPool-2] INFO com.lyyzoo.hystrix.CommandMySQL - coming mysql fallback
     * 22:28:46.344 [hystrix-RedisPool-1] INFO com.lyyzoo.hystrix.CommandRedis - get data from redis
     * 22:28:46.344 [main] INFO com.lyyzoo.hystrix.Demo03_HystrixCommand_MultiFallback - result: redis-number-2
     */
    @Test
    public void test_HystrixCommand_multi_fallback() {
        CommandMySQL command = new CommandMySQL("ExampleGroup", 1);
        logger.info("result: {}", command.execute());

        CommandMySQL command2 = new CommandMySQL("ExampleGroup", 2);
        logger.info("result: {}", command2.execute());
    }

}

class CommandMySQL extends HystrixCommand<String> {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final String group;
    private final Integer id;

    public CommandMySQL(String group, Integer id) {
        super(
            HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey(group))
                // 指定不同的线程池
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("MySqlPool"))
        );
        this.group = group;
        this.id = id;
    }

    @Override
    protected String run() throws Exception {
        logger.info("get data from mysql");
        if (id % 2 == 0) {
            throw new RuntimeException("data not found in mysql");
        }
        return "mysql-number-" + id;
    }

    // 快速失败的降级逻辑
    @Override
    protected String getFallback() {
        logger.info("coming mysql fallback");
        // 嵌套 Command
        HystrixCommand<String> command = new CommandRedis(group, id);
        return command.execute();
    }
}

class CommandRedis extends HystrixCommand<String> {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final Integer id;

    public CommandRedis(String group, Integer id) {
        super(
            HystrixCommand.Setter
                .withGroupKey(HystrixCommandGroupKey.Factory.asKey(group))
                // 指定不同的线程池
                .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("RedisPool"))
        );
        this.id = id;
    }

    @Override
    protected String run() throws Exception {
        logger.info("get data from redis");
        return "redis-number-" + id;
    }

    // 快速失败的降级逻辑
    @Override
    protected String getFallback() {
        logger.info("coming redis fallback");
        return "error";
    }
}

HystrixCommand 维度划分

1、配置分组名称

默认情况下,就是通过 command group 来定义一个线程池,同一个 command group 中的请求,都会进入同一个线程池中,而且还会通过command group来聚合一些监控和报警信息。通过 HystrixCommandGroupKey.Factory.asKey(group) 指定组名。

class CommandHello extends HystrixCommand<String> {

    protected CommandHello(String group) {
        super(HystrixCommandGroupKey.Factory.asKey(group));
    }
}

2、配置命令名称

command 名称不配置默认就是类名,可以通过 andCommandKey(HystrixCommandKey.Factory.asKey("CommandName")) 配置。

例如 CommandHystrixConfig:

public CommandHystrixConfig() {
    super(
        Setter
            // 分组名称
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
            // 命令名称,默认为类名 getClass().getSimpleName()
            //.andCommandKey(HystrixCommandKey.Factory.asKey("ExampleName"))
    );
}

未配置名称时,默认就是类名:

将注释放开,就是指定的名称:

3、配置线程池名称

线程池名称默认是分组名称,如果不想用分组名称,可以手动设置线程池名称,配置了线程名称后,通过 .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ExampleThread")) 配置。

public CommandHystrixConfig() {
    super(
        Setter
            // 分组名称
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
            // 命令名称,默认为类名 getClass().getSimpleName()
            .andCommandKey(HystrixCommandKey.Factory.asKey("ExampleName"))
            // 线程名称
            .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("ExampleThread"))
    );
}

4、线程池隔离划分

综上来看,一个 command 的执行划分为三个维度:command threadpool -> command group -> command key。

  • command key:一般对应到一个依赖服务的接口调用。
  • command group:一般对应到一个服务的所有接口,包含同一个服务的多个接口,便于对同一个服务的接口调用情况聚合统计。
  • command threadpool:command 执行的线程池,一般一个 group 对应一个 threadpool,但如果想细粒度拆分,command 可以指定不同的线程池。

配置资源隔离策略

Hystrix 核心的一项功能就是资源隔离,要解决的最核心的问题,就是将多个依赖服务的调用分别隔离到各自自己的资源池内,避免对某一个依赖服务的调用,因为依赖服务的接口调用的延迟或者失败,导致服务所有的线程资源全部耗费在这个服务的接口调用上,一旦某个服务的线程资源全部耗尽,就可能导致服务崩溃,甚至故障会不断蔓延。

Hystrix 有线程池隔离信号量隔离两种资源隔离技术:

  • 线程池隔离:适合绝大多数的场景,依赖服务调用、网络请求,被调用方可能会不可用、超时等,用线程池隔离
  • 信号量隔离:适合不是对外部依赖的访问,而是对内部的一些比较复杂的业务逻辑的访问,不涉及任何的网络请求,只要做信号量的普通限流就可以了,在高并发的情况下做限流。

1、启用线程池隔离

public CommandHystrixConfig() {
    super(
        Setter
            // 分组名称
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
            .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                    // 设置隔离策略
                    .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
                    // 超时时间,默认 1000 毫秒
                    .withExecutionTimeoutInMilliseconds(1000)
                    // 是否启用降级策略
                    .withFallbackEnabled(true)
                    // 是否启用缓存
                    .withRequestCacheEnabled(true)
            )
    );
}

2、启用信号量隔离

在设置 .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD) 时,只需要将 THREAD 改为 SEMAPHORE 即可,这个时候 run() 方法将在主线程中执行。

3、资源容量大小控制

  • withCoreSize(10):设置线程池的大小,默认是10,一般设置10个就足够了
  • withQueueSizeRejectionThreshold(5):command 在提交到线程池之前会先进入一个队列中,这个队列满了之后,才会reject,这个参数可以控制队列拒绝的阈值。
public CommandHystrixConfig() {
    super(
        Setter
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
            .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("DemoPool"))
            .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                    // 设置隔离策略
                    .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
                    // 超时时间,默认 1000 毫秒
                    .withExecutionTimeoutInMilliseconds(1000)
            )
            .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                    // 核心线程池大小,默认10个
                    .withCoreSize(10)
                    // 最大线程数,默认10个
                    .withMaximumSize(10)
                    // 队列数量达到多少后拒绝请求
                    .withQueueSizeRejectionThreshold(5)
            )
    );
}
  • withExecutionIsolationSemaphoreMaxConcurrentRequests(10):信号量隔离时,设置信号量并发数,与线程池大小的设置类似。
public CommandHystrixConfig() {
    super(
        Setter
            .withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
            .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("DemoPool"))
            .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                    // 设置隔离策略
                    .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
                    // 信号量隔离时最大并发量,默认10
                    .withExecutionIsolationSemaphoreMaxConcurrentRequests(10)
            )
    );
}

开启请求缓存

在一次请求上下文中,可能会创建多个 command 去执行远程调用,如果调用的参数一样、接口一样,hystrix 支持缓存来减少执行同样的 command,从而提升性能。

1、开启缓存

开启缓存只需要在 Command 中实现 getCacheKey() 方法返回缓存的 key 就可以开启缓存了。

// 指定缓存的key
@Override
protected String getCacheKey() {
    return name;
}

2、Examples

Hystrix 的缓存需要自己管理缓存上下文,需要自己初始化缓存上下文 HystrixRequestContext.initializeContext(),请求结束后需要关闭缓存上下文 HystrixRequestContext.getContextForCurrentThread().shutdown()。一般在web应用中,可以通过增加前置过滤器来开启 Hystrix 请求上下文,增加后置过滤器来关闭 Hystrix 上下文。

从验证结果来看,最后两个 command 执行时,由于缓存已经存在同样的 key 了,就没有进入 run 方法了,而且可以验证就算是不同组,只要key相同就会被缓存。

/**
 * 运行结果:
 *
 * 20:55:43.759 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute
 * 20:55:43.878 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result: hello hystrix
 * 20:55:43.879 [hystrix-ExampleGroup-2] INFO com.lyyzoo.hystrix.CommandHello - hystrix command execute
 * 20:55:43.985 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result: hello ribbon
 * 20:55:43.989 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result: hello hystrix
 * 20:55:43.989 [main] INFO com.lyyzoo.hystrix.Demo01_HystrixCommand - result: hello hystrix
 */
@Test
public void test_HystrixCommand_cache() {
    // 先初始化上下文
    HystrixRequestContext context = HystrixRequestContext.initializeContext();

    try {
        CommandHello command1 = new CommandHello("ExampleGroup", "hystrix", 100);
        CommandHello command2 = new CommandHello("ExampleGroup", "ribbon", 100);
        CommandHello command3 = new CommandHello("ExampleGroup", "hystrix", 100);
        CommandHello command4 = new CommandHello("ExampleGroupTwo", "hystrix", 100);

        logger.info("result: {}", command1.execute());
        logger.info("result: {}", command2.execute());
        logger.info("result: {}", command3.execute());
        logger.info("result: {}", command4.execute());
    } finally {
        //HystrixRequestContext.getContextForCurrentThread().shutdown();
        context.shutdown();
    }
}

3、手动清除缓存

当数据更新时,我们期望能够手动清理掉缓存,可以通过如下方式清理指定 command 的缓存。

public static void flushCache(String key) {
    HystrixRequestCache.getInstance(HystrixCommandKey.Factory.asKey(CommandHello.class.getSimpleName()),
            HystrixConcurrencyStrategyDefault.getInstance()).clear(key);
}

开启断路器

1、Hystrix 断路器的基本原理如下

  • 首先请求达到一个阈值后,才会判断是否开启断路器,通过 HystrixCommandProperties.circuitBreakerRequestVolumeThreshold() 设置,默认为 20。
  • 超过阈值后,判断错误比率是否超过阈值,通过 HystrixCommandProperties.circuitBreakerErrorThresholdPercentage() 设置,默认为 50%。
  • 超过 50% 后就会打开断路器,从 CLOSE 状态变为 OPEN
  • 当断路器打开了之后,所有的请求都会被拒绝,直接进入降级逻辑。
  • 休眠一段时间后,通过 HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds() 设置,默认 5000毫秒;会进入半打开(half-open)状态,放一个请求过去,如果这个请求还是失败,断路器就继续维持打开的状态;如果请求成功,就关闭断路器,自动恢复,转到 CLOSE 状态。

2、下面用一个 Demo 简单测试下

断路器配置如下:

// 超时时间1000毫秒
.withExecutionTimeoutInMilliseconds(1000)
// 启用断路器
.withCircuitBreakerEnabled(true)
// 限流阈值,超过这个值后才会去判断是否限流,默认20
.withCircuitBreakerRequestVolumeThreshold(4)
// 请求失败百分比阈值,默认50
.withCircuitBreakerErrorThresholdPercentage(50)
// 断路器打开后休眠多久,默认5000毫秒
.withCircuitBreakerSleepWindowInMilliseconds(5000)

Examples:

通过测试可以发现,请求阈值设置为4,第五个请求会继续执行,不过也失败了,这个时候就会判断失败比率超过50%了,断路器打开,之后的所有请求就直接进入降级;休眠5秒后,进入半打开状态,放一个请求过去,还是失败,断路器继续打开。

public class Demo05_HystrixCommand_CircuitBreaker {
    @Test
    public void test_CircuitBreaker() throws InterruptedException {
        for (int i = 1; i <= 10; i++) {
            CommandCircuitBreaker command = new CommandCircuitBreaker(1500L, i);
            command.execute();
            if (i == 7) {
                Thread.sleep(5000); // 休眠5秒
            }
        }
    }
}

/**
 * 运行结果:
 *
 * 10:53:30.910 [hystrix-ExampleGroup-1] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [1] execute command
 * 10:53:31.920 [HystrixTimer-1] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [1] execute fallback
 * 10:53:31.925 [hystrix-ExampleGroup-2] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [2] execute command
 * 10:53:32.936 [HystrixTimer-1] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [2] execute fallback
 * 10:53:32.937 [hystrix-ExampleGroup-3] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [3] execute command
 * 10:53:33.940 [HystrixTimer-2] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [3] execute fallback
 * 10:53:33.942 [hystrix-ExampleGroup-4] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [4] execute command
 * 10:53:34.950 [HystrixTimer-1] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [4] execute fallback
 * 10:53:34.952 [hystrix-ExampleGroup-5] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [5] execute command
 * 10:53:35.962 [HystrixTimer-3] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [5] execute fallback
 * 10:53:35.964 [main] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [6] execute fallback
 * 10:53:35.965 [main] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [7] execute fallback
 * 10:53:40.977 [hystrix-ExampleGroup-6] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [8] execute command
 * 10:53:41.986 [HystrixTimer-2] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [8] execute fallback
 * 10:53:41.988 [main] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [9] execute fallback
 * 10:53:41.989 [main] INFO com.lyyzoo.hystrix.CommandCircuitBreaker - [10] execute fallback
 */
class CommandCircuitBreaker extends HystrixCommand<String> {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final Long timeoutMill;
    private final Integer id;

    protected CommandCircuitBreaker(Long timeoutMill, Integer id) {
        super(
            Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
                .andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
                    // 超时时间1000毫秒
                    .withExecutionTimeoutInMilliseconds(1000)
                    // 启用断路器
                    .withCircuitBreakerEnabled(true)
                    // 限流阈值,超过这个值后才会去判断是否限流,默认20
                    .withCircuitBreakerRequestVolumeThreshold(4)
                    // 请求失败百分比阈值,默认50
                    .withCircuitBreakerErrorThresholdPercentage(50)
                    // 断路器打开后休眠多久,默认5000毫秒
                    .withCircuitBreakerSleepWindowInMilliseconds(5000)
                )
        );
        this.timeoutMill = timeoutMill;
        this.id = id;
    }

    @Override
    protected String run() throws Exception {
        logger.info("[{}] execute command", id);
        if (timeoutMill != null) {
            Thread.sleep(timeoutMill);
        }
        return "number-" + id;
    }

    @Override
    protected String getFallback() {
        logger.info("[{}] execute fallback", id);
        return "error-" + id;
    }
}

基于注解的方式使用 HystrixCommand

在 springboot 程序中,还可以在组件方法上使用 @HystrixCommand 等注解将方法调用封装到 hystrix 中去执行。同时还需要在启动类上加上 @EnableHystrix 来启用 Hystrix 的功能。

@Component
public class ProducerService {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @HystrixCommand(
            groupKey = "ExampleGroup",
            commandKey = "demo-producer",
            threadPoolKey = "ExamplePool",
            fallbackMethod = "queryFallback"
    )
    public String query(Integer id) {
        logger.info("execute query");
        if (id % 2 == 0) {
            throw new RuntimeException("execution error");
        }
        return "number-" + id;
    }

    // 回调方法参数要和原方法参数一致
    public String queryFallback(Integer id) {
        return "error-" + id;
    }
}

测试结果:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ConsumerApplication.class)
public class ProducerServiceTest {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ProducerService producerService;

    /**
     * 执行结果:
     *
     * INFO 10:49:13 [hystrix-ExamplePool-1] c.l.s.r.h.ProducerService.query 25 :execute query
     * INFO 10:49:13 [main] c.l.h.ProducerServiceTest.test_query 29 :result: number-1
     * INFO 10:49:13 [hystrix-ExamplePool-2] c.l.s.r.h.ProducerService.query 25 :execute query
     * INFO 10:49:13 [main] c.l.h.ProducerServiceTest.test_query 30 :result: error-2
     */
    @Test
    public void test_query() {
        logger.info("result: {}", producerService.query(1));
        logger.info("result: {}", producerService.query(2));
    }
}