Springcloud基础知识(6)- Spring Cloud Hystrix (一) | Hystrix 简介、服务降级


在微服务架构中,一个应用往往由多个服务组成,这些服务之间相互依赖,依赖关系错综复杂。

例如一个微服务系统中存在 A、B、C、D、E、F 等多个服务,它们的依赖关系如下图。

    请求1      请求2        请求3
      |              |               |
     V             V              V
    服务A      服务B       服务C
      |              |               |
     V             V              V
   服务D <-> 服务E <-> 服务F

通常情况下,一个用户请求往往需要多个服务配合才能完成。如上图所示的服务场景中,请求1 需要调用 A、D、E、F 四个服务才能完成,请求2 需要调用 B、E、D 三个服务才能完成,请求3 需要调用服务 C、F、E、D 四个服务才能完成。

当服务 E 发生故障或网络延迟时,会出现以下情况:

    (1) 即使其他所有服务都可用,由于服务 E 的不可用,那么用户请求 1、2、3 都会处于阻塞状态,等待服务 E 的响应。在高并发的场景下,会导致整个服务器的线程资源在短时间内迅速消耗殆尽。
    (2) 所有依赖于服务 E 的其他服务,例如服务 B、D 以及 F 也都会处于线程阻塞状态,等待服务 E 的响应,导致这些服务的不可用。
    (3) 所有依赖服务B、D 和 F 的服务,例如服务 A 和服务 C 也会处于线程阻塞状态,以等待服务 D 和服务 F 的响应,导致服务 A 和服务 C 也不可用。

从以上过程可以看出,当微服务系统的一个服务出现故障时,故障会沿着服务的调用链路在系统中疯狂蔓延,最终导致整个微服务系统的瘫痪,这就是“雪崩效应”。为了防止此类事件的发生,微服务架构引入了 “熔断器” 的一系列服务容错和保护机制。

熔断器(Circuit Breaker)一词来源物理学中的电路知识,它的作用是当线路出现故障时,迅速切断电源以保护电路的安全。

在微服务领域,熔断器最早是由 Martin Fowler 在他发表的 《Circuit Breaker》一文中提出。与物理学中的熔断器作用相似,微服务架构中的熔断器能够在某个服务发生故障后,向服务调用方返回一个符合预期的、可处理的降级响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常。这样就保证了服务调用方的线程不会被长时间、不必要地占用,避免故障在微服务系统中的蔓延,防止系统雪崩效应的发生。


1. Hystrix 简介

    Spring Cloud Hystrix 是一款优秀的服务容错与保护组件,也是 Spring Cloud 中最重要的组件之一。

    Spring Cloud Hystrix 是基于 Netflix 公司的开源组件 Hystrix 实现的,它提供了熔断器功能,能够有效地阻止分布式微服务系统中出现联动故障,以提高微服务系统的弹性。Spring Cloud Hystrix 具有服务降级、服务熔断、线程隔离、请求缓存、请求合并以及实时故障监控等强大功能。

    Hystrix [hɪst'rɪks],中文含义是豪猪,豪猪的背上长满了棘刺,使它拥有了强大的自我保护能力。而 Spring Cloud Hystrix 作为一个服务容错与保护组件,也可以让服务拥有自我保护的能力,因此也有人将其戏称为“豪猪哥”。

    在微服务系统中,Hystrix 能够帮助我们实现以下目标:

        (1) 保护线程资源:防止单个服务的故障耗尽系统中的所有线程资源。
        (2) 快速失败机制:当某个服务发生了故障,不让服务调用方一直等待,而是直接返回请求失败。
        (3) 提供降级(FallBack)方案:在请求失败后,提供一个设计好的降级方案,通常是一个兜底方法,当请求失败后即调用该方法。
        (4) 防止故障扩散:使用熔断机制,防止故障扩散到其他服务。
        (5) 监控功能:提供熔断器故障监控组件 Hystrix Dashboard,随时监控熔断器的状态。

    Netflix Hystrix GitHub: https://github.com/Netflix/Hystrix


2. Hystrix 服务降级

    Hystrix 提供了服务降级功能,能够保证当前服务不受其他服务故障的影响,提高服务的健壮性。

    Hystrix 会在以下场景下进行服务降级处理:

        (1) 程序运行异常
        (2) 服务超时
        (3) 熔断器处于打开状态
        (4) 线程池资源耗尽

    Hystrix 服务降级 FallBack 既可以放在服务端进行,也可以放在客户端进行。

    可以通过重写 HystrixCommand 的 getFallBack() 方法或 HystrixObservableCommand 的 resumeWithFallback() 方法,使服务支持服务降级。


3. 服务端服务降级

    本文将在 “ Springcloud基础知识(5)- Spring Cloud OpenFeign | 声明式服务调用 ” 里SpringcloudDemo03 项目基础上(包含 ConsumerFeign 子模块),添加一个 ServiceProviderHystrix 子模块,来演示 Hystrix 服务端服务降级。

    SpringcloudDemo03 的 Spring Boot 版本是 2.3.12.RELEASE。

    1) 创建 ServiceProviderHystrix 模块

        选择左上的项目列表中的 SpringcloudDemo03,点击鼠标右键,选择 New -> Module 进入 New Module 页面:

            Maven -> Project SDK: 1.8 -> Check "Create from archtype" -> select "org.apache.maven.archtypes:maven-archtype-quickstart" -> Next

                Name: ServiceProviderHystrix
                GroupId: com.example
                ArtifactId: ServiceProviderHystrix

            -> Finish

        注:模块 ServiceProviderHystrix 创建后,Maven 命令会自动修改主项目 SpringcloudDemo03 的 pom.xml,添加如下内容:

            <modules>
                ...

                <module>ServiceProviderHystrix</module>
            </modules>

    2) 修改 ServiceProviderHystrix 的 pom.xml 内容如下

 1         <?xml version="1.0" encoding="UTF-8"?>
 2         <project xmlns="http://maven.apache.org/POM/4.0.0"
 3                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4                 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
 5                                     http://maven.apache.org/xsd/maven-4.0.0.xsd">
 6             <parent>
 7                 <artifactId>SpringcloudDemo03</artifactId>
 8                 <groupId>com.example</groupId>
 9                 <version>1.0-SNAPSHOT</version>
10             </parent>
11             <modelVersion>4.0.0</modelVersion>
12 
13             <artifactId>ServiceProviderHystrix</artifactId>
14 
15             <name>ServiceProviderHystrix</name>
16             <!-- FIXME change it to the project's website -->
17             <url>http://www.example.com</url>
18 
19             <properties>
20                 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
21                 <maven.compiler.source>1.8</maven.compiler.source>
22                 <maven.compiler.target>1.8</maven.compiler.target>
23             </properties>
24 
25             <dependencies>
26                 <dependency>
27                     <groupId>junit</groupId>
28                     <artifactId>junit</artifactId>
29                     <version>4.12</version>
30                     <scope>test</scope>
31                 </dependency>
32                 <dependency>
33                     <groupId>org.springframework.boot</groupId>
34                     <artifactId>spring-boot-starter-web</artifactId>
35                 </dependency>
36                 <dependency>
37                     <groupId>org.springframework.boot</groupId>
38                     <artifactId>spring-boot-starter-test</artifactId>
39                     <scope>test</scope>
40                 </dependency>
41                 <!-- 引入公共子模块 -->
42                 <dependency>
43                     <groupId>com.example</groupId>
44                     <artifactId>CommonAPI</artifactId>
45                     <version>${project.version}</version>
46                 </dependency>
47                 <dependency>
48                     <groupId>org.projectlombok</groupId>
49                     <artifactId>lombok</artifactId>
50                     <version>1.18.8</version>
51                 </dependency>
52                 <!-- logback 日志 -->
53                 <dependency>
54                     <groupId>ch.qos.logback</groupId>
55                     <artifactId>logback-core</artifactId>
56                 </dependency>
57                 <!-- 修改后立即生效,热部署 -->
58                 <dependency>
59                     <groupId>org.springframework</groupId>
60                     <artifactId>springloaded</artifactId>
61                     <version>1.2.8.RELEASE</version>
62                 </dependency>
63                 <!-- 引入 Eureka Client 的依赖,将服务注册到 Eureka Server -->
64                 <dependency>
65                     <groupId>org.springframework.cloud</groupId>
66                     <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
67                 </dependency>
68                 <dependency>
69                     <groupId>org.springframework.cloud</groupId>
70                     <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
71                 </dependency>
72                 <!-- 添加 Spring Boot 的监控模块 -->
73                 <dependency>
74                     <groupId>org.springframework.boot</groupId>
75                     <artifactId>spring-boot-starter-actuator</artifactId>
76                 </dependency>                
77             </dependencies>
78 
79             <build>
80                 <plugins>
81                     <plugin>
82                         <groupId>org.springframework.boot</groupId>
83                         <artifactId>spring-boot-maven-plugin</artifactId>
84                         <configuration>
85                             <mainClass>com.example.App</mainClass>
86                             <layout>JAR</layout>
87                         </configuration>
88                         <executions>
89                             <execution>
90                                 <goals>
91                                     <goal>repackage</goal>
92                                 </goals>
93                             </execution>
94                         </executions>
95                     </plugin>
96                 </plugins>
97             </build>
98 
99         </project>

 

    3) 创建 src/main/resources/application.yml 文件

 1         server:
 2             port: 8004      # 服务端口号
 3         spring:
 4             application:
 5                 name: employee-service-provider-hystrix
 6 
 7         #### Eureka Client ####
 8         eureka:
 9             client:       # 将客户端注册到 eureka 服务列表内
10                 service-url:
11                     #defaultZone: http://localhost:7001/eureka  # 服务注册中心地址
12                     defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/   # 服务注册中心地址
13 
14             instance:
15                 instance-id: sevice-provider-8004   # 自定义服务名称信息
16                 prefer-ip-address: true             # 显示访问路径的 ip 地址
17 
18
19 # Spring Boot actuator 20 management: 21 endpoints: 22 web: 23 exposure: 24 include: "*" # "*" 表示打开全部节点,多个节点用逗号分隔,比如 "beans,bus-refresh,channels" 25 info: 26 app.name: employee-service-provider-hystrix 27 company.name: com.example 28 build.aetifactId: @project.artifactId@ 29 build.version: @project.version@


    4) 创建 src/main/java/com/example/service/EmployeeService.java 文件

 1         package com.example.service;
 2 
 3         public interface EmployeeService {
 4 
 5             // Hystrix 熔断器,显示 Ok
 6             public String employeeInfo_Ok(Integer id);
 7 
 8             // Hystrix 熔断器,显示 Timeout
 9             public String employeeInfo_Timeout(Integer id);
10 
11         }


    5) 创建 src/main/java/com/example/service/EmployeeServiceImpl.java 文件

 1         package com.example.service;
 2 
 3         import java.util.concurrent.TimeUnit;
 4         import org.springframework.stereotype.Service;
 5         import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
 6         import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
 7 
 8         @Service("employeeService")
 9         public class EmployeeServiceImpl implements EmployeeService {
10 
11             @Override
12             @HystrixCommand(fallbackMethod = "timeoutHandler",
13                     commandProperties = {
14                             @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
15                                     value = "5000")
16                     })
17             public String employeeInfo_Ok(Integer id) {
18 
19                 int timeoutValue = 4;
20                 try {
21                     TimeUnit.SECONDS.sleep(timeoutValue);
22                 } catch (InterruptedException e) {
23                     e.printStackTrace();
24                 }
25 
26                 return "EmployeeServiceImpl -> employeeInfo_Ok(): thread = " + Thread.currentThread().getName() + ", id = " + id + ", timeoutValue = " + timeoutValue;
27             }
28 
29             @Override
30             @HystrixCommand(fallbackMethod = "timeoutHandler",
31                             commandProperties = {
32                                 @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
33                                                 value = "5000")
34                             })
35             public String employeeInfo_Timeout(Integer id) {
36 
37                 int timeoutValue = 6;
38                 try {
39                     TimeUnit.SECONDS.sleep(timeoutValue);
40                 } catch (InterruptedException e) {
41                     e.printStackTrace();
42                 }
43 
44                 return "EmployeeServiceImpl -> employeeInfo_Timeout(): thread = " + Thread.currentThread().getName() + ", id = " + id + ", timeoutValue = " + timeoutValue;
45             }
46 
47             public String timeoutHandler(Integer id) {
48                 return  "Server: system in busy, please try later! thread = " + Thread.currentThread().getName() + ", id = " + id;
49             }
50         }


        注:timeoutHandler() 的参数必须完全匹配  @HystrixCommand 所注解的方法(服务)的参数,即参数数量和类型要相同。

    6) 创建 src/main/java/com/example/controller/EmployeeController.java 文件

 1         package com.example.controller;
 2 
 3         import lombok.extern.slf4j.Slf4j;
 4         import org.springframework.beans.factory.annotation.Autowired;
 5         import org.springframework.beans.factory.annotation.Value;
 6         import org.springframework.web.bind.annotation.RestController;
 7         import org.springframework.web.bind.annotation.RequestMapping;
 8         import org.springframework.web.bind.annotation.PathVariable;
 9 
10         import com.example.service.EmployeeService;
11 
12         @RestController
13         @Slf4j
14         @RequestMapping(value = "/employee")
15         public class EmployeeController {
16             @Autowired
17             private EmployeeService employeeService;
18             @Value("${server.port}")
19             private String serverPort;
20 
21             @RequestMapping(value = "/hystrix/ok/{id}")
22             public String employeeInfo_Ok(@PathVariable("id") Integer id) {
23 
24                 String result = employeeService.employeeInfo_Ok(id);
25                 log.info("port:" + serverPort + ", result:" + result);
26                 return result + ", port: " + serverPort;
27 
28             }
29 
30             // Hystrix 服务超时降级
31             @RequestMapping(value = "/hystrix/timeout/{id}")
32             public String employeeInfo_Timeout(@PathVariable("id") Integer id) {
33 
34                 String result = employeeService.employeeInfo_Timeout(id);
35                 log.info("port:" + serverPort + ", result:" + result);
36                 return result + ", port: " + serverPort;
37 
38             }
39         }


    7) 修改 src/main/java/com/example/App.java 文件

 1         package com.example;
 2 
 3         import org.springframework.boot.SpringApplication;
 4         import org.springframework.boot.autoconfigure.SpringBootApplication;
 5         import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
 6         import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
 7 
 8         @SpringBootApplication
 9         @EnableEurekaClient     // 开启 Eureka 客户端功能
10         @EnableCircuitBreaker   // 激活熔断器功能
11         public class App {
12             public static void main(String[] args) {
13                 SpringApplication.run(App.class, args);
14             }
15         }


    8) 运行

        下面我们在 “ Springcould基础知识(3)- Spring Cloud Eureka (二) | Eureka Server 集群 ” 里的集群基础上,测试服务端服务降级。

        依次启动 server-7001、server-7002、server-7003,启动的间隔 5 ~ 10 秒,都启动后等待 10 秒左右。

        启动 ServiceProviderHystrix 模块:

            浏览器访问 http://eureka7001.com:7001/,页面上 “Instances currently registered with Eureka” 区域显示:

                Instances currently registered with Eureka
                
                Application                                                   AMIs    Availability Zones          Status
                EMPLOYEE-SERVICE-PROVIDER-HYSTRIX    n/a (1)        (1)                UP (1) - sevice-provider-8004

            浏览器访问 http://localhost:8004/employee/hystrix/ok/1,4 秒后显示结果如下:

                EmployeeServiceImpl -> employeeInfo_Ok(): thread = hystrix-EmployeeServiceImpl-1, id = 1, timeoutValue = 4, port: 8004

                注:employeeInfo_Ok() 设置的熔断器 timeout 是 5 秒,employeeInfo_Ok() 函数 4 秒就返回了, 所以没有触发熔断器,即 timeoutHandler() 没有被调用。

            访问 http://localhost:8004/employee/hystrix/timeout/1,5 秒后后显示结果如下:

                Server: system in busy, please try later! thread = HystrixTimer-1, id = 1, port: 8004

                注:5 秒后 employeeInfo_Timeout() 函数还没返回,所以触发了熔断器,调用了 timeoutHandler()。

        参考集群的 service-8001 的配置,我们把 ServiceProviderHystrix 模块打包配置为 service-8004。


4. 客户端服务降级

    通常情况下,都会在客户端进行服务降级,当客户端调用的服务端的服务不可用时,客户端直接进行服务降级处理,避免其线程被长时间、不必要地占用。

    本文将在 SpringcloudDemo03 项目的 ConsumerFeign 子模块里,添加 Hystrix 功能,实现客户端服务降级。

    1) 在 ConsumerFeign 模块的 pom.xml 里添加 Hystrix 的依赖,代码如下

1         <!-- hystrix 依赖-->
2         <dependency>
3             <groupId>org.springframework.cloud</groupId>
4             <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
5         </dependency>


    2) 创建 src/main/java/com/example/EmployeeHystrixService.java 文件

 1         package com.example.service;
 2 
 3         import org.springframework.stereotype.Service;
 4         import org.springframework.cloud.openfeign.FeignClient;
 5         import org.springframework.web.bind.annotation.PathVariable;
 6         import org.springframework.web.bind.annotation.RequestMapping;
 7 
 8         @Service
 9         @FeignClient(value = "EMPLOYEE-SERVICE-PROVIDER-HYSTRIX")
10         public interface EmployeeHystrixService {
11             @RequestMapping(value = "/employee/hystrix/ok/{id}")
12             public String employeeInfo_Ok(@PathVariable("id") Integer id);
13 
14             @RequestMapping(value = "/employee/hystrix/timeout/{id}")
15             public String employeeInfo_Timeout(@PathVariable("id") Integer id);
16         }


        EmployeeHystrixService 的服务接口,与 ServiceProviderHystrix 模块的服务接口进行绑定。

    3) 修改 src/main/java/com/example/controller/ConsumerController.java 文件

 1         package com.example.controller;
 2 
 3         import java.util.List;
 4 
 5         import org.springframework.beans.factory.annotation.Autowired;
 6         import org.springframework.web.bind.annotation.PathVariable;
 7         import org.springframework.web.bind.annotation.RequestMapping;
 8         import org.springframework.web.bind.annotation.RestController;
 9         import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
10 
11         import com.example.entity.Employee;
12         import com.example.service.EmployeeHystrixService;
13 
14         @RestController
15         @RequestMapping(value = "/consumer")
16         public class ConsumerController {
17             @Autowired
18             private EmployeeHystrixService employeeHystrixService;
19 
20             @RequestMapping(value = "/employee/hystrix/ok/{id}")
21             @HystrixCommand(fallbackMethod = "timeoutHandler")
22             public String employeeInfo_Ok(@PathVariable("id") Integer id) {
23                 return employeeHystrixService.employeeInfo_Ok(id);
24             }
25 
26             @RequestMapping(value = "/employee/hystrix/timeout/{id}")
27             public String employeeInfo_Timeout(@PathVariable("id") Integer id) {
28                 return employeeHystrixService.employeeInfo_Timeout(id);
29             }t
30 
31             // employeeInfo_Ok() 的 fallback 方法
32             public String timeoutHandler(@PathVariable("id") Integer id) {
33                 return "Client: system in busy, please try later!";
34             }
35 
36         }


    4) 修改 src/main/java/com/example/App.java 文件

 1         package com.example;
 2 
 3         import org.springframework.boot.SpringApplication;
 4         import org.springframework.boot.autoconfigure.SpringBootApplication;
 5         import org.springframework.cloud.openfeign.EnableFeignClients;
 6         import org.springframework.cloud.netflix.hystrix.EnableHystrix;
 7 
 8         @SpringBootApplication
 9         @EnableFeignClients     // 开启 OpenFeign 功能
10         @EnableHystrix          //启用 Hystrix
11         public class App {
12             public static void main(String[] args) {
13                 SpringApplication.run(App.class, args);
14             }
15         }

    5) 修改 src/main/resource/application.yml 文件,添加如下配置。

 1         feign:
 2             hystrix:
 3                 enabled: true   # 开启客户端 hystrix
 4 
 5                 (5) 修改 src/main/resource/application.yml 文件,添加如下配置。
 6 
 7         # Ribbon 客户端超时控制
 8         ribbon:
 9             ReadTimeout: 6000  # 建立连接所用的时间,适用于网络状况正常的情况下,两端两端连接所用的时间
10             ConnectionTimeout: 6000 # 建立连接后,服务器读取到可用资源的时间
11 
12         # 配置请求超时时间
13         hystrix:
14             command:
15                 default:
16                     execution:
17                         isolation:
18                             thread:
19                                 timeoutInMilliseconds: 7000
20         
21                 # 配置具体方法超时为 3 秒
22                 EmployeeHystrixService#employeeInfo_Ok(Integer):
23                     execution:
24                         isolation:
25                             thread:
26                                 timeoutInMilliseconds: 3000


    6) 运行

        依次启动 server-7001、server-7002、server-7003,启动的间隔 5 ~ 10 秒,都启动后等待 10 秒左右。

        启动 server-8004,访问 http://localhost:8004/employee/hystrix/ok/1,4 秒后显示结果如下:

            EmployeeServiceImpl -> employeeInfo_Ok(): thread = hystrix-EmployeeServiceImpl-1, id = 1, timeoutValue = 4, port: 8004

        启动 ConsumerFeign 模块,访问 http://localhost/consumer/employee/hystrix/ok/1,3 秒后显示结果如下:

            Client: system in busy, please try later!

        注:关于 application.yml 里 “Ribbon 客户端超时控制” ,在运行测试过程中遇到了客户端默认 1 ~ 2 超时问题(这个问题在 “ Springcloud基础知识(5)- Spring Cloud OpenFeign” | 声明式服务调用 ” 的 OpenFeign 单独实例中没有发现)。
        
        这里只讲一下测试过程,不讨论具体原因:

            a) 注释掉 “Ribbon 客户端超时控制” 下面的 3 行,注释掉 “# 配置具体方法超时为 3 秒” 下面的 5 行;

            b) 重启 ConsumerFeign 模块,访问 http://localhost/consumer/employee/hystrix/ok/1,1 ~ 2 秒后显示结果如下:

                    Client: system in busy, please try later!

                很显然,这不是期待的显示结果,期待的结果应该是 4 秒后显示如下:

                    EmployeeServiceImpl -> employeeInfo_Ok(): thread = hystrix-EmployeeServiceImpl-8, id = 1, timeoutValue = 4, port: 8004
            
            c) 恢复 “Ribbon 客户端超时控制” 下面的 3 行,重启 ConsumerFeign 模块,访问 http://localhost/consumer/employee/hystrix/ok/1,4 秒后显示结果如下:

                    EmployeeServiceImpl -> employeeInfo_Ok(): thread = hystrix-EmployeeServiceImpl-8, id = 1, timeoutValue = 4, port: 8004

            d) 继续测试,恢复 “# 配置具体方法超时为 3 秒” 下面的 5 行,并把 timeoutInMilliseconds 改成 4100 毫秒,重启 ConsumerFeign 模块,访问 http://localhost/consumer/employee/hystrix/ok/1,约 4 秒后显示结果如下:

                    Client: system in busy, please try later!

                多次刷新页面,都是显示:

                    EmployeeServiceImpl -> employeeInfo_Ok(): thread = hystrix-EmployeeServiceImpl-10, id = 1, timeoutValue = 4, port: 8004

                服务端(ServiceProviderHystrix)是 4 秒 (4000 毫秒)返回,客户端(ConsumerFeign)4100 毫秒 > 4000 毫秒,理论上不应该出现客户端熔断,当然,这里不需要把精度提高到 0.1 秒,设置为 5000 毫秒后,就不会出现这个误差。


posted @ 2022-06-29 09:38  垄山小站  阅读(106)  评论(0编辑  收藏  举报