SpringCloud下基于Hystrix的服务实践
Hystrix是Netflix开源的一款分布式容错框架,其为微服务提供了一整套服务保护、容错机制。从而避免由于个别服务故障而引起的级联故障,即所谓的服务雪崩效应

服务降级
所谓服务降级是指所调用服务发生意外时,调用自己本地方法(即fallback降级方法)返回缺省值。而导致服务降级的原因包括但不限于服务超时、异常、宕机、熔断、服务资源不足(例如线程、信号量等)等。实践过程中,首先在payment服务的POM文件引入Hystrix依赖,如下所示
<dependencyManagement>
  <dependencies>
    
    <dependency>
      <groupId>org.springframework.bootgroupId>
      <artifactId>spring-boot-dependenciesartifactId>
      <version>2.2.2.RELEASEversion>
      <type>pomtype>
      <scope>importscope>
    dependency>
    
    <dependency>
      <groupId>org.springframework.cloudgroupId>
      <artifactId>spring-cloud-dependenciesartifactId>
      <version>Hoxton.SR1version>
      <type>pomtype>
      <scope>importscope>
    dependency>
  dependencies>
dependencyManagement>
<dependencies>
  
  
  <dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-netflix-hystrixartifactId>
  dependency>
dependencies>
可通过@HystrixCommand注解,配置该方法所对应的降级方法。具体通过fallbackMethod属性配置一个入参、出参类型一致的降级方法。进一步地,还可以通过commandProperties属性设置相关降级配置。但如果需要对每个@HystrixCommand注解都添加重复的配置,显然十分麻烦。故可以在类上添加@DefaultProperties注解设置该类默认的降级属性配置,其会对该类中添加了@HystrixCommand注解的方法,提供默认的降级配置。特别地对于该类默认的降级方法,一方面通过defaultFallback属性设置;另一方面该降级方法是无入参的,但出参类型需要与被降级的方法保持一致
@RestController
@RequestMapping("pay2")
// 该类默认的降级配置
@DefaultProperties(defaultFallback = "defaultFallback")
public class PaymentController2 {
    @GetMapping("/test1")
    // 单独配置该方法的降级方法, 并将方法的超时配置由默认1s改为5s
    @HystrixCommand(fallbackMethod = "timeoutFallback", commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
    })
    public String test1(@RequestParam Integer time) {
        // 模拟业务耗时
        try {
            Thread.sleep( time );
        } catch (Exception e) {
        }
        String msg = "[Payment Service - test1], time:" + time;
        return msg;
    }
    @GetMapping("/test2")
    // 使用@DefaultProperties注解提供的默认降级相关配置
    @HystrixCommand
    public String test2(@RequestParam Integer time) {
        // 模拟业务耗时
        try {
            Thread.sleep( time );
        } catch (Exception e) {
        }
        String msg = "[Payment Service - test2], time:" + time;
        return msg;
    }
    @GetMapping("/test3")
    // 使用@DefaultProperties注解提供的默认降级相关配置
    @HystrixCommand
    public String test3(@RequestParam Integer num) {
        int result = 10/num;
        String msg = "[Payment Service - test3], num:" + num;
        return msg;
    }
    /**
     * 超时降级方法
     * @param time
     * @return
     */
    public String timeoutFallback(Integer time) {
        return "payment服务发生超时, param: time: " + time;
    }
    /**
     * 默认降级方法
     * @return
     */
    public String defaultFallback() {
        return "payment服务暂不可用";
    }
}
最后,在启动类上添加@EnableHystrix注解以启用Hystrix
@SpringBootApplication
@EnableDiscoveryClient // 使用Consul作为注册中心时使用
@EnableHystrix // 启用Hystrix
public class PaymentApplication {
    public static void main(String[] args) {
        SpringApplication.run(PaymentApplication.class, args);
    }
}
测试效果如下,符合预期

服务熔断
Hystrix的服务熔断采用断路器模式的思想进行设计。在该模式中存在如下三种状态
Closed:关闭状态。接受请求,服务的主逻辑可以被访问调用 Open:打开状态。拒绝请求,服务的主逻辑无法执行。该服务如果有相应的降级方法则执行降级方法,否则直接抛出错误响应 Half-Open:半开状态。处于该状态时,其会允许一部分请求流量通过,尝试执行主逻辑。如果发现调用成功则说明当前服务可以被恢复,进而变为Closed状态;否则调用失败,继续变为Open状态 
从上可以看到,正是由于Half-Open状态的存在,为服务的自我恢复提供了一种思路。关于上述三种状态的转换逻辑,如下所示。在服务初始阶段,断路器肯定处于Closed状态。在断路器统计的滑动时间窗口内,满足一定的打开条件时,断路器才会进入Open状态。当断路器进入Open状态达到一定时间后会进入Half-Open状态,让一部分请求通过以验证服务是否可以被恢复,并根据验证结果决定进入Closed或Open状态

这里需要补充说明的是,断路器统计的滑动时间窗口可通过 metrics.rollingStats.timeInMilliseconds 进行配置。而断路器的打开条件则是指在该时间窗口内,其接受的请求数、请求的失败率均达到阈值。需要注意的是,这两个阈值必须同时满足。可分别通过 circuitBreaker.requestVolumeThreshold、circuitBreaker.errorThresholdPercentage 进行配置。最后,对于断路器在进入Open状态后需要多长时间才会进入Half-Open状态,则可通过 circuitBreaker.sleepWindowInMilliseconds 进行配置
为了方便验证测试,我们在PaymentController2类继续添加一个新的方法test4
@RestController
@RequestMapping("pay2")
// 该类默认的降级配置
@DefaultProperties(defaultFallback = "defaultFallback")
public class PaymentController2 {
    ...
    @GetMapping("/test4")
    @HystrixCommand( commandProperties = {
        // 打开断路器
        @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
        // 断路器统计的滑动时间窗口, Unit: ms
        @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000"),
        // 断路器的请求数阈值
        @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "5"),
        // 断路器的请求失败率阈值, Unit: %
        @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "70"),
        // 断路器进入Open打开状态多长时间后, 允许再次尝试处理部分请求(即Half-Open半开状态), Unit: ms
        @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000")
    })
    public String test4(@RequestParam Integer num) {
        if(num < 50) {
            throw new RuntimeException("非法参数异常");
        }
        String msg = "[Payment Service - test4], num:" + num;
        return msg;
    }
    /**
     * 默认降级方法
     * @return
     */
    public String defaultFallback() {
        return "payment服务暂不可用";
    }
}
测试结果如下所示。第①次访问时,可以看到访问正常,执行了主流程;第②次访问时,发送了5个请求且访问结果均失败被降级;然后马上开始第③次访问,从测试结果可以看出由于该服务已经被熔断了,直接走降级方法;等过了一段时间后,服务恢复第④次访问成功

服务限流
Hystrix提供了两种隔离策略:线程池隔离、信号量隔离。这里我们以前者为例介绍如何进行限流,信号量隔离同理。在线程池隔离的场景下,可通过控制线程池相关参数实现流量控制。这样当线程池资源不足时,对于新的请求就进行降级。为了方便验证测试,我们在PaymentController2类继续添加一个新的方法test5
@RestController
@RequestMapping("pay2")
// 该类默认的降级配置
@DefaultProperties(defaultFallback = "defaultFallback")
public class PaymentController2 {
    ...
    
    @GetMapping("/test5")
    @HystrixCommand(
        commandProperties = {
            // 方法超时配置, Unit: ms
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "15000"),
            // 资源隔离模式: 线程池隔离
            @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD")
        },
        threadPoolProperties = {
            // 核心线程数
            @HystrixProperty(name = "coreSize", value = "2"),
            // 等待队列容量, -1表示不启用
            @HystrixProperty(name = "maxQueueSize", value = "-1")
        }
    )
    public String test5(@RequestParam Integer num) {
        if(num < 50) {
            throw new RuntimeException("非法参数异常");
        }
        // 模拟业务耗时
        try {
            Thread.sleep( 10000 );
        } catch (Exception e) {
        }
        String msg = "[Payment Service - test5], num:" + num;
        return msg;
    }
    /**
     * 默认降级方法
     * @return
     */
    public String defaultFallback() {
        return "payment服务暂不可用";
    }
}
对于该服务接口其最大并发量为2。这样同时发送3个请求时,可以看到前2个请求正常处理。但第3个请求由于(线程池)资源不足被限流了,故进行降级处理。测试结果符合预期

服务监控
Hystrix还提供了Dashboard以便通过可视化的方式实现服务监控。这里我们建立一个HystrixDashboard服务对目标服务进行监控。首先在POM中我们引入 spring-cloud-starter-netflix-hystrix-dashboard 依赖,如下所示
<dependencyManagement>
  <dependencies>
    
    <dependency>
      <groupId>org.springframework.bootgroupId>
      <artifactId>spring-boot-dependenciesartifactId>
      <version>2.2.2.RELEASEversion>
      <type>pomtype>
      <scope>importscope>
    dependency>
    
    <dependency>
      <groupId>org.springframework.cloudgroupId>
      <artifactId>spring-cloud-dependenciesartifactId>
      <version>Hoxton.SR1version>
      <type>pomtype>
      <scope>importscope>
    dependency>
  dependencies>
dependencyManagement>
<dependencies>
  
  
  <dependency>
      <groupId>org.springframework.cloudgroupId>
      <artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
  dependency>
  
  <dependency>
      <groupId>org.springframework.bootgroupId>
      <artifactId>spring-boot-starter-webartifactId>
  dependency>
dependencies>
然后为该服务添加一个启动类即可,并通过添加@EnableHystrixDashboard注解实现Hystrix Dashboard的启用
@SpringBootApplication
// 启用 Hystrix Dashboard
@EnableHystrixDashboard
public class HystrixDashboardApplication {
    public static void main(String[] args) {
        SpringApplication.run(HystrixDashboardApplication.class, args);
    }
}
对于HystrixDashboard服务而言,其配置文件如下所示
spring:
  application:
    name: HystrixDashboard
server:
  port: 9111
这里我们以监控payment服务为例展开说明,在此之前我们需要对被监控服务payment进行一些必要的配置。首先需要在POM文件中添加 spring-boot-starter-actuator 依赖
<dependencies>
  
  
  <dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-actuatorartifactId>
    <version>2.2.2.RELEASEversion>
  dependency>
dependencies>
与此同时,需要配置Actuator以开启 hystrix.stream 端点,相关配置如下所示
# Actuator配置: 开启 hystrix.stream 端点
management:
  endpoints:
    web:
      exposure:
        include: hystrix.stream
      base-path: /actuator
启动payment、HystrixDashboard服务,分别使用8006、9111端口。首先访问payment服务的 hystrix.stream 端点验证是否有相关监控数据,打开 http://localhost:8006/actuator/hystrix.stream 页面。效果如下,符合预期

然后访问HystrixDashboard服务,打开 http://localhost:9111/hystrix 页面。并在页面上填写被监控服务的 hystrix.stream 端点地址、延迟时间、被监控服务名称

最后,点击Monitor Stream开启监控,效果如下所示

参考文献
Spring微服务实战 John Carnell著 
