说明:关于SpringCloud系列的文章中的代码都在码云上面
地址:https://gitee.com/zh_0209_java/springcloud-alibaba.git
分布式系统面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免的失败。
Hystrix的作用
Hystrix是一个用于分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的,可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间,不必要的占用,从而避免故障在分布式系统中的蔓延,乃至雪崩。
Hystrix的作用就是:服务降级,服务熔断,接近实时的监控等等。
那些情况会触发服务降级?
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
服务降级:当某个服务不能用时,可以向调用方返回一个符合预期的,可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常
服务熔断:类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法。
服务限流:秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
测试Hystrix
创建新的服务cloud-provider-hystrix-payment8001
- 修改pom
<dependencies>
<!--hystrix 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!-- 核心依赖-->
<dependency>
<groupId>com.zh.springcloud</groupId>
<artifactId>core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--引入eureka Client依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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>
<scope>test</scope>
</dependency>
</dependencies>
- 添加配置文件
server:
port: 8001
spring:
application:
name: cloud-payment-hystrix-service
datasource:
url: jdbc:mysql://localhost:3306/zh_springcloud?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowMutiQueries=true
#root
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
#mybatis plus 设置
mybatis-plus:
mapper-locations: classpath*:mapper/*Mapper.xml
global-config:
# 关闭MP3.0自带的banner
banner: false
db-config:
#主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)";
id-type: 3
# 默认数据库表下划线命名
table-underline: true
# 逻辑已删除值(默认为 1)
logic-delete-value: 1
# 逻辑未删除值(默认为 0)
logic-not-delete-value: 0
configuration:
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 返回类型为Map,显示null对应的字段
call-setters-on-nulls: true
# ============ eureka client ===========
eureka:
# 设置控制台显示的服务路径
instance:
instance-id: payment8001
prefer-ip-address: true # 访问地址可以显示ip
# Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
lease-renewal-interval-in-seconds: 1
# Eureka 服务端在收到最后一次心跳后等待时间上线,单位为秒(默认是90秒),超时将剔除服务
lease-expiration-duration-in-seconds: 2
client:
# 表示是否将自己注册进eurekaServer,默认为true
register-with-eureka: true
# 是否从EurekaServer抓取已有的注册信息,默认为true.单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 本机入住eurekaServer 地址
defaultZone: http://localhost:7001/eureka # 单机版
# defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka # 集群版
- 修改启动类
package com.zh.springcloud;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @Description:
* @ClassName PaymentApplication
* @date: 2021.06.08 14:35
* @Author: zhanghang
*/
@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.zh.springcloud.mapper")
public class PaymentHystrixApplication8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixApplication8001.class,args);
}
}
- 编写业务类
public interface PaymentService extends IService<Payment> {
/**
* description: 可以正常访问ok
* date: 2021年-06月-22日 11:56
* author: zhanghang
*
* @param id
* @return java.lang.String
*/
String paymentInfo_ok(Integer id);
/**
* description: 模拟出错
* date: 2021年-06月-22日 11:56
* author: zhanghang
*
* @param id
* @return java.lang.String
*/
String paymentInfo_timeout(Integer id);
}
@Service
public class PaymentServiceImpl extends ServiceImpl<PaymentMapper, Payment> implements PaymentService {
@Override
public String paymentInfo_ok(Integer id) {
return "线程:"+Thread.currentThread().getName()+" paymentInfo_ok,id:"+id+" \t "+"O(∩_∩)O";
}
@Override
public String paymentInfo_timeout(Integer id) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程:"+Thread.currentThread().getName()+" paymentInfo_timeout,id:"+id+" \t "+"O(∩_∩)O";
}
}
@RestController
@Slf4j
public class PaymentController {
@Autowired
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentinfo_ok(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_ok(id);
log.info("====result ->"+result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentinfo_timeout(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_timeout(id);
log.info("====result ->"+result);
return result;
}
}
- 启动服务
先启动eureka,在启动服务提供者,
可能出现的问题:因为我在父项目中集成了swagger2.9.2版本,而springfox-swagger2中依赖的guava是20.0版本,但是spring-cloud-starter-netflix-hystrix中依赖的guava为15.0版本,所有启动会tomcat会包jar包冲突的问题,解决办法就是在父项目的dependencyManagement定义中定义guava的版本即可
<!--因为springfox-swagger2是2.9.2版本,引入的guava是20.0版本,
而hystrix中的hystrix-javanica中引入的guava版本为15.0,所有会引起jar包冲突,所有在父项目中声明好版本的使用 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
当我们使用JMeter 对 timeout接口进行2W请求压测时,在访问ok接口,发现ok接口耗时明显增大。也就是超时接口会拉跨别的接口
创建访问hystrix8001的客户端 cloud-consumer-feign-hystrix-order80 服务
- 修改pom
<dependencies>
<!--open feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.zh.springcloud</groupId>
<artifactId>core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--引入eureka Client依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</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>
<scope>test</scope>
</dependency>
</dependencies>
- 修改配置文件
server:
port: 80
spring:
application:
name: cloud-order-service
datasource:
url: jdbc:mysql://localhost:3306/zh_springcloud?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowMutiQueries=true
#root
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
#mybatis plus 设置
mybatis-plus:
mapper-locations: classpath*:mapper/*Mapper.xml
global-config:
# 关闭MP3.0自带的banner
banner: false
db-config:
#主键类型 0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)";
id-type: 3
# 默认数据库表下划线命名
table-underline: true
# 逻辑已删除值(默认为 1)
logic-delete-value: 1
# 逻辑未删除值(默认为 0)
logic-not-delete-value: 0
configuration:
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 返回类型为Map,显示null对应的字段
call-setters-on-nulls: true
# ============ eureka client ===========
eureka:
client:
# 表示是否将自己注册进eurekaServer,默认为true
register-with-eureka: true
# 是否从EurekaServer抓取已有的注册信息,默认为true.单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
# 本机入住eurekaServer 地址
defaultZone: http://localhost:7001/eureka # 单机版
# defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka # 集群版
logging:
level:
com.zh.springcloud: debug
- 新增启动类
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderFeignApplication {
public static void main(String[] args) {
SpringApplication.run(OrderFeignApplication.class,args);
}
}
- 新增feign 接口
@Component
// 想要调用提供方的接口,必须加@FeignClient注解,value的值就是提供方服务的服务名称
@FeignClient(value = "cloud-payment-hystrix-service")
public interface PaymentService {
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentinfo_ok(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentinfo_timeout(@PathVariable("id") Integer id);
}
- 新增controller
@RestController
@Slf4j
public class OrderFeignController {
// 将feign接口注入进来
@Autowired
private PaymentService paymentService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public Result<?> paymentinfo_ok(@PathVariable Integer id){
Result<Object> result = new Result<>();
// 可以直接使用
String str = paymentService.paymentinfo_ok(id);
result.setResult(str);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public Result<?> paymentinfo_timeout(@PathVariable Integer id){
Result<Object> result = new Result<>();
// 可以直接使用
String str = paymentService.paymentinfo_timeout(id);
result.setResult(str);
return result;
}
}
- 启动80服务进行测试,
故障现象和导致原因:8001同一层次的其他接口服务被困死,因为tomcat线程池里面的工作线程已经被挤占完毕,80此时调用8001,客户端访问响应缓慢。
解决方案
- 超时导致服务器变慢(转圈圈 )-- 超时不在等待
- 出错(宕机或程序运行出错)-- 出错要有兜底方案
解决:对方服务(8001)超时或者宕机了,调用者(80)不能一直卡死等待,必须有服务降级。
服务降级
先从8001服务提供者找问题:设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作为服务降级的fallback
@HystrixCommand
注解使用在资源接口上,
解释:一旦调用服务方法失败并抛出了错误信息后会自动调用@HystrixCommand
标注好的fallbackMethod
调用类中的指定方法
服务提供者服务降级使用方法:
- 在启动类开启hystrix
@EnableCircuitBreaker
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker // 开启hystrix
@MapperScan("com.zh.springcloud.mapper")
public class PaymentHystrixApplication8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixApplication8001.class,args);
}
}
- 在controller 方法上添加
@HystrixCommand
注解
@GetMapping("/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentinfo_timeoutHandler",commandProperties =
// 设置超时条件
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
)
public String paymentinfo_timeout(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_timeout(id);
log.info("====result ->"+result);
return result;
}
// 当paymentinfo_timeout 超时时调用这个方法
private String paymentinfo_timeoutHandler(@PathVariable("id") Integer id){
return "线程:"+Thread.currentThread().getName()+" paymentInfo_timeout,id:"+id+" \t "+"o(╥﹏╥)o";
}
- 设置paymentInfo_timeout方法睡眠5秒,启动8001访问测试发现调用paymentinfo_timeoutHandler方法
经测试得出结论:无论是计算异常还是超时异常,只要paymentinfo_timeout方法不可用,都会做服务降级。兜底的方案都是paymentinfo_timeoutHandler方法
服务调用者客户端服务降级
- 服务调用者客户端服务降级
<!--hystrix 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
- 在配置文件中添加配置
# 开启feign对hystrix的支持
feign:
hystrix:
enabled: true
- 在启动类上添加注解
@EnableHystrix
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableHystrix
public class OrderFeignApplication {
public static void main(String[] args) {
SpringApplication.run(OrderFeignApplication.class,args);
}
}
- 在业务类上写调用者超时规则以及兜底方法
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentinfo_timeoutFallbackMethod", commandProperties =
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
)
public Result<?> paymentinfo_timeout(@PathVariable Integer id){
Result<Object> result = new Result<>();
// 可以直接使用
// int a = 10 / 0;
String str = paymentService.paymentinfo_timeout(id);
result.setResult(str);
return result;
}
public Result<?> paymentinfo_timeoutFallbackMethod(@PathVariable("id") Integer id){
Result<Object> result = new Result<>();
String str ="我是消费者80,对方服务繁忙,请稍后再试!"+id;
result.setResult(str);
return result;
}
- 启动测试,发现当调用者超时1.5秒时就会调用paymentinfo_timeoutFallbackMethod方法
服务熔断
简介:类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
熔断机制概述
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
当检查到该节点微服务调用响应正常后,回复调用链路。
在Spring Cloud 框架里,熔断机制通过Hystrix 实现。Hystrix 会监控微服务间调用的状况。
当失败的调用到一定阈值时,缺省是5秒内50次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand
熔断状态
- 熔断打开
请求不在进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长到达所设时钟则进入半熔断状态 - 熔断关闭
熔断关闭不会对服务进行熔断 - 熔断半开
部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
测试服务熔断
修改cloud-provider-hystrix-payment8001项目
- 新增接口
@GetMapping("/payment/hystrix/circuitBreaker/{id}")
@HystrixCommand(fallbackMethod = "circuitBreaker_fallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60") // 失败率达到多少后跳闸,这里是60%
})
public String circuitBreaker(@PathVariable("id") Integer id){
if (id < 0 ){
throw new RuntimeException("不能小于零");
}
String str = IdUtil.simpleUUID();
return Thread.currentThread().getName() + " \t "+ "调用成功,流水号:"+str;
}
public String circuitBreaker_fallback(@PathVariable("id") Integer id){
return "id 不能为负数,请稍后再试,id:"+id;
}
启动服务访问接口进行测试,多次传入负值,导致服务熔断,在传入正整数,会发现前几次依然是走fallback方法,慢慢的会恢复正常访问
涉及到断路器的三个重要参数: 快照时间窗口,请求总数阈值,错误百分比阈值
- 快照时间窗口:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗口,默认为最近的10秒。
- 请求总数阈值:在快照时间窗内,必须满足请求总数鯯才有资格熔断,默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
- 错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,有15次发生了超时异常,也就是超过了50%的错误百分比,在默认设定50%阈值情况下,这时候就会将断路器打开。
当断路器关闭状态时,一段时间之后(默认是5秒),这时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。
断路器打开之后再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback.通过断路器,实现了自动发现的错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
服务监控HystrixDashboard
简介
除了隔离依赖服务的调用以外,Hystrix 还提供了 准实时的调用监控(Hystrix Dashboard),Hystrix会持续的记录所有通过Hystrix 发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等,Netflix通过Hystrix-metrics-event-stream 项目实现了对以上指标的监控,Spring Cloud 也提供了Hystrix Dashboard 的整合,对监控内容转化成可视化界面。
新建Hystrix 监控项目 cloud-consumer-hystrix-dashboard9001
- 修改pom文件
<dependencies>
<!--hystrix 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
<scope>test</scope>
</dependency>
</dependencies>
- 修改配置文件
server:
port: 9001
- 启动类添加注解
@EnableHystrixDashboard
@SpringBootApplication
@EnableHystrixDashboard // 开启Hystrix可视化页面
public class HystrixDashboard9001Application {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboard9001Application.class,args);
}
}
- 启动测试
修改被监控项目cloud-provider-hystrix-payment8001
- 修改pom文件
<!--被监控项目必须添加此依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 在主启动类里添加Bean
/**
* 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
* ServletRegistrationBean因为SpringBoot的默认路径不是“、hystrix.stream”,
* 只有在自己的项目里配置上下文servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
- 启动测试