SpringBoot + SpringCloud Hystrix 实现服务熔断

在分布式系统中,每个服务都可能会调用很多其他服务,被调用的那些服务就是依赖服务,有的时候某些依赖服务出现故障也是很常见的。

什么是Hystrix

Hystrix是Netflix公司开源的一个项目,它提供了熔断器功能,能够解决分布式系统中出现联动故障,Hystrix是通过隔离服务的访问点阻止故障,并提供故障解决方案,从而提高分布式系统弹性。

Hystrix可以让我们在分布式系统中对服务间的调用进行控制,加入一些调用延迟或者依赖故障的容错机制。Hystrix通过将依赖服务进行资源隔离,进而阻止某个依赖服务出现故障时在整个系统所有的依赖服务调用中进行蔓延;同时Hystrix 还提供故障时的 fallback 降级机制。

总而言之,Hystrix 通过这些方法帮助我们提升分布式系统的可用性和稳定性。

Hystrix解决了什么问题

在分布式系统中,可能有几十个服务相互依赖。这些服务由于某些原因导致不可用。如果系统不隔离不可用的服务,则可能会导致整个系统不可用。

在高并发情况下,单个服务的延迟会导致整个请求都处于延迟状态,可能在几秒钟就使整个线程处于负载饱和状态。

某个服务的单点故障会导致用户的请求处于阻塞状态,最终的结果就是整个服务的线程资源消耗殆尽。由于服务的依赖性,会导致依赖该故障服务的其他服务也处于线程阻塞状态,最终导致这些依赖服务的线程资源消耗殆尽,直到不可用,从而导致整个服务系统不可用,这就是雪崩效应。

为了防止雪崩效应,因而产生了熔断器模型。Hystrix是业界表现非常好的一个熔断器模型实现的开源组件,是SpringCloud组件不可缺少的一部分。

Hystrix设计原则

  • 防止单个服务故障耗尽整个服务的Servlet容器(Tomcat/Jetty)的线程资源。
  • 快速失败机制,如果某个服务出现故障,则调用该服务的请求迅速失败,而不是线程等待。
  • 提供回退方案(fallback),在请求发生故障时,提供设定好的回退方案。
  • 使用熔断机制,防止故障扩散到其他服务。
  • 提供熔断器的监控组件Hystrix Dashboard,近实时的监控,报警以及运维操作。

Hystrix工作服原理


首先,当服务的某个API接口的失败次数在一定时间内小于设定的阈值时,熔断器处于关闭状态,该API接口正常提供服务。当该API接口处理请求失败的次数大于设定阈值时,Hystrix判定该接口出现了故障,打开熔断器。这时请求该API接口会执行快速失败的逻辑(fallback的逻辑),不执行业务,请求的线程不会处于阻塞状态。 处于打开状态的熔断器,一段时间后会处于半打开状态,并将一定数量的请求执行正常逻辑。剩余的请求会执行快速失败,若执行正常请求的逻辑失败了,则熔断器继续打开。如果执行成功了,则将熔断器关闭,这样设计的熔断器则具备了自我修复的能力。

在RestTemplate和Ribbon作为服务消费者时使用Hystrix

新建父Module和4个子Module,项目结构如下图


父Module主要用来引入DependencyManagement

  <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.2.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

创建EurekaServer

创建子Module, try-spring-cloud-eureka-server。
pom.xml

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</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-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

application.yml

server:
  port: 7001
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka # 注册中心端口7001
    register-with-eureka: false
    fetch-registry: false

Main 函数入口

@SpringBootApplication
@EnableEurekaServer
public class MyEurekaServer7001 {

    public static void main(String[] args) {
        SpringApplication.run(MyEurekaServer7001.class,args);
    }
}

创建Student Server Provider

创建子Module,try-spring-cloud-student-service
pom.xml

 <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</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-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

application.yml

server:
  port: 8001
spring:
  application:
    name: CLOUD-STUDENT-SERVICE

eureka:
  client:
    fetch-registry: false
    register-with-eureka: true # 注册进Eureka Server
    service-url:
      defaultZone: http://localhost:7001/eureka # 单机版指向7001

Main函数

@SpringBootApplication
@EnableEurekaClient
public class MyStudentService8001 {

    public static void main(String[] args) {
        SpringApplication.run(MyStudentService8001.class,args);
    }
}

Controller, 我在这里先返回了一个字符串用来测试使用。

@RestController
@RequestMapping("/student")
public class StudentController {

    @GetMapping("/version")
    public String version(){
        return "8001,202008182343";
    }
}

创建Ribbon Client

创建子Module,try-spring-cloud-ribbon-hystrix。 该模块下,我们基于RestTemplate和Ribbon作为消费者调用服务,先测试下服务正常时访问逻辑, 和前面两个Module不同,这个Module里需要引入spring-cloud-starter-netflix-hystrix依赖。
pom.xml

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</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-test</artifactId>
        </dependency>
        <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-netflix-hystrix</artifactId>
        </dependency>
    </dependencies>

application.yml

server:
  port: 8087

spring:
  application:
    name: STUDENT-CONSUMER
eureka:
  client:
    register-with-eureka: false
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka # 服务注册中心地址

Main函数,这里增加EnableHystrix注解,开启熔断器。

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrix
public class MyStudentRibbonHystrix {

    public static void main(String[] args) {
        SpringApplication.run(MyStudentRibbonHystrix.class,args);
    }

}

Ribbon调用配置,关键注解LoadBalanced。

@Configuration
public class MyWebConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

Controller, 这里调用CLOUD-STUDENT-SERVICE的version()接口,同时增加了HystrixCommand注解,设置了属性fallbackMethod, 如果方法调用失败则执行快速失败方法getErrorInfo。

@RestController
@RequestMapping("/student")
public class StudentController {

    @Autowired
    RestTemplate restTemplate;

    /**
     * 使用HystrixCommand注解,设置服务调用失败时回调方法(getErrorInfo)
     * **/
    @GetMapping("/version")
    @HystrixCommand(fallbackMethod = "getErrorInfo")
    public String version() {
        System.out.println("Ribbon调用前");
        String result = restTemplate.getForObject("http://CLOUD-STUDENT-SERVICE/student/version", String.class);
        System.out.println("Ribbon调用后,返回值:" + result);
        return result;
    }

    public String getErrorInfo(){
        return "Network error, please hold on...";
    }
}

依此启动Eureka-Server、Student-Service、Ribbon-Hystrix先测试下服务正常调用逻辑。
访问http://localhost:8087/student/version, 如果正常输出8001,202008182343字符串,则说明服务目前正常。然后我们在停止Student-Service(8001)的服务,再访问下8087服务, 结果输出Network error, please hold on… 则说明熔断器已经生效。
image

HystrixCommand注解参数

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HystrixCommand {
    #配置全局唯一标识服务的名称
    String groupKey() default "";
    
    #配置全局唯一标识服务分组的名称,比如,库存系统、订单系统、系统用户就是一个单独的服务分组
    String commandKey() default "";

    #对线程池进行设定,细粒度的配置,相当于对单个服务的线程池信息进行设置,也可多个服务设置同一个threadPoolKey构成线程组
    String threadPoolKey() default "";

    #执行快速失败的回调函数,@HystrixCommand修饰的函数必须和这个回调函数定义在同一个类中,因为定义在了同一个类中,所以fackback method可以是public/private均可
    String fallbackMethod() default "";

    #配置该命令的一些参数,如executionIsolationStrategy配置执行隔离策略,默认是使用线程隔离
    HystrixProperty[] commandProperties() default {};

    #线程池相关参数设置,具体可以设置哪些参数请见:com.netflix.hystrix.HystrixThreadPoolProperties
    HystrixProperty[] threadPoolProperties() default {};

    #调用服务时,除了HystrixBadRequestException之外,其他@HystrixCommand修饰的函数抛出的异常均会被Hystrix认为命令执行失败而触发服务降级的处理逻辑(调用fallbackMethod指定的回调函数),所以当需要在命令执行中抛出不触发降级的异常时来使用它,通过这个参数指定,哪些异常抛出时不触发降级(不去调用fallbackMethod),而是将异常向上抛出
    Class<? extends Throwable>[] ignoreExceptions() default {};

    #定义hystrix observable command的模式
    ObservableExecutionMode observableExecutionMode() default ObservableExecutionMode.EAGER;

    #任何不可忽略的异常都包含在HystrixRuntimeException中
    HystrixException[] raiseHystrixExceptions() default {};

    #默认的回调函数,该函数的函数体不能有入参,返回值类型与@HystrixCommand修饰的函数体的返回值一致。如果指定了fallbackMethod,则fallbackMethod优先级更高
    String defaultFallback() default "";
}

在OpenFeign作为服务消费者时使用Hystrix

创建子Module, try-spring-cloud-openfeign-hystrix,这个节点使用OpenFeign作为服务消费者时使用Hystrix, OpenFeign里已经依赖了Hystrix,所以这里不需要再单独引入,只需要引入spring-cloud-starter-openfeign即可。
pom.xml

   <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</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-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <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>
    </dependencies>
server:
  port: 8087

spring:
  application:
    name: STUDENT-OPENFEIGN-CONSUMER
eureka:
  client:
    register-with-eureka: false
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka # 服务注册中心地址

feign:
  hystrix:
    enabled: true #在feign中开启hystrix

定义调用CLOUD-STUDENT-SERVICE的接口,也就是增加注解FeignClient,设置value和fallback属性。

@FeignClient(value = "CLOUD-STUDENT-SERVICE",fallback = StudentFallbackService.class)
public interface StudentService {
    
    @GetMapping("/student/version")
    String version();
}

StudentFallbackService

@Component
public class StudentFallbackService implements StudentService {
    @Override
    public String version() {
        return "Network Error, I am callback service...";
    }
}

Controller

@RestController
@RequestMapping("/student")
public class StudentController {

    @Autowired
    StudentService studentService;

    @GetMapping("/version")
    public String version(){
        System.out.println("===openfeign 调用===");
        return studentService.version();
    }
}

Main函数

@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class MyStudentOpenfeignHystrix {

    public static void main(String[] args) {
        SpringApplication.run(MyStudentOpenfeignHystrix.class,args);
    }
}

依次启动Eureka-Server, Student-Service,OpenFeign-Hystrix-Client,和上面Ribbon一样,先来测试下正常逻辑,访问http://localhost:8087/student/version. 输出8001,202008182343字符串说明服务调用正常,然后停掉Student-Service,再次访问接口,输出Network Error, I am callback service… 则说明基于OpenFeign的熔断器已经生效。
image

请求缓存功能@CacheResult

Hystrix还提供了请求缓存功能,当一些查询服务不可用时,可以调用缓存查询。@CacheResult需要和@HystrixCommand组合使用

注解 描述 属性
@CacheResult 该注解用来标记请求命令返回的结果应该被缓存,它必须与@HystrixCommand注解结合使用 cacheKeyMethod
@CacheRemove 该注解用来让请求命令的缓存失效,失效的缓存根据定义Key决定 commandKey, cacheKeyMethod
@CacheKey 该注解用来在请求命令的参数上标记,使其作为缓存的Key值,如果没有标注则会使用所有参数。如果同事还是使用了@CacheResult和@CacheRemove注解的cacheKeyMethod方法指定缓存Key的生成,那么该注解将不会起作用 value

总结

Hystrix现在已经宣布停止更新了,进入了维护模式,但是其性能也比较稳定了,spring官方推荐了Resilience4J、阿里的SentinelSpring Retry作为替代方案。


原文:SpringBoot + SpringCloud Hystrix 实现服务熔断 - 歪头儿在帝都 - 博客园
作者: 歪头儿在帝都