目录
1 服务雪崩效应
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,即服务雪崩。
服务雪崩效应是一种因服务提供者的不可用导致服务调用者的不可用,并将不可用逐渐放大的过程。(注:此定义来自某些博客大神,无法确定出处)
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列、线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都意味着需要对故障和延迟进行隔离和管理,防止单个依赖的失败,不会影响到整个应用程序或系统。
Hystrix是一个用于处理分布式系统延迟和容错的开源库。在分布式系统里,依赖之间不可避免会发生调用失败的情况,如超时、异常等。Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,提高分布式系统的弹性。
2 重要概念
2.1 服务降级
服务器忙,请稍后再试,不让客户端等待并返回一个友好提示,这个友好提示其实就是一个兜底方案。
哪些情况会出现服务降级
程序运行异常、超时、服务熔断触发降级、线程池或信号量打满也会触发降级
2.2 服务熔断
类比保险丝达到最大服务访问后,直接拒绝访问,拉闸断电,然后调用服务降级的方法返回友好提示。
流程为:服务熔断->服务降级->恢复调用链路。
2.3 服务限流
秒杀高并发操作,严禁一窝蜂的过来拥挤,大家排队,一秒N个,有序进行。有点类似春节时火车站的限流场景。
3 服务降级实测(服务提供者)
3.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.bighuan.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-provider-hystrix-payment8001</artifactId>
<dependencies>
<!-- hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!-- Eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--引入自定义的api通用包,可以使用Payment支付Entity-->
<dependency>
<groupId>com.bighuan.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<!-- <version>1.1.10</version>-->
</dependency>
<!--mysql connector-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.bighuan.springcloud.PaymentHystrixMain8001</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
3.2 application.yml
注意:数据库相关配置与测试服务降级无关
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包
url: jdbc:mysql://127.0.0.1:3306/db2019?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
eureka:
client:
# 表示是否将自己注册进Eureka Server,默认为true
register-with-eureka: true
# 是否从Eureka Server获取已有的注册信息,默认为true,单节点无所谓,集群必须设置为true,才能配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka # 单机版
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.bighuan.springcloud.entities # 所有Entity别类名所在包
3.3 主启动类
主启动类上添加了@EnableCircuitBreaker注解,启动断路器功能。
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
3.4 业务类
接口PaymentService
/**
* 测试hystrix,返回正常的信息
* @param id
* @return
*/
String paymentInfoOK(Integer id);
/**
* 测试hystrix,方法不能正常执行
* @param id
* @return
*/
String paymentInfoTimeout(Integer id);
接口实现PaymentServiceImpl
@Override
public String paymentInfoOK(Integer id) {
return "线程池:"+Thread.currentThread().getName()+" paymentInfoOK,id:"+id;
}
@HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="5000")
})
@Override
public String paymentInfoTimeout(Integer id) {
int timeout=3;
try {
TimeUnit.SECONDS.sleep(timeout);
} catch (InterruptedException e) {
// e.printStackTrace();
}
//int age = 10/0;
return "线程池:"+Thread.currentThread().getName()+" paymentInfoTimeout,id:"+id+",耗时(秒)";
}
// 兜底方法
public String paymentInfoTimeoutHandler(Integer id) {
return "线程池:"+Thread.currentThread().getName()+" 系统繁忙或运行报错,请稍后再试!,id:"+id+",o(╥﹏╥)o";
}
(controller省略)
3.5 测试
a)当接口实现的paymentInfoTimeOut方法中,timeOut=3秒时,并没有超过设置的超时时间5秒,所有可以正常返回信息。(浏览器访问)
b)当接口实现的paymentInfoTimeOut方法的timeOut设置为6秒时,超过设置的超时时间5秒,此时就进入到兜底方法中,返回给用户一个友好提示。浏览器访问)
c)当接口实现的paymentInfoTimeOut方法中,timeOut=3秒时,并没有超过设置的超时时间5秒,但是通过JMeter进行压力测试(设置一个线程组200个线程,每秒发一次,循环100次,共20000个请求),高并发使服务不可用,发现程序立马就进行了服务降级。
但是最后有几个请求得到了成功响应,请求到最后系统压力没那么大了,没有走兜底方法。
4 服务降级实测(服务调用者)
4.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.bighuan.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-feign-hystrix-order80</artifactId>
<dependencies>
<!--Hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--引入自定义的api通用包,可以使用Payment支付Entity-->
<dependency>
<groupId>com.bighuan.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.bighuan.springcloud.OrderFeignHystrixMain80</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
4.2 application.yml
server:
port: 80
spring:
application:
name: cloud-order-service
eureka:
instance:
instance-id: order${server.port} #例如:order80
prefer-ip-address: true # 访问路径可以显示ip地址
client:
# 表示是否将自己注册进Eureka Server,默认为true
register-with-eureka: true
# 是否从Eureka Server获取已有的注册信息,默认为true,单节点无所谓,集群必须设置为true,才能配合ribbon使用负载均衡
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
# defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
ribbon:
# 处理请求的超时时间,默认为1秒
ReadTimeout: 5000
# 连接建立的超时时间,默认1秒
ConnectTimeout: 5000
logging:
level:
# feign日志以什么级别监控哪个接口
#com.bighuan.springcloud.service.PaymentFeignService: debug
com.bighuan.springcloud.service: debug
feign:
hystrix:
# 开启feign的hystrix支持
enabled: true
4.3 启动类
@EnableHystrix注解用以激活Hystrix。
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableHystrix // 激活Hystrix
public class OrderFeignHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignHystrixMain80.class,args);
}
}
4.4 业务类
@Component
@FeignClient(name = "CLOUD-PROVIDER-HYSTRIX-PAYMENT-SERVICE",fallback = PaymentFeignHystrixFallbackService.class) // 设置远程服务,将一个远程服务映射为一个本地方法调用
public interface PaymentFeignHystrixService {
/**
* 查询
*
* @param id
* @return
*/
@GetMapping(value = "/payment/get/{id}")
CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
@GetMapping(value = "/payment/feign/timeout")
String paymentFeignTimeOut();
@GetMapping(value="/payment/hystrix/ok/{id}")
public String paymentInfoOK(@PathVariable("id") Integer id);
@GetMapping(value="/payment/hystrix/timeout/{id}")
public String paymentInfoTimeout(@PathVariable("id") Integer id);
}
@Component
public class PaymentFeignHystrixFallbackService implements PaymentFeignHystrixService {
@Override
public CommonResult<Payment> getPaymentById(Long id) {
return null;
}
@Override
public String paymentFeignTimeOut() {
return null;
}
@Override
public String paymentInfoOK(Integer id) {
return "-----PaymentFeignHystrixFallbackService fall back -paymentInfoOK,o(╥﹏╥)o";
}
@Override
public String paymentInfoTimeout(Integer id) {
return "-----PaymentFeignHystrixFallbackService fall back -paymentInfoTimeout,o(╥﹏╥)o";
}
}
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "paymentGlobalFallBackMethod")
public class OrderHystrixFeignController {
// 有特殊指定的兜底方法
/* @HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="1500")
})*/
// 使用全局默认的兜底方法
@HystrixCommand
@GetMapping(value = "/consumer/payment/hystrix/timeout/{id}")
public String paymentInfoTimeout(@PathVariable("id") Integer id) {
int age =10/0;
String result = paymentFeignHystrixService.paymentInfoTimeout(id);
return result;
}
// 兜底方法
public String paymentInfoTimeoutHandler(Integer id) {
return "我是消费者80,对方支付系统繁忙,请10秒钟后重新尝试或者自身运行出错,请检查自己!!!o(╥﹏╥)o";
}
// 下面是全局fallback方法,全局默认的兜底方法
public String paymentGlobalFallBackMethod(){
return "Global异常处理信息,请稍后再试!o(╥﹏╥)o";
}
}
说明如下
代码膨胀问题:
a)如果每个方法都要做一个方法进行服务降级,那么会导致代码膨胀的问题,那么此时可以通过全局服务降级来避免该问题。
@DefaultProperties(defaultFallback = "paymentGlobalFallBackMethod")
如果一个方法有指定兜底方法,那么就走指定的兜底方法;如果没有指定兜底方法,但是配置了全局兜底方法(使用了注解@HystrixCommand),就走全局的兜底方法。
代码混乱问题:
每一个方法都要对应一个兜底方法的话,如果都在同一个类或接口中,那么代码是比较混乱的。所以可以通过接口和对应的实现来解决代码混乱问题。
如上代码,PaymentFeignHystrixService是一个FeignClient,这个接口对应的实现即为服务降级对应的方法。需要在PaymentFeignHystrixService上添加如下对应的fallback配置。
@FeignClient(name = "CLOUD-PROVIDER-HYSTRIX-PAYMENT-SERVICE",fallback = PaymentFeignHystrixFallbackService.class)
对应的测试与第三章节类似,不赘述。
5 服务熔断
(说明:此章节在第三章节基础上进行测试)
5.1 测试代码
增加对应的测试代码
PaymentService:
/**
* 测试服务熔断
* @param id
* @return
*/
String paymentCircuitBreaker(Integer id);
PaymentServiceImpl:
/***********服务熔断**********/
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback",commandProperties={
@HystrixProperty(name="circuitBreaker.enabled",value = "true"), // 是否看齐断路器
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="10"), // 请求次数
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value="10000"), // 时间窗口期
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value="10") // 当失败率达到多少后就跳闸
})
@Override
public String paymentCircuitBreaker( Integer id){
if(id < 0){
throw new RuntimeException("************id为负数");
}
String serialNumber= IdUtil.simpleUUID();
return Thread.currentThread().getName()+"调用成功,流水号为:"+serialNumber;
}
public String paymentCircuitBreakerFallback(@PathVariable("id") Integer id){
return "id:"+id+"不能为负数,请稍后再试!o(╥﹏╥)o";
}
涉及断路器的几个重要重要参数(Hystrix相关配置)
a)circuitBreaker.enabled :是否开启断路器
b)circuitBreaker.requestVolumeThreshold :请求总数阈值,在快照时间窗内,必须满足请求总数阈值才有资格熔断,默认为20次,即在快照时间窗内,若Hystrix命令的调用次数不足20次,即使所有请求超时或其他原因失败,也不会打开断路器。
c)circuitBreaker.sleepWindowInMilliseconds :快照时间窗,断路器确定是否打开需要统计请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的5秒。
d)circuitBreaker.errorThresholdPercentage:错误百分比阈值,当请求总数在快照时间窗内超过了阈值,比如30次调用,发生了15次超时异常,即超过50%的错误率,在默认设定50%的阈值情况下,此时断路器就会打开。
controller:
/************服务熔断**************/
@GetMapping(value="/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
String result = paymentService.paymentCircuitBreaker(id);
return result;
}
测试:
a)当一个快照时间窗同时满足了请求总数阈值、错误百分比阈值,那么断路器将会开启。当断路器开启的时候,所有请求都不会进行转发。
b)当一个时间窗后,断路器会切换为半开状态,会让其中一个请求进行转发,转发成功,断路器关闭,否则继续开启,并按上述步骤重复尝试关闭断路器。