概述

分布式系统面临的问题

  • 复杂分布式体系结构中的应用程序 有数10个依赖关系,每个依赖关系在某些时候将不可避免地失败。
  • 假设一个服务需要走ABCD是个服务,如果D服务出现问题会导致服务雪崩的问题

服务雪崩

  • 扇出:多个服务之间调用的时候,假设微服务A调用微服务B和微服务C的时候,微服务B和微服务C又调用其他的微服务。

  • 雪崩:一个模块下的某个实例失败后,这时候这个模块依然还会接收请求,这个有问题的模块还会调用其他的模块,这就会发生级联故障。

  • 如果扇出的链路扇某个微服务调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓雪崩效应。

  • 对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还会导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致系统发生更多的级联故障。

  • 这些问题表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能影响整个应用程序或系统。

Hystrix 是什么

  • 官网资料:GitHub - Netflix/Hystrix
  • 一个用于处理分布式系统的延迟容错的开源库。Hystrix能够保证在系统出现问题(比如超时,异常等)的情况下,不会发生整体服务失败,避免级联故障,进而提高分布式系统的弹性
  • “断路器”本身是一种开关装置,当某个服务单元发生故障后,通过断路器的故障监控(类似熔断保险丝),向调用者返回一个符合预期、可处理的备选响应(FallBack) ,而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用的线程不会被长时间、不必要的占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
  • Hystrix 提供了以下功能
    • 服务降级
    • 服务熔断
    • 接近实时的监控

已停更😒

  • 官宣:停更进维,不在接受合并请求,不再发布新版本,被动修复bug

  • 推荐新项目使用 resilience4j (github.com),国外使用的多,国内使用阿里出的Sentinel。

重要概念

服务降级

  • 降级就是为了解决资源不足和访问量增加的矛盾
  • 比如服务器忙,调用后立刻返回友好的提示
  • 需要发生降级的情况
    • 程序运行异常
    • 超时
    • 服务熔断触发服务降级
    • 线程池/信号量也会导致服务降级

服务熔断

  • 在分布式系统中,我们往往需要依赖下游服务,不管是内部系统还是第三方服务,如果下游出现问题,我们还是盲目地去请求,如果失败了多次,还是傻傻的去请求,去等待。一是增加了整个链路的请求时间,第二,下游系统本身就出现了问题,不断的请求又把系统问题加重了,恢复困难。
  • 熔断模式可以防止应用程序不断地尝试可能超时和失败的服务,能达到应用程序执行而不必等待下游服务修正错误服务。
  • 类比保险丝达到了最大的服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好的提示。

服务限流

  • 通过对并发访问进行限速,从进入的流量上进行限制,达到保护系统的作用;

构建8001服务

  • 新建 Module,cloud-provider-hystrix-payment8001, 加入hystrix的依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    <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-server</artifactId>
    </dependency>
    <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.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>

  • 配置文件 application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    server:
    port: 8001
    spring:
    application:
    name: cloud-provider-hystrix-payment
    eureka:
    client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
    defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  • 启动类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableEurekaClient
    public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
    SpringApplication.run(PaymentHystrixMain8001.class, args);
    }
    }

业务类

  • service:提供超时和正常访问的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    @Service
    public class PaymentService {
    /**
    * 正常访问
    *
    * @param id
    * @return
    */
    public String paymentInfo_OK(Integer id) {
    return "线程池:" + Thread.currentThread().getName() + " paymentInfo_OK,id:" + id + "\t" + "O(∩_∩)O哈哈~";
    }

    /**
    * 超时访问
    *
    * @param id
    * @return
    */
    public String paymentInfo_TimeOut(Integer id) {
    int timeNumber = 3;
    try {
    // 暂停3秒钟
    TimeUnit.SECONDS.sleep(timeNumber);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    return "线程池:" + Thread.currentThread().getName() + " paymentInfo_TimeOut,id:" + id + "\t" +
    "O(∩_∩)O哈哈~ 耗时(秒)" + timeNumber;
    }
    }
  • controller: 提供正常访问和超时的接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    @RestController
    @Slf4j
    public class PaymentController {
    @Resource
    private PaymentService paymentService;

    @Value("${server.port}")
    private String servicePort;

    /**
    * 正常访问
    *
    * @param id
    * @return
    */
    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id) {
    String result = paymentService.paymentInfo_OK(id);
    log.info("*****result:" + result);
    return result;
    }

    /**
    * 超时访问
    *
    * @param id
    * @return
    */
    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
    String result = paymentService.paymentInfo_TimeOut(id);
    log.info("*****result:" + result);
    return result;

    }
    }

正常测试

高并发测试8001

  • 上述在非高并发的情况下还勉强能满足, ok接口7ms

  • 使用 Jmeter压测测试, 使用2000个并发访问timeout接口后,再次访问

  • 可以看到耗时变长了,20000个线程访问的是timeout接口,tomcat的默认工作线程数被打满了,没有多余的线程来分解压力和处理

构建80微服务

  • 创建cloud-comsumer-feign-hystrix-order80模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    <dependencies>
    <!--openfeign-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--eureka client-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <dependency>
    <groupId>com.atguigu.springcloud</groupId>
    <artifactId>cloud-api-common</artifactId>
    <version>${project.version}</version>
    </dependency>
    <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>
  • application.yml 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    server:
    port: 80
    eureka:
    client:
    register-with-eureka: false
    fetch-registry: true
    service-url:
    defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  • 主启动类

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableEurekaClient
    @EnableFeignClients
    public class OrderHystrixMain80 {
    public static void main(String[] args) {
    SpringApplication.run(OrderHystrixMain80.class, args);
    }
    }

业务类

  • 通过feign调用8001微服务的正常和超时接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Component
    @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
    public interface PaymentHystrixService {

    /**
    * 正常访问
    *
    * @param id
    * @return
    */
    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentInfo_OK(@PathVariable("id") Integer id);

    /**
    * 超时访问
    *
    * @param id
    * @return
    */
    @GetMapping("/payment/hystrix/timeout/{id}")
    String paymentInfo_TimeOut(@PathVariable("id") Integer id);
    }
  • controller提供访问接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class OrderController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("test")
    public String test() {
    return "test";
    }

    @GetMapping("info/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
    String res = paymentHystrixService.paymentInfo_OK(id);
    System.out.println(res);
    return res;
    };


    @GetMapping("info/timeout/{id}")
    public String paymentInfo_ERROR(@PathVariable("id") Integer id){
    return paymentHystrixService.paymentInfo_TimeOut(id);
    }
    }

正常测试

高并发测试

  • 使用jmeter,用30000个线程访问8001微服务

  • 使用80微服务调用8001的OK服务 http://localhost/info/ok/2

  • 可以看到有二种情况

    • 一种是在转圈,比较慢

    • 出现error页面,报超时错误

  • 故障现象

    • 8001同一层次的其他接口被困死,因为tomcat线程池里面的工作线程已经被挤占完毕
    • 80此时调用8001,客户端访问响应缓慢,转圈圈

问题解决

思路

  • 超时导致服务器变慢(转圈) , 超时不再等待
  • 出错(宕机或程序运行出错),出错要有兜底

解决

  • 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级
  • 对方服务(8001)down了,调用者(80)不能一直卡死等待,必须有服务降级
  • 对方服务(8001)OK,调用者(80)自己有故障或自己的超时等待时间小于服务提供者

8001服务降级

  • 设置服务提供者(8001)自身调用超时时间的峰值,在峰值内可以正常运行,超过了需要有兜底的方法处理进行服务降级(fallback)

启用Hystrix

  • 在主启动类上添加 @EnableCircuitBreaker, 也可使用@EnableHystrix,EnableHystrix中包含了EnableCircuitBreaker注解

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableCircuitBreaker //激活hystrix
    public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
    SpringApplication.run(PaymentHystrixMain8001.class, args);
    }
    }

超时启用降级

  • 使用使用@HystrixCommand(fallbackMethod = “getFallback”),

  • fallbackMethod 是降级服务时使用的方法,这个方法需要返回值一样,参数一样

  • 设置接收3秒钟,方法运行超过3秒钟,出现超时异常走fallback指定的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @HystrixCommand(fallbackMethod = "getPaymentInfo_TimeOutHandler",commandProperties = {
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
    })
    public String paymentInfo_TimeOut(Integer id) {
    int timeNumber = 4;
    try {
    // 暂停3秒钟
    TimeUnit.SECONDS.sleep(timeNumber);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    return "线程池:" + Thread.currentThread().getName() + " paymentInfo_TimeOut,id:" + id + "\t" +
    "O(∩_∩)O哈哈~ 耗时(秒)" + timeNumber;
    }



    public String getPaymentInfo_TimeOutHandler(Integer id) {
    return Thread.currentThread().getName()+"-----------paymentInfo_TimeOut, 请稍后重试。--------"+id;
    }
  • 重新启动8001服务后访问 http://localhost:8001/payment/hystrix/timeout/1 ,可以看到3s后就有返回结果。

80服务降级

  • 80订单微服务,也可以更好的保护自己,进行客户端端降级保护

  • 我们自己配置过的热部署方式对java代码的改动明显,但对@HystrixCommand内属性的修改建议重启微服务

项目准备

  • 加入Hystrix的依赖

    1
    2
    3
    4
    5
    <!--hystrix-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
  • yaml配置

    1
    2
    3
    4
    5
    6
    7
    8
    server:
    port: 80
    eureka:
    client:
    register-with-eureka: false
    fetch-registry: true
    service-url:
    defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  • 主启动类添加@EnableHystrix

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @SpringBootApplication
    @EnableEurekaClient
    @EnableFeignClients
    @EnableHystrix
    public class OrderHystrixMain80 {
    public static void main(String[] args) {
    SpringApplication.run(OrderHystrixMain80.class, args);
    }
    }

超时服务降级

  • 设置超时时间,调用者(80)不能一直卡死等待

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @GetMapping("info/timeout/{id}")
    @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
    })
    public String paymentInfo_ERROR(@PathVariable("id") Integer id) {
    //int age = 10/0;
    return paymentHystrixService.paymentInfo_TimeOut(id);
    }

    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
    return "我是消费者80,对方支付系统繁忙请10秒种后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
    }

服务down降级

  • 可以自己测试异常导致的服务降级

  • 先启动eurek,然后启动8001微服务,80微服务,然后正常访问后,关闭8001微服务,再次用80调用者访问

  • 此时服务端8001已经downl ,但是我们做了服务降级处理,让客户端在服务端不可用时也会获得提示信息而不会挂起耗死服务器

优化

  • 配置了服务降级后,可以避免了错误故障,通过上面的写法,我们发现我们需要对每一个业务方法配置一个fallbakc方法,随着业务的接口增加,兜底方法也会增加,出现代码膨胀。

解决代码膨胀

  • (×)为每个方法1:1配置服务降级方法

  • (√)除了个别重要和兴业务外,其他方法通过全局的fallback方法统一处理

  • @DefaultProperties(defaultFallback="")指定全局的降级方法,如果@HystrixCommand没有指定fallbackMethod就走统一的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @RestController
    @DefaultProperties(defaultFallback = "globalFallbackMethod")
    public class OrderController {
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("info/timeout/{id}")
    - @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
    - @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
    - })
    + @HystrixCommand
    public String paymentInfo_ERROR(@PathVariable("id") Integer id) {
    //int age = 10/0;
    return paymentHystrixService.paymentInfo_TimeOut(id);
    }

    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
    return "我是消费者80,对方支付系统繁忙请10秒种后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
    }

    + public String globalFallbackMethod() {
    + return "Global异常处理信息,请稍后重试.o(╥﹏╥)o";
    + }
    }

业务代码分离

  • 上面的写法虽然减少了代码的膨胀,但是降级方法和业务逻辑写在一起。

  • 80调用者controller层调用的service接口,用了@FeignClient注解,FeignClient是服务调用,把客户端与服务器端注入eureka,来调用我们的服务器端的接口,fallback的是继承HystrixService 类为其设计的fallback处理逻辑,我们只需要为Feign客户端定义的接口,添加一个服务降级处理的实现类即可实现解耦。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Component   //将fallback接口添加到容器中
    public class PaymentFallbackHystrixService implements PaymentHystrixService {
    @Override
    public String paymentInfo_OK(Integer id) {
    return "-------PaymentFallbackService fall back-paymentInfo_OK,o(╥﹏╥)o";
    }

    @Override
    public String paymentInfo_TimeOut(Integer id) {
    return "-------PaymentFallbackService fall back-paymentInfo_TimeOut,o(╥﹏╥)o";
    }
    }
  • 在feign中指定fallback

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    @Component
    - @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
    + @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackHystrixService.class)
    public interface PaymentHystrixService {

    /**
    * 正常访问
    *
    * @param id
    * @return
    */
    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentInfo_OK(@PathVariable("id") Integer id);

    /**
    * 超时访问
    *
    * @param id
    * @return
    */
    @GetMapping("/payment/hystrix/timeout/{id}")
    String paymentInfo_TimeOut(@PathVariable("id") Integer id);
    }
  • 配置yaml

    1
    2
    3
    4
    # 用于服务降级,在@FeignClient注解中加入fallback属性值
    feign:
    hystrix:
    enabled: true

服务降级优先级

  • 通过上面的示例,可以知道有3种方式配置服务降级方法,fallback的位置定义如下
    1. 控制类HystirxController上DefaultProperties注解默认fallback
    2. 控制层执行方法SingleHystirxController上HystrixCommand注解单独设置的fallback
    3. 调用service方法@FeignClient(fallback = FallbackService .class)
  • 如果都定义了,fallback优先显示:3>2>1

服务熔断

熔断机制

  • 熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息
  • 当检测到该节点微服务调用响应正常后,恢复调用链路。
  • 在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定间值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。
  • 参考 https://martinfowler.com/bliki/CircuitBreaker.html
  • 翻译:【译文】熔断器-CircuitBreaker)

实操

  • 配置文档:Configuration · Netflix/Hystrix Wiki · GitHub

  • Circuit Breaker

    1. circuitBreaker.enabled
    2. circuitBreaker.requestVolumeThreshold
    3. circuitBreaker.sleepWindowInMilliseconds
    4. circuitBreaker.errorThresholdPercentage
    5. circuitBreaker.forceOpen
    6. circuitBreaker.forceClosed
  • 对8001微服务的service设置短路器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package pers.fulsun.sc.service;

    import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
    import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
    import org.springframework.stereotype.Service;
    import org.springframework.web.bind.annotation.PathVariable;

    import java.util.UUID;
    import java.util.concurrent.TimeUnit;

    @Service
    public class PaymentService {
    //---服务的熔断
    @HystrixCommand(
    fallbackMethod = "paymentCircuitBreaker_fallback", 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"),//失败率达到多少后跳闸
    }
    )
    public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
    if (id < 0) {
    throw new RuntimeException("******id不能为负数");
    }
    String simpleUUID = UUID.randomUUID().toString().replace("-", "");
    return Thread.currentThread().getName() + "\t" + "成功调用,流水号是:" + simpleUUID;
    }

    public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
    return "id不能为负数,请稍后再试............" + id;
    }
    }
  • controller提供接口

    1
    2
    3
    4
    5
    @GetMapping("info/circuit/{id}") //circuit 电路
    public String paymentCircuitBreaker(@PathVariable("id") Integer id){
    String res = paymentService.paymentCircuitBreaker(id);
    return res;
    }
  • 测试,多次访问异常接口 http://localhost:8001/info/circuit/-11 , 然后访问正常的接口,发现就算是正确的访问也不能进行了

熔断原理

  • This simple circuit breaker avoids making the protected call when the circuit is open,but would need an external intervention to reset it when things are well again. This is a reasonable approach with electrical circuit breakers in buildings, but for software circuit breakers we can have the breaker itself detect if the underlying calls are working again. We can implement this self-resetting behavior by trying the protected
    call again after a suitable interval, and resetting the breaker should it succeed.

  • 这种简单的熔断器避免在电路打开时发出受保护的调用,但在事情再次良好时需要外部干预来重置它。这是一个合理的方法,用于建筑物中的电气熔断器,但对于软件熔断器,我们可以让熔断器自己检测底层调用是否再次工作。我们可以通过在适当的间隔后再次尝试受保护的调用来实现这种自我重置行为,如果它成功的话,重新设置熔断器。

  • 熔断打开:请求不再调用当前服务,内部设置一般为MTTR(平均故障处理时间),当打开长达导所设时钟则进入半熔断状态

  • 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断

  • 熔断关闭:熔断关闭后不会对服务进行熔断

流程图

熔断器逻辑

  1. 假设电路的音量达到一定阈值 ()…HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()
  2. 假设错误百分比超过阈值错误百分比 ()…HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()
  3. 然后断路器从CLOSED过渡到OPEN
  4. 当断路器是OPEN的,会短路对该断路器进行的所有请求。
  5. 经过一段时间后(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()),下一个请求是允许通过(HALF-OPEN)。如果请求失败,断路器在睡眠窗口期间返回OPEN状态。如果请求成功,断路器将转换为CLOSED.逻辑1再次接管。

开启断路器条件

  • 涉及到断路器的三个重要参数:快照时间窗、请求总数阀值、错误百分比阀值。

    • 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
    • 请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,
      即使所有的请求都超时或其他原因失败,断路器都不会打开。
    • 错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过
      50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。
  • 到达以下阈值,断路器将会开启,所有请求不会转发。

    • 当满足一定的阈值的时候(默认10秒钟超过20个请求次数)
    • 当失败率达到一定的时候(默认10秒内超过50%的请求次数)
  • 一段时间之后(默认5秒),这个时候断路器是半开状态,会让其他一个请求进行转发.如果成功,断路器会关闭,若失败,继续开启.重复4和5操作。

详细配置

  • 断路器配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    @HystrixCommand(fallbackMethod = "str_fallbackMethod",
    groupKey = "strGroupCommand",
    commandKey = "strCommand",
    threadPoolKey = "strThreadPool",

    commandProperties = {
    // 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离
    @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
    // 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数)
    @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
    // 配置命令执行的超时时间
    @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"),
    // 是否启用超时时间
    @HystrixProperty(name = "execution.timeout.enabled", value = "true"),
    // 执行超时的时候是否中断
    @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"),
    // 执行被取消的时候是否中断
    @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"),
    // 允许回调方法执行的最大并发数
    @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"),
    // 服务降级是否启用,是否执行回调函数
    @HystrixProperty(name = "fallback.enabled", value = "true"),
    // 是否启用断路器
    @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
    // 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,
    // 如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。
    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
    // 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过
    // circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50,
    // 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。
    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
    // 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后,
    // 会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态,
    // 如果成功就设置为 "关闭" 状态。
    @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"),
    // 断路器强制打开
    @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
    // 断路器强制关闭
    @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),
    // 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间
    @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"),
    // 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据
    // 设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。
    // 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常
    @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
    // 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。
    @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
    // 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
    @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
    // 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。
    @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
    // 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,
    // 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行,
    // 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
    @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
    // 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。
    @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"),
    // 是否开启请求缓存
    @HystrixProperty(name = "requestCache.enabled", value = "true"),
    // HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中
    @HystrixProperty(name = "requestLog.enabled", value = "true"),
    },
    threadPoolProperties = {
    // 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量
    @HystrixProperty(name = "coreSize", value = "10"),
    // 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列,
    // 否则将使用 LinkedBlockingQueue 实现的队列。
    @HystrixProperty(name = "maxQueueSize", value = "-1"),
    // 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。
    // 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue
    // 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。
    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"),
    }
    )
    public String strConsumer() {
    return "hello word";
    }
    public String str_fallbackMethod()
    {
    return "fall back str_fallbackMethod";
    }

监控调用

  • 除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard), Hystrix会持续地记录所有通过Hystrix发
    起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。
  • Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成
    可视化界面。

新建hystrix-dashboard

  • 新建module: cloud-consumer-hystrix-dashboard9001

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <dependencies>
    <!--hystrix dashboard-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    </dependency>
    <!--监控-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </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>
    <!--热部署-->
    <!-- <dependency>-->
    <!-- <groupId>org.springframework.boot</groupId>-->
    <!-- <artifactId>spring-boot-devtools</artifactId>-->
    <!-- <scope>runtime</scope>-->
    <!-- <optional>true</optional>-->
    <!-- </dependency>-->
    </dependencies>

  • 配置端口

    1
    2
    server:
    port: 9001
  • 启动类添加注解 @EnableHystrixDashboard

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableHystrixDashboard
    public class HystrixDashboardMain9001 {
    public static void main(String[] args) {
    SpringApplication.run(HystrixDashboardMain9001.class);
    }
    }

    监控

    • 所有Provider微服务提供类都需要添加监控依赖

      1
      2
      3
      4
      5
      <!--监控-->
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
    • 启动9001项目,访问 http://localhost:9001/hystrix

监控8001

  • 注意:SpringCloud2.0版本,不再默认支持hystrix.stream路径输出,需要在主启动MainAppHystrix8001中指定监控路径,

  • 这里我们监控8001,在8001的主启动进行配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableCircuitBreaker //激活hystrix
    public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
    SpringApplication.run(PaymentHystrixMain8001.class,args);
    }

    /**
    * 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
    * ServletRegistrationBean因为springboot的默认路径不是/hystrix.stream
    * 只要在自己的项目里配置下面的Servlet就可以了
    * @return
    */
    @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;
    }
    }
  • 重启8001后,在9001监控服务输入监控地址 http://localhost:8001/hystrix.stream 到此单个应用的熔断监控已经完成。

  • 第一次访问是没有数据的,可以访问带有@HystrixCommand注解的接口 http://localhost:8001/info/circuit/11http://localhost:8001/info/circuit/-11 来测试

怎么看

  • 7色

  • 实心圆

    • 实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康度从绿色<黄色<橙色<红色递减。
    • 它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。
    • 所以通过该实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压力实例。
  • 曲线:

    • 用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。

断路器的监视器

  • 整体解释

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    折线图代表了指定方法过去两分钟的流量,简要显示了改方法的繁忙情况。
    折线图的背景是一个大小和颜色会出现波动的圆圈,圆圈的大小表示当前的流量,圆圈越大,流量越大。圆圈的颜色表示它的建库状况:绿色表示建库的断路器,黄色表示偶尔发生故障的断路器,红色表示故障断路器。
    在监视器的右上角,以3列的形式显示各种计数器。在最左边的一列中,从上到下
    第一个数字(绿色)表示当前成功调用的数量;
    第二个数字(蓝色)表示短路请求的数量;
    最后一个数字(蓝绿色)表示错误请求的数量。
    中间一列显示超时请求的数量(黄色)、
    线程池拒绝的数量(紫色)
    失败请求的数量(红色)。
    第三列显示过去10s内错误的百分率。
    计数器下面有两个数字,代表每秒主机和集群的请求数量。
    这两个请求率下面是断路器的状态。
    Median和Mean显示了延迟的中位数和平均值。90th、99th、99.5表示百分位的延迟。

线程池的监视器说明

  • 与断路器的监视器类似,每个线程池监视器在左上角都包含一个圆圈,圆圈大小和颜色代表了线程池的活跃状态以及健康状况。与断路器的监视器不同的是,线程池的监视器没有显示过去几分钟线程池活跃的折线图。

  • 右上角显示线程池的名称,其下方是线程池中线程每秒处理请求的数量。

  • 线程池监视器的左下角显示如下信息

    • 活跃线程:当前活跃线程的数量
    • 排队线程:当前有多少线程在排队。默认情况下,队列功能是禁用的,所以这个值始终为0.
    • 线程池的大小:线程池中有多少线程
  • 右下角显示如下信息

    • 最大活跃线程:在当前的采样周期中,活跃线程的最大数量
    • 执行次数:线程池中的线程被调用执行Hystrix命令的次数
    • 线程队列大小:线程池队列的大小。线程队列功能默认是禁用的,所以这个值没有什么意义

Turbine

  • 在复杂的分布式系统中,相同服务的节点经常需要部署上百甚至上千个,很多时候,运维人员希望能够把相同服务的节点状态以一个整体集群的形式展现出来,这样可以更好的把握整个系统的状态。 为此,Netflix提供了一个开源项目(Turbine)来提供把多个hystrix.stream的内容聚合为一个数据源供Dashboard展示。

构建项目

  • pom.xml

    1
    2
    3
    4
    5
    6
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
    </dependency>
    </dependencies>
  • 配置文件

    • turbine.appConfig :配置Eureka中的serviceId列表,表明监控哪些服务

    • turbine.aggregator.clusterConfig :指定聚合哪些集群,多个使用”,”分割,默认为default。可使用 http://ip:port/turbine.stream?cluster={clusterConfig之一}访问

    • turbine.clusterNameExpression : 指定集群名称

      • 默认表达式appName;此时turbine.aggregator.clusterConfig需要配置想要监控的应用名称;

      • 当clusterNameExpression: default时,turbine.aggregator.clusterConfig可以不写,因为默认就是default;

      • 当clusterNameExpression: metadata[‘cluster’]时,假设想要监控的应用配置了eureka.instance.metadata-map.cluster: ABC,则需要配置,同时turbine.aggregator.clusterConfig: ABC

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      server:
      port: 9002
      spring:
      application:
      name: hystrix-dashboard-turbine
      eureka:
      instance:
      hostname: localhost
      prefer-ip-address: true #注册时使用ip而不是主机名
      client:
      register-with-eureka: true
      fetch-registry: true
      service-url:
      defaultZone: http://127.0.0.1:7001/eureka
      turbine:
      app-config: cloud-provider-hystrix-payment,cloud-comsumer-order
      cluster-name-expression: new String("default")
      combine-host-port: true
      instanceUrlSuffix: /hystrix.stream
  • 启动类添加@EnableTurbine,激活对Turbine的支持

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableTurbine
    public class TerbineMain9002 {
    public static void main(String[] args) {
    SpringApplication.run(TerbineMain9002.class);

    }
    }
  • 到此Turbine(hystrix-dashboard-turbine)配置完成

测试