4.SpringCloud学习-Hystrix熔断器

749 阅读9分钟

1.背景

今天我们学习SpringCloud的Hystrix熔断器

我们今天继续使用之前eureka-server作为服务注册中心

使用Springboot和springcloud的版本如下

  • springboot版本:2.3.5-release
  • springcloud版本:Hoxton.SR9

2.Hystrix 是什么

在分布式环境中,许多服务依赖项中的一些必然会失败。Hystrix是一个库,通过添加延迟容忍和容错逻辑,帮助你控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点、停止级联失败和提供回退选项来实现这一点,所有这些都可以提高系统的整体弹性。

3.Hystrix为了什么

Hystrix被设计的目标是:

  1. 对通过第三方客户端库访问的依赖项(通常是通过网络)的延迟和故障进行保护和控制。
  2. 在复杂的分布式系统中阻止级联故障。
  3. 快速失败,快速恢复。
  4. 回退,尽可能优雅地降级。
  5. 启用近实时监控、警报和操作控制。

4..Hystrix有哪些功能

通过hystrix可以解决雪崩效应问题,它提供了资源隔离、降级机制、融断、缓存等功能。

  1. 资源隔离:包括线程池隔离和信号量隔离,限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。
  2. 降级机制:超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。
  3. 融断:当失败率达到阀值自动触发降级(如因网络故障/超时造成的失败率高),熔断器触发的快速失败会进行快速恢复。
  4. 缓存:返回结果缓存,后续请求可以直接走缓存。
  5. 请求合并:可以实现将一段时间内的请求(一般是对同一个接口的请求)合并,然后只对服务提供者发送一次请求。

服务熔断和降级

  • 服务熔断:一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施。简单的说服务熔断是条件,服务熔断是配置于服务端的防范机制

  • 服务降级:简单的数据服务降级是服务熔断的解决问题的手段之一

资源隔离:

  1. 线程隔离

Hystrix在用户请求和服务之间加入了线程池。 Hystrix为每个依赖调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。线程数是可以被设定的。 原理:用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,则会进行降级处理,用户的请求不会被阻塞,至少可以看到一个执行结果(例如返回友好的提示信息),而不是无休止的等待或者看到系统崩溃

  1. 信号量隔离

在该模式下,接收请求和执行下游依赖在同一个线程内完成,不存在线程上下文切换所带来的性能开销,所以大部分场景应该选择信号量模式,但是在下面这种情况下,信号量模式并非是一个好的选择

对比

对比线程池隔离信号量隔离
是否支持熔断支持,当线程池到达MaxSize后,再请求会触发fallback接口进行熔断支持,当信号量达到maxConcurrentRequest后,再请求会触发fallback
是否支持超时支持,可直接返回不支持,如果阻塞,只能通过调用协议
隔离原理每个服务单独用线程池通过信号量的计数器
是否支持异步调用可以是异步,也可以是同步。看调用的方法同步调用,不支持异步
资源消耗大,大量线程的上下文切换,容易造成机器负载高小,只是个计数器
是否支持超时row 2 col 2row 1 col 2

5熔断器原理(CircuitBreaker)

熔断器的三种状态

  1. CLOSED:熔断器关闭状态,正常请求流程
  2. OPEN:熔断器开状态,请求直接熔断降级处理
  3. HALF-OPEN:熔断器半开状态,在一个熔断时间窗口结束后放一个请求

熔断器配置参数-重要

Circuit Breaker主要包括如下6个参数:

1、circuitBreaker.enabled

是否启用熔断器,默认是TRUE。 2 、circuitBreaker.forceOpen

熔断器强制打开,始终保持打开状态,不关注熔断开关的实际状态。默认值FLASE。 3、circuitBreaker.forceClosed 熔断器强制关闭,始终保持关闭状态,不关注熔断开关的实际状态。默认值FLASE。

4、circuitBreaker.errorThresholdPercentage 错误率,默认值50%,例如一段时间(10s)内有100个请求,其中有54个超时或者异常,那么这段时间内的错误率是54%,大于了默认值50%,这种情况下会触发熔断器打开。

5、circuitBreaker.requestVolumeThreshold

默认值20。含义是一段时间内至少有20个请求才进行errorThresholdPercentage计算。比如一段时间了有19个请求,且这些请求全部失败了,错误率是100%,但熔断器不会打开,总请求数不满足20。

6、circuitBreaker.sleepWindowInMilliseconds

半开状态试探睡眠时间,默认值5000ms。如:当熔断器开启5000ms之后,会尝试放过去一部分流量进行试探,确定依赖服务是否恢复

熔断器流程分析

熔断器工作的详细过程如下:

第一步,调用allowRequest()判断是否允许将请求提交到线程池

如果熔断器强制打开,circuitBreaker.forceOpen为true,不允许放行,返回。 如果熔断器强制关闭,circuitBreaker.forceClosed为true,允许放行。此外不必关注熔断器实际状态,也就是说熔断器仍然会维护统计数据和开关状态,只是不生效而已。

第二步,调用isOpen()判断熔断器开关是否打开

如果熔断器开关打开,进入第三步,否则继续; 如果一个周期内总的请求数小于circuitBreaker.requestVolumeThreshold的值,允许请求放行,否则继续; 如果一个周期内错误率小于circuitBreaker.errorThresholdPercentage的值,允许请求放行。否则,打开熔断器开关,进入第三步。

第三步,调用allowSingleTest()判断是否允许单个请求通行,检查依赖服务是否恢复

如果熔断器打开,且距离熔断器打开的时间或上一次试探请求放行的时间超过circuitBreaker.sleepWindowInMilliseconds的值时,熔断器器进入半开状态,允许放行一个试探请求;否则,不允许放行。 此外,为了提供决策依据,每个熔断器默认维护了10个bucket,每秒一个bucket,当新的bucket被创建时,最旧的bucket会被抛弃。其中每个blucket维护了请求成功、失败、超时、拒绝的计数器,Hystrix负责收集并统计这些计数器。

6.项目搭建

6.1 消费者

添加依赖

<dependency>
       <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

添加注解

@EnableCircuitBreaker

@EnableCircuitBreaker

增加配置类

@FeignClient(name ="ms-feign-producer",path = "/api/user",configuration = Config.class,fallback = UserServiceImpl.class)
public interface UserService {

    @GetMapping("/{id}")
    public String selectUser(@PathVariable("id") String id);
}

@Component
public class UserServiceImpl  implements UserService{
    @Override
    public String selectUser(String id) {
        return "我是熔断机制";
    }
}

配置文件

spring:
  application:
    name: ms-feign-consumer
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka
    register-with-eureka: true
  instance:
    prefer-ip-address: true
    #appname: ${spring.application.name}
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
    hostname: ${spring.cloud.client.ip-address}
server:
  port: 8081
  
hystrix:
  command:
    default:
      circuitBreaker:
        requestVolumeThreshold: 5 #设置时间窗口的请求量至少为5个
        sleepWindowInMilliseconds: 5000
        errorThresholdPercentage: 50
      metrics:
        rollingStats:
          timeInMilliseconds: 5000   # 时间窗口

使用

@RequestMapping("/api/comsumer/user")
@RestController
public class UserController {

    @Autowired
    UserService userService;

    @GetMapping("/{id}")
    public String selectUser(@PathVariable("id") String id){
        return userService.selectUser(id);
    }
}

6.2 生产者

添加依赖

<dependency>
       <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

修改配置文件

spring:
  application:
    name: ms-feign-producer
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka
    register-with-eureka: true
  instance:
    prefer-ip-address: true
    #appname: ${spring.application.name}
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
    hostname: ${spring.cloud.client.ip-address}
server:
  port: 8082

提供服务

@RequestMapping("/api/user")
@RestController
public class UserController {
    @Value("${server.port}")
    Integer port;
    @GetMapping("/{id}")
    public String selectUser(@PathVariable("id") String id){
        if ("1".equals(id)) {
           int i =  1/0;
        }
        User user = new User();
        user.setId(id);
        user.setName("wangyunqi");
        user.setPort(port);
        return user.toString();
    }
}

测试

"hystrix": {
            "status": "CIRCUIT_OPEN",
            "details": {
                "openCircuitBreakers": [
                    "ms-feign-producer::UserService#selectUser(String)"
                ]
            }
        },
        "ping": {
            "status": "UP"
        },
        "refreshScope": {
            "status": "UP"
        }
  • 2:等待sleepWindowInMilliseconds一个时间后,方位id=2,发现正常返回数据
User{id='2', name='111', age=0, port=8082}

通过端口认真熔断器: http://192.168.1.119:8081/actuator/health

        "hystrix": {
            "status": "UP"
        },
        "ping": {
            "status": "UP"
        },
        "refreshScope": {
            "status": "UP"
        }
    }

7.熔断器工作流程

image

流程说明:

  • 1:每次调用创建一个新的HystrixCommand,把依赖调用封装在run()方法中。
  • 2:执行execute()/queue做同步或异步调用。
  • 3:判断熔断器(circuit-breaker)是否打开,如果打开跳到步骤8,进行降级策略,如果关闭进入步骤。
  • 4:判断线程池/队列/信号量是否跑满,如果跑满进入降级步骤8,否则继续后续步骤。
  • 5:调用HystrixCommand的run方法。运行依赖逻辑
  • 5a:依赖逻辑调用超时,进入步骤8。
  • 6:判断逻辑是否调用成功
  • 6a:返回成功调用结果
  • 6b:调用出错,进入步骤8。
  • 7:计算熔断器状态,所有的运行状态(成功, 失败, 拒绝,超时)上报给熔断器,用于统计从而判断熔断器状态。
  • 8:getFallback()降级逻辑。
  • 以下四种情况将触发getFallback调用:
  • (1):run()方法抛出非HystrixBadRequestException异常
  • (2):run()方法调用超时
  • (3):熔断器开启拦截调用
  • (4):线程池/队列/信号量是否跑满
  • 8a:没有实现getFallback的Command将直接抛出异常
  • 8b:fallback降级逻辑调用成功直接返回
  • 8c:降级逻辑调用失败抛出异常
  • 9:返回执行成功结果