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 毫秒后,就不会出现这个误差。