SpringCloud系列 (六)Hystrix熔断器

498 阅读14分钟

Hystrix熔断器

1、微服务中的雪崩效应

微服务中,⼀个请求可能需要多个微服务接⼝才能实现,会形成复杂的调⽤链路

首先,我们来看一个正常的微服务请求,用户微服务-> 自动投递微服务 -> 简历微服务。目前各个接口都是工作良好

但是,万一最后一个服务挂了或者响应特别慢呢?

搞得中间服务 也慢了,堆积了大量的请求

最终,就造成了服务雪崩的情况。这样的例子在各大电商网站的经历应该不会少

这里说两个概念扇入扇出

  • 扇入:代表着该微服务被调⽤的次数,扇⼊⼤,说明该模块复⽤性好
  • 扇出:该微服务调⽤其他微服务的个数,扇出⼤,说明业务逻辑复杂

扇⼊⼤是⼀个好事,扇出⼤不⼀定是好事

雪崩效应在微服务架构中,⼀个应⽤可能会有多个微服务组成,微服务之间的数据交互通过远程过程调⽤完成。这就带来⼀个问题,假设微服务A调⽤微服务B和微服务C,微服务B和微服务C⼜调⽤其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调⽤响应时间过⻓或者不可⽤,对微服务A的调⽤就会占⽤越来越多的系统资源,进⽽引起系统崩溃,所谓的“雪崩效应”。

如上图所示,最下游简历微服务响应时间过⻓,⼤量请求阻塞,⼤量线程不会释放,会导致服务器资源耗尽,最终导致上游服务甚⾄整个系统瘫痪。

2、雪崩效应解决⽅案

从可⽤性可靠性着想,为防⽌系统的整体缓慢甚⾄崩溃,采⽤的技术⼿段;下⾯,我们介绍三种技术⼿段应对微服务中的雪崩效应,这三种⼿段都是从系统可⽤性、可靠性⻆度出发,尽量防⽌系统整体缓慢甚⾄瘫痪。

  • 服务熔断

    熔断机制是应对雪崩效应的⼀种微服务链路保护机制。我们在各种场景下都会接触到熔断这两个字。⾼压电路中,如果某个地⽅的电压过⾼,熔断器就会熔断,对电路进⾏保护。股票交易中,如果股票指数过⾼,也会采⽤熔断机制,暂停股票的交易。同样,在微服务架构中,熔断机制也是起着类似的作⽤。当扇出链路的某个微服务不可⽤或者响应时间太⻓时,熔断该节点微服务的调⽤,进⾏服务的降级,快速返回错误的响应信息。当检测到该节点微服务调⽤响应正常后,恢复调⽤链路。

    注意:

    1)服务熔断重点在“”,切断对下游服务的调⽤

    2)服务熔断和服务降级往往是⼀起使⽤的,Hystrix就是这样。

  • 服务降级

    通俗讲就是整体资源不够⽤了,先将⼀些不关紧的服务停掉(调⽤我的时候,给你返回⼀个预留的值,也叫做兜底数据),待渡过难关⾼峰过去,再把那些服务打开。

    服务降级⼀般是从整体考虑,就是当某个服务熔断之后,服务器将不再被调⽤,此刻客户端可以⾃⼰准备⼀个本地的fallback回调,返回⼀个缺省值,这样做,虽然服务⽔平下降,但好⽍可⽤,⽐直接挂掉要强

  • 服务限流

    服务降级是当服务出问题或者影响到核⼼流程的性能时,暂时将服务屏蔽掉,待⾼峰或者问题解决后再打开;但是有些场景并不能⽤服务降级来解决,⽐如秒杀业务这样的核⼼功能,这个时候可以结合服务限流来限制这些场景的并发/请求量 限流措施也很多,⽐如

    • 限制总并发数(⽐如数据库连接池、线程池)
    • 限制瞬时并发数(如nginx限制瞬时并发连接数)
    • 限制时间窗⼝内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,
    • 限制每秒的平均速率)
    • 限制远程接⼝调⽤速率、限制MQ的消费速率等

3、Hystrix简介

[来⾃官⽹]Hystrix(豪猪----->刺),宣⾔“defend your app”是由Netflix开源的⼀个 延迟和容错库,⽤于隔离访问远程系统、服务或者第三⽅库,防⽌级联失败,从⽽ 提升系统的可⽤性与容错性。Hystrix主要通过以下⼏点实现延迟和容错。

  • 包裹请求:使⽤HystrixCommand包裹对依赖的调⽤逻辑。 ⾃动投递微服务⽅法(@HystrixCommand 添加Hystrix控制) ——调⽤简历微服务

  • 跳闸机制:当某服务的错误率超过⼀定的阈值时,Hystrix可以跳闸,停⽌请求该服务⼀段时间。

  • 资源隔离:Hystrix为每个依赖都维护了⼀个⼩型的线程池(舱壁模式)(或者信号量)。如果该线程池已满, 发往该依赖的请求就被⽴即拒绝,⽽不是排队等待,从⽽加速失败判定。

  • 监控:Hystrix可以近乎实时地监控运⾏指标和配置的变化,例如成功、失败、超时、以及被拒绝 的请求等。

  • 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执⾏回退逻辑。回退逻辑由开发⼈员 ⾃⾏提供,例如返回⼀个缺省值。

  • ⾃我修复:断路器打开⼀段时间后,会⾃动进⼊“半开”状态。

4、Hystrix熔断应⽤

简历微服务⻓时间没有响应,服务消费者—>⾃动投递微服务快速失败给⽤户

(1) 服务消费者⼯程引⼊Hystrix依赖坐标

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

(2) 服务消费者 增加 @EnableCircuitBreaker 注解

//@EnableHystrix ==> 开启断路器
@EnableCircuitBreaker  //==> 开启断路器,适用性更广
@EnableDiscoveryClient //==> 开启注册中心客户端通用型
@SpringBootApplication
public class AutodeliverApplicaton {
    public static void main(String[] args) {
        SpringApplication.run(AutodeliverApplicaton.class,args);
    }
    //使用Resttemple进行远程调用,先对注入该对象
    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplete(){
        return new RestTemplate();
    }
}

(3)配置hystix相关参数,HystrixCommandProperties类中,声明了各种hystix配置项, 这里使用的是超时配置项: execution.isolation.thread.timeoutInMilliseconds ,意思是,调用这个接口的时候,如果接口处理的时间大于2秒后,则会报错

  //HystrixCommand进行熔断控制
  @HystrixCommand(commandProperties ={ 
  	@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="2000")
  })
  @GetMapping("/checkStateTimeout/{userId}")
  public Integer findResumeOpenStateTimeOut(@PathVariable Long userId){
    String url="http://lagou-service-resume/resume/openstate/" + userId;
    Integer forObject = restTemplate.getForObject(url ,Integer.class);
    return forObject;
  }

(4)接着改造 一下简历微服务的接口,让接口返回时,睡个个10秒Thread.sleep(10000);

@GetMapping("/openstate/{userId}")
public Integer findDefaultResumeState(@PathVariable Long userId){
  try {
 	 Thread.sleep(10000);
  } catch (InterruptedException e) {
	  e.printStackTrace();
  }

  System.out.println("===》》》》》》》》》》》》我是8080");
  return port;
}

(5) 重新发布项目后,调用简历投递服务,会发现,在请求接口时,负载均衡到了8080项目,因为简历微服务的接口调用过程过长,导致发生崩溃了

调用正常的简历微服务8081时,没有问题

(6) 那么有些时候,我们并不想直接返回这种报错,我们想返回一个兜底服务,那么我们在 @HystrixCommand中,增加一个fallbackMethod方法,那么在超时的时候,就会返回一个默认的值

    @HystrixCommand(commandProperties ={ 
            @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")
    },fallbackMethod = "fallback") 
    @GetMapping("/checkStateFailback/{userId}")
    public Integer findResumeOpenStateFailBack(@PathVariable Long userId){
        String url="http://lagou-service-resume/resume/openstate/" + userId;
        Integer forObject = restTemplate.getForObject(url ,Integer.class);
        return forObject;
    }

    /**
     * 兜底数据
     * 方法形参和返回值和原始方法一致
     * @param userId
     * @return
     */
    public Integer fallback(Long userId){
        return -1;
    }

5、Hystrix舱壁模式(线程池隔离策略)

一般使用模式

如果不进⾏任何设置,所有熔断⽅法使⽤⼀个Hystrix线程池(10个线程),那么这样的话会导致问题,这个问题并不是扇出链路微服务不可⽤导致的,⽽是我们的线程机制导致的,如果⽅法A的请求把10个线程都⽤了,⽅法2请求处理的时候压根都没法去访问B,因为没有线程可⽤,并不是B服务不可⽤。

为了避免问题服务请求过多导致正常服务⽆法访问,Hystrix 不是采⽤增加线程数,⽽是单独的为每⼀个控制⽅法创建⼀个线程池的⽅式,这种模式叫做“舱壁模式",也是线程隔离的⼿段

我们可以通过jps命令,jstack命令,来查看究竟是不是只有10个线程池

(1)使用Postman工具,调用两个接口各10次

(2) 打开命令行,输入jps,查看当前的java进程

(2)查看一下AutodeliverApplicaton对应进程的信息,可以看到,这里的确有10个hystix相关的线程信息

舱壁模式的改造

(1) 在改成舱壁模式的时候,需要改动@HystrixCommand里面的参数配置

  • threadPoolKey 配置线程池的名称
  • threadPoolProperties 配置线程池的属性

这里针对findResumeOpenStateTimeOut方法设置的线程池数量为1,然后设置最大的等待队列数为20, 针对findResumeOpenStateFailBack方法设置的 线程池数量为2,设置最大的等待队列数通用为20

@HystrixCommand(
  threadPoolKey = "findResumeOpenStateTimeOut",
  threadPoolProperties = {
    @HystrixProperty(name = "coreSize",value = "1"),
    @HystrixProperty(name = "maxQueueSize",value = "20"),
  },

  commandProperties ={ 
  @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="2000")
})
@GetMapping("/checkStateTimeout/{userId}")
public Integer findResumeOpenStateTimeOut(@PathVariable Long userId){
}



@HystrixCommand(
threadPoolKey = "findResumeOpenStateFailBack",
threadPoolProperties = {
  @HystrixProperty(name = "coreSize",value = "2"),
  @HystrixProperty(name = "maxQueueSize",value = "20"),
},
commandProperties ={ 
	@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")
},fallbackMethod = "fallback")
@GetMapping("/checkStateFailback/{userId}")
public Integer findResumeOpenStateFailBack(@PathVariable Long userId){
}

(2) 同样针对两个接口请求10次

(3)输出的线程数量的总数量为3,其中findResumeOpenStateFailBack的线程数量为2

6、Hystrix⼯作流程与⾼级应⽤

总体介绍

1)当调⽤出现问题时,开启⼀个时间窗(10s)

2)在这个时间窗内,统计调⽤次数是否达到最⼩请求数?

  • 如果没有达到,则重置统计信息,回到第1步
  • 如果达到了,则统计失败的请求数占所有请求数的百分⽐,是否达到阈值?
  • 如果达到,则跳闸(不再请求对应服务)
  • 如果没有达到,则重置统计信息,回到第1步

3)如果跳闸,则会开启⼀个活动窗⼝(默认5s),每隔5s,Hystrix会让⼀个请求通过,到达那个问题服务,看 是否调⽤成功,如果成功,重置断路器回到第1步,如果失败,回到第3步

演示步骤

下面进行比较高级的用法及配置

(1) 项目开启健康检查状态监控,这个功能是SPringBoot的功能,基于springboot的健康检查观察跳闸状态

# springboot中暴露健康检查等断点接⼝
management:
  endpoints:
   web:
    exposure:
     include: "*"
  # 暴露健康接⼝的细节
  endpoint:
   health:
    show-details: always

访问健康检查接⼝:http://localhost:8090/actuator/health

hystrix正常⼯作状态 status:up

(2) 8秒钟内,请求次数达到2个,并且失败率在50%以上,就跳闸,跳闸后活动窗⼝设置为3s

注解形式

@HystrixCommand( 
commandProperties = {
  @HystrixProperty(name ="metrics.rollingStats.timeInMilliseconds",value = "8000"),
  @HystrixProperty(name ="circuitBreaker.requestVolumeThreshold",value = "2"),
  @HystrixProperty(name ="circuitBreaker.errorThresholdPercentage",value = "50"),
  @HystrixProperty(name ="circuitBreaker.sleepWindowInMilliseconds",value = "3000")
  }
)

配置形式

# 配置熔断策略:
hystrix:
 command:
  default:
   circuitBreaker:
   # 强制打开熔断器,如果该属性设置为true,强制断路器进⼊打开状态,将会拒绝所有的请求。 默认false关闭的
    forceOpen: false
     # 触发熔断错误⽐例阈值,默认值50%
     errorThresholdPercentage: 50
     # 熔断后休眠时⻓,默认值5秒
     sleepWindowInMilliseconds: 3000 
     # 熔断触发最⼩请求次数,默认值是20
     requestVolumeThreshold: 2 
    execution:
     isolation:
     thread:
     # 熔断超时设置,默认为1秒
     timeoutInMilliseconds: 2000

(3)使用Postman进行调用两个接口

我们不停的刷健康检查状态,发现熔断器在满足我们自己所设定的条件时,系统发生熔断

等了3秒过后,接口恢复可用,这里需要注意一点,在这段恢复期内,必须至少要对服务发起一次请求,判断远程服务是否正常,如果是正常的,则会重新恢复熔断开关,如果没有接口去调用,则这个状态值永远都不会发生变动

7、Hystrix Dashboard断路监控仪表盘

如果想对应用进行监控,进行相关统计等,可以使用断路监控仪表盘来进行相关监控

监控接口的相关准备

(1)确认父工程中,必须加入了actuator,只有开启actuator,才能对后续的应用进行监控

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

(2)在监控的项目中,暴露监控检查等接口

# springboot中暴露健康检查等断点接⼝
management:
  endpoints:
   web:
    exposure:
     include: "*"
  # 暴露健康接⼝的细节
  endpoint:
   health:
    show-details: always

(3)访问监控检查接口 http://localhost:8090//actuator/health

但是这里的监控接口只是单纯的显示服务是否可用,看不到多少细节

(4)在被监测的微服务中注册监控servlet,这样我们才能知道更多的细节

//在被监测的微服务中注册监控servlet
@Bean
public ServletRegistrationBean getServlet() {
   HystrixMetricsStreamServlet streamServlet = new  HystrixMetricsStreamServlet();
   ServletRegistrationBean registrationBean = new   ServletRegistrationBean(streamServlet);
   registrationBean.setLoadOnStartup(1);
   registrationBean.addUrlMappings("/actuator/hystrix.stream");
   registrationBean.setName("HystrixMetricsStreamServlet");
   return registrationBean;
}

(5)访问地址http://localhost:8090//actuator/hystrix.stream

被监控微服务发布之后,可以直接访问监控servlet,但是得到的数据并不直观,后期可以结合仪表盘更友好的展示

搭建监控仪表盘项目

(1)新建一个项目 lagou-cloud-hystrix-dashboard-9000,导入相关依赖

<dependencies>
    <!--hystrix-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <!--hystrix 仪表盘-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

(2)新建启动类,加入注解 @EnableHystrixDashboard 激活仪表盘

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication9000 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixDashboardApplication9000.class, args);
    }
}

(3)新建application.yml,加入配置

仪表盘项目需要注册到 注册中心上面去

server:
  port: 9000
spring:
  application:
    name: lagou-cloud-hystrix-dashboard

eureka:
  client:
    service-url:
      # 注册集群,就把多个Eureka地址 使用逗号链接起来即可
      defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
  instance:
    prefer-ip-address: true # 服务实例中显示IP,而不是显示主机名
    # ip-address: 192.168.56.1
    # 自定义实例名称
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

(4)启动项目,访问 http://localhost:9000/hystrix ,可以看到一个豪猪仪表盘

(5)我们把刚刚的投递简历微服务的监控流地址,输入到这里的监控地址中,点击 Monitor Stream

http://localhost:8090//actuator/hystrix.stream

仪表盘解析

实心圆

+ ⼤⼩:代表请求流量的⼤⼩,流量越⼤球越⼤
+ 颜⾊:代表请求处理的健康状态,从绿⾊到红⾊递减,绿⾊代表健康,红⾊就代表很不健康

刚开始请求调用时,为红色,请求流量很大

快结束调用时,为绿色

曲线波动图

记录了2分钟内该⽅法上流量的变化波动图,判断流量上升或者下降的趋势

数字

8、Hystrix Turbine聚合监控

思考:微服务架构下,⼀个微服务往往部署多个实例,如果每次只能查看单个实例的监控,就需要经常切换很不⽅便,在这样的场景下,我们可以使⽤ HystrixTurbine 进⾏聚合监控,它可以把相关微服务的监控数据聚合在⼀起,便于查看

(1)前期准备工作,我们先构建两个自动投递简历微服务 lagou-service-autodeliver-8091,配置和lagou-service-autodeliver一样

(2)新建聚合监控项目 lagou-cloud-hystrix-turbine-9001

  • 配置pom依赖
 <dependencies>
        <!--hystrix turbine聚合监控-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-turbine</artifactId>
        </dependency>
        <!--
        引⼊eureka客户端的两个原因
        1、⽼师说过,微服务架构下的服务都尽量注册到服务中⼼去,便于统⼀管
       理
        2、后续在当前turbine项⽬中我们需要配置turbine聚合的服务,⽐如,
       我们希望聚合
        lagou-service-autodeliver这个服务的各个实例的hystrix数据
       流,那随后
        我们就需要在application.yml⽂件中配置这个服务名,那么
       turbine获取服务下具体实例的数据流的
        时候需要ip和端⼝等实例信息,那么怎么根据服务名称获取到这些信息
       呢?
        当然可以从eureka服务注册中⼼获取
        -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
  • application.yml
server:
  port: 9001
spring:
  application:
    name: lagou-cloud-hystrix-turbine

eureka:
  client:
    service-url:
      # 注册集群,就把多个Eureka地址 使用逗号链接起来即可
      defaultZone: http://LagouCloudEurekaServerA:8761/eureka,http://LagouCloudEurekaServerB:8762/eureka
  instance:
    prefer-ip-address: true # 服务实例中显示IP,而不是显示主机名
    # 自定义实例名称
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}:@project.version@

#turbine配置
turbine:
  # appCofing配置需要聚合的服务名称,⽐如这⾥聚合⾃动投递微服务的hystrix监控数据
  # 如果要聚合多个微服务的监控数据,那么可以使⽤英⽂逗号拼接,⽐如 a,b,c
  appConfig: lagou-service-autodeliver
  clusterNameExpression: "'default'" # 集群默认名称
  • 新建启动类 HystrixTurbineApplication9001,标注 @EnableTurbine
@SpringBootApplication
@EnableDiscoveryClient
@EnableTurbine // 开启聚合功能
public class HystrixTurbineApplication9001 {
    public static void main(String[] args) {
        SpringApplication.run(HystrixTurbineApplication9001.class, args);
    }
}

把启动项目之后

(3) postman新增加两个新项目的URL地址,进行访问

(4)访问聚合项目URL地址http://localhost:9001/turbine.stream,可以看到有数据了

(5)把这数据地址,放入到http://localhost:9000/hystrix 进行监控

(6)可以看到仪表盘和之前单个的内容相似,只是在数字方面有些变动

9、Hystrix核⼼源码剖析

@EnableCircuitBreaker注解激活了熔断功能,那么该注解就是Hystrix源码追踪的⼊⼝

  • @EnableCircuitBreaker注解激活熔断器

  • 查看EnableCircuitBreakerImportSelector类

  • 继续关注⽗类 SpringFactoryImportSelector

org.springframework.core.io.support.SpringFactoriesLoader#loadFactoryNames

  • spring.factories⽂件内容如下

由上图所示,从spring.factories中,获取到了配置的名称,然后系统就会注入 org.springframework.cloud.netflix.hystrix.HystrixCircuitBreakerConfiguration

我们看下这个类,发现注入了切面, hystix就是靠切面干活的

com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect类型上标注 了@Aspect注解

分析下切入点的定义

重点分析下环绕方法

## ==================================================================
## com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect#methodsAnnotatedWithHystrixCommand
## ==================================================================
@Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()")
public Object methodsAnnotatedWithHystrixCommand(final ProceedingJoinPoint joinPoint) throws Throwable {

    ## ===> 获取原始目标方法
    Method method = getMethodFromTarget(joinPoint);
   
    MetaHolderFactory metaHolderFactory = META_HOLDER_FACTORY_MAP.get(HystrixPointcutType.of(method));
    
    ## ==> 获取封装的元数据
    MetaHolder metaHolder = metaHolderFactory.create(joinPoint);
    
    ## ==> 获取HystrixInvokable对象, GenericCommand对象
    HystrixInvokable invokable = HystrixCommandFactory.getInstance().create(metaHolder);
    ExecutionType executionType = metaHolder.isCollapserAnnotationPresent() ?
            metaHolder.getCollapserExecutionType() : metaHolder.getExecutionType();
 
}

GenericCommand中根据元数据信息重写了两个很核⼼的⽅法,⼀个是run⽅法封装了对原始⽬标⽅法的调⽤,另外⼀个是getFallBack⽅法它封装了对回退⽅法的调⽤

另外,在GenericCommand的上层类构造函数中会完成资源的初始化,⽐如线程池 GenericCommand —>AbstractHystrixCommand—>HystrixCommand—>AbstractCommand

代码示例