Spring Cloud 熔断器之Hystrix

一、概述

  • 微服务之间可能会互相调用、级联调用,复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败。
  • 多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”
  • 因此,对于一些延迟和错误,就需要有对应的处理服务,避免长时间等待造成整体服务的失败。
  • Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
  • 简单来说,熔断器Hystrix防止服务处理时间过长、异常而造成整体服务的失败。对于这些不正常的服务处理,采用备选方案fallback。赶紧先敷衍过去,别让这个服务导致整个服务器瘫痪。
  • Hystrix官网
  • 通过Hystrix可以实现服务降级、服务熔断。

二、服务降级

  • 服务降级就是在调用服务的时候,调用fallback方法作为返回。
  • 当程序运行异常、超时、服务熔断触发服务降级、线程池/信号量打满的时候,就会触发服务降级。
  • 下面是实验过程

1. 导包

  • 最重要的是spring-cloud-starter-netflix-hystrix这个包
<dependencies>
    <!--hystrix-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <!--eureka client-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
复制代码

2. 配置文件

  • 没什么特殊的,基本上都是标配
server:
  port: 8001

spring:
  application:
    name: cloud-provider-hystrix-payment

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka,http://127.0.0.1:7002/eureka
复制代码

3. 主启动类

  • 在主启动类上添加一个@EnableHystrix注解,表示开启Hystrix
@SpringBootApplication
@EnableEurekaClient //本服务启动后会自动注册进eureka服务中
@EnableHystrix
public class PaymentHystrixMain8001 {
  public static void main(String[] args) {
    SpringApplication.run(PaymentHystrixMain8001.class, args);
  }
}
复制代码

4. 业务类

  • 下面有一个运行正常的方法,有一个模拟耗时3s的方法
@Service
public class PaymentService {
  public String runsWell(Integer id) {
    return "一切正常,线程号:" + Thread.currentThread().getName() + ", id:" + id;
  }

  public String runsTimeOut(Integer id) {
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    return "执行超时,线程号:" + Thread.currentThread().getName() + ", id:" + id;
  }
}
复制代码
  • 请求处理。通过在方法上面标注@HystrixCommand声明降级后的处理方法及一些参数(比如说超时时长)
@Slf4j
@RestController
@RequestMapping("/hystrix")
public class HystrixController {
  @Autowired
  PaymentService paymentService;

  @GetMapping("/ok/{id}")
  public String runsWell(@PathVariable("id") Integer id) {
    String result = paymentService.runsWell(id);
    log.info("result: " + result);
    return result;
  }
  
  // 目前设定了超时时间为3s,调用的方法是 runsTimeOutHandler
  @HystrixCommand(fallbackMethod = "runsTimeOutHandler",
          commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")})  
  @GetMapping("/timeOut/{id}")
  public String runsTimeOut(@PathVariable("id") Integer id) {
    String result = paymentService.runsTimeOut(id);
    log.info("result: " + result);
    return result;
  }

  // 设定了服务降级的处理方法是runsTimeOutHandler
  @HystrixCommand(fallbackMethod = "runsTimeOutHandler")
  @GetMapping("/exception/{id}")
  public String runsException(@PathVariable("id") Integer id) {
    Integer result = 10 / 0;
    log.info("result: " + result);
    return result.toString();
  }
  
  // 就是一个平平无奇的方法,值得一提的是,参数列表需要和被降级的方法相同
  public String runsTimeOutHandler(Integer id) {
    String result = "触发了服务降级,当前线程名为:" + Thread.currentThread().getName();
    log.info(result);
    return result;
  }
}
复制代码
  • 至此,完成了简单的服务降级。但有一个问题是,如果每个方法的参数都不同,就需要针对每个方法都写一个降级方法。
  • 因此,可以配置一个默认的降级方法,当方法上面没有指定降级方法时,调用该方法。首先在类上面标注一个注解@DefaultProperties声明默认处理方法,再在需要fallback的方法上面标注@HystrixCommand即可。
@Slf4j
@RestController
@RequestMapping("/hystrix")
@DefaultProperties(defaultFallback = "runsTimeOutHandler")
public class HystrixController {
  @Autowired
  PaymentService paymentService;

  @GetMapping("/ok/{id}")
  public String runsWell(@PathVariable("id") Integer id) {
    String result = paymentService.runsWell(id);
    log.info("result: " + result);
    return result;
  }

  @HystrixCommand(
          commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")})
  @GetMapping("/timeOut/{id}")
  public String runsTimeOut(@PathVariable("id") Integer id) {
    String result = paymentService.runsTimeOut(id);
    log.info("result: " + result);
    return result;
  }

  @HystrixCommand
  @GetMapping("/exception/{id}")
  public String runsException(@PathVariable("id") Integer id) {
    Integer result = 10 / 0;
    log.info("result: " + result);
    return result.toString();
  }

  public String runsTimeOutHandler() {
    String result = "触发了服务降级,当前线程名为:" + Thread.currentThread().getName();
    log.info(result);
    return result;
  }
}
复制代码

5. 和Feign一起

  • 对于Feign来说,也存在超时控制。对此,可以设置超时、异常、宕机之后的fallback
  • 首先,需要在配置文件中开启
feign:
  hystrix:
   enabled: true  # 在Feign中开启Hystrix
复制代码
  • 这是feign的远程调用接口,在注解内声明了一个接口的fallback方法
@Service
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = HystrixServiceFallback.class)
public interface HystrixService {
  @GetMapping("/hystrix/ok/{id}")
  String runsWell(@PathVariable("id") Integer id);

  @GetMapping("/hystrix/timeOut/{id}")
  String runsTimeOut(@PathVariable("id") Integer id);

  @GetMapping("/hystrix/exception/{id}")
  String runsException(@PathVariable("id") Integer id);
}
复制代码
  • 针对上面的接口,写一个实现类,对应它声明的fallback方法
@Component
@Slf4j
public class HystrixServiceFallback implements HystrixService {
  @Override
  public String runsWell(Integer id) {
    String result = "熔断版runsWell";
    log.info(result);
    return result;
  }

  @Override
  public String runsTimeOut(Integer id) {
    String result = "熔断版runsTimeOut";
    log.info(result);
    return result;
  }

  @Override
  public String runsException(Integer id) {
    String result = "熔断版runsException";
    log.info(result);
    return result;
  }
}
复制代码
  • 至此,完成了服务降级的基本内容。总的来说,对于微服务无论是调用方还是被调用方,都可以使用服务降级。可以针对某个方法写一个fallback方法,也可以写一个默认的fallback供多个方法使用,也可以针对feign的远程调用接口写fallback方法。

三、服务熔断

  • 服务熔断就是,当在单位时间内触发了超过N次服务降级,并且服务降级占比超过阈值之后,就会触发服务熔断。当服务过来的时候,直接拒绝尝试服务调用,直接让服务降级处理。之后再慢慢尝试恢复。
  • 相当于保险丝,当多个大功率电器过来之后,发现已经没办法正常工作了,那就直接烧断保险丝,暂时不处理服务。
  • 相关论文
  • 熔断机制:熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
  • 对于Hystrix来说,当5s内出现20次调用失败,就会触发熔断。
  • 熔断一般有三种状态:
    • 熔断关闭:不会对服务进行熔断
    • 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态
    • 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
  • 熔断器三个重要的参数:
    • 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
    • 请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
    • 错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。
  @HystrixCommand(fallbackMethod = "divFallback", commandProperties = {
          @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
          @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
          @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), 
          @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),
  }) //开启,10次,10s, 60%
  @GetMapping("/div/{i}")
  public String div(@PathVariable("i") Integer i) {
    int result = 10 / i;
    log.info("result: " + result);
    return Integer.toString(result);
  }
复制代码

四、工作流程

image.png

  1. 创建 HystrixCommand(用在依赖的服务返回单个操作结果的时候) 或 HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候) 对象。
  2. 命令执行。其中 HystrixComand 实现了下面前两种执行方式;而 HystrixObservableCommand 实现了后两种执行方式:execute():同步执行,从依赖的服务返回一个单一的结果对象, 或是在发生错误的时候抛出异常。queue():异步执行, 直接返回 一个Future对象, 其中包含了服务执行结束时要返回的单一结果对象。observe():返回 Observable 对象,它代表了操作的多个结果,它是一个 Hot Obserable(不论 “事件源” 是否有 “订阅者”,都会在创建后对事件进行发布,所以对于 Hot Observable 的每一个 “订阅者” 都有可能是从 “事件源” 的中途开始的,并可能只是看到了整个操作的局部过程)。toObservable(): 同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个Cold Observable(没有 “订阅者” 的时候并不会发布事件,而是进行等待,直到有 “订阅者” 之后才发布事件,所以对于 Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程)。
  3. 若当前命令的请求缓存功能是被启用的, 并且该命令缓存命中, 那么缓存的结果会立即以 Observable 对象的形式 返回。
  4. 检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到 fallback 处理逻辑(第 8 步);如果断路器是关闭的,检查是否有可用资源来执行命令(第 5 步)。
  5. 线程池/请求队列/信号量是否占满。如果命令依赖服务的专有线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满, 那么 Hystrix 也不会执行命令, 而是转接到 fallback 处理逻辑(第8步)。
  6. Hystrix 会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。HystrixCommand.run() :返回一个单一的结果,或者抛出异常。HystrixObservableCommand.construct(): 返回一个Observable 对象来发射多个结果,或通过 onError 发送错误通知。
  7. Hystrix会将 “成功”、”失败”、”拒绝”、”超时” 等信息报告给断路器, 而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行 “熔断/短路”。
  8. 当命令执行失败的时候, Hystrix 会进入 fallback 尝试回退处理, 我们通常也称该操作为 “服务降级”。而能够引起服务降级处理的情况有下面几种:第4步: 当前命令处于”熔断/短路”状态,断路器是打开的时候。第5步: 当前命令的线程池、 请求队列或 者信号量被占满的时候。第6步:HystrixObservableCommand.construct() 或 HystrixCommand.run() 抛出异常的时候。
  9. 当Hystrix命令执行成功之后, 它会将处理结果直接返回或是以Observable 的形式返回。

五、服务监控 hystrixDashboard

  • 对于Hystrix的状态,可以通过hystrixDashboard这个工具进行查看。
  • 首先,在配置类下添加一个组件
  @Bean
  public ServletRegistrationBean getServlet() {
    HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
    ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
    registrationBean.setLoadOnStartup(1);
    registrationBean.addUrlMappings("/hystrix.stream");
    registrationBean.setName("HystrixMetricsStreamServlet");
    return registrationBean;
  }
复制代码
  • 打开localhost:9001/hystrix监视,并且设置参数。
    • Delay:该参数用来控制服务器上轮询监控信息的延迟时间,默认为2000毫秒,可以通过配置该属性来降低客户端的网络和CPU消耗。
    • Title:该参数对应了头部标题Hystrix Stream之后的内容,默认会使用具体监控实例的URL,可以通过配置该信息来展示更合适的标题。

image.png
image.png
image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享