分布式系统面临的问题:复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某个时候将不可避免的失败。
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,这就是所谓的 “ 扇出 ” 。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A 的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的 “ 雪崩效应 ”。
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。所以,通常当发现一个模块下的某个实例失败后,这时候这个模块依然还会接受流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
要避免这样的级联故障,就需要有一种链路中断的方案:服务降级、服务熔断。
Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,已提高分布式系统的弹性。
“ 断路器 ” 本身是一种开关装置,当某个服务单元发送故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
服务降级
服务熔断
限流、隔离等
接近实时的监控
服务降级 Fallback
不让客户端等待而立刻返回的一个友好提示。例如:服务器忙,请稍后再试。当程序运行异常、超时、服务熔断触发服务降级、线程池 / 信号量打满也会导致服务降级等情况发生。
服务熔断 Break
类似保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。
服务限流 Flowlimit
限制秒杀高并发等操作,严禁一窝蜂的过来拥挤。
创建 cloud-provider-hystrix-payment8001
pom.xml
<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>
<!--web-->
<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>
<!--引入自定义的api通用包,可用使用Payment支付Entity-->
<dependency>
<groupId>com.hangzhou.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</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>
application.yml
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
#defaultZone: http://eureka7001.com:7001/eureka
PaymentHystrixMain8001
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
public static void main(String[] args){
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}
service 层
@Service
public class PaymentService {
public String paymentInfo_OK(Integer id){
return "线程:"+Thread.currentThread().getName()+" paymentInfo_OK:id="+id;
}
public String paymentInfo_Timeout(Integer id){
int timeNumber = 3;
try{
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程:"+Thread.currentThread().getName()+" paymentInfo_Timeout:id="+id+"\t timeout";
}
}
controller 层
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/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(value = "/payment/hystrix/timeout/{id}")
public String paymentInfo_Timeout(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_Timeout(id);
log.info("-----result:"+result);
return result;
}
}
启动服务访问 url
timeout 服务中线程休眠3s也能访问
下载解压完成后在终端中进入 bin 文件夹中输入 sh jmeter 启动
JMeter 界面
修改中文
vim /#相应位置/apache-jmeter-5.0/bin/jmeter.properties
添加 language=zh_CN
创建 module cloud-consumer-feign-hystrix-order80
pom.xml
<dependencies>
<!--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-server</artifactId>
</dependency>
<!--引入自定义的api通用包,可以使用Payment支付Entity-->
<dependency>
<groupId>com.hangzhou.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>
application.yml
server:
port: 80
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
OrderHystrixMain80
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
service 层
@Component
@FeignClient(value = "cloud-provider-hystrix-payment")
public interface PaymentHystrixService {
/**
* 正常访问
*/
@GetMapping("/payment/hystrix/ok/{id}")
String paymentInfo_OK(@PathVariable("id") Integer id);
/**
* 超时访问
*/
@GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
controller 层
@RestController
@Slf4j
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consume/payment/hystrix/ok/{id}")
String paymentInfo_OK(@PathVariable("id") Integer id){
return paymentHystrixService.paymentInfo_OK(id);
}
@GetMapping("/consume/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id){
return paymentHystrixService.paymentInfo_TimeOut(id);
}
}
调用服务
因为 OpenFeign 客户端调用接口等待结果1秒钟,但是服务端处理如果需要超过1秒钟会直接报错所以修改 application.yml
#设置feign 客户端超时时间(openFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
重启后服务调用成功
当多个线程去访问服务8001的时候,消费者80再去访问8001服务的时候消费者要么转圈圈等待要么超时报错。也正是因为有上述故障或不佳表现才有我们的降级/容错/限流等技术诞生。
超时导致服务器变慢(转圈) --> 超时不再等待
出错(宕机或程序运行出错) --> 出错要有兜底
cloud-provider-hystrix-payment8001 service 层
public class PaymentService {
/**
* 正常访问
* @param id
* @return
*/
public String paymentInfo_OK(Integer id){
return "线程:"+Thread.currentThread().getName()+" paymentInfo_OK:id="+id;
}
/**
* http://localhost:8001/payment/hystrix/timeout/1
* @HystrixCommand报异常后如何处理:
* 一旦调用服务方法失败并抛出了错误信息后,
* 会自动调用@HystrixCommand标注好的fallbackMethod调用类中的指定方法
* @param id
* @return
*/
@HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
public String paymentInfo_Timeout(Integer id){
int timeNumber = 5;
// int age = 10/0;
try{
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程:"+Thread.currentThread().getName()+"\tpaymentInfo_Timeout:id="+id+"\t timeout";
}
public String paymentInfo_TimeoutHandler(Integer id){
return "线程:"+Thread.currentThread().getName()+"\t8001系统繁忙或运行出错,paymentInfo_TimeoutHandler:id="+id+"\t 55555";
}
}
主启动类中添加注解 @EnableCircuitBreaker
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
访问相应的 url 显示:线程名称是hystrix的线程,页面并没有返回错误页面,而是执行了fallbackMethod指定的方法
控制台报错
在消费者端访问服务也是执行 fallbackMethod 指定的方法
并且在程序中故意存在10/0错误产生异常,返回的也是fallbackMethod指定的方法,所以当服务不可用了,做服务降级兜底方法都是fallbackMethod 指定的方法。
cloud-consumer-feign-hystrix-order80 pom.xml
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
application.yml
feign:
hystrix:
enabled: true
主启动类前添加注解 @EnableHystrix
@EnableHystrix
public class OrderHystrixMain80 {
controller 层
@RestController
@Slf4j
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consume/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
})
String paymentInfo_TimeOut(@PathVariable("id") Integer id){
return paymentHystrixService.paymentInfo_TimeOut(id);
}
//没有添加 @PathVariable("id") Integer id 会报错
String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
return "我是消费者80,对方payment系统繁忙请10秒种后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
}
当时在想 fallbackMethod 方法中参数是否有其存在的必要然后试了一下不添加参数,访问页面时报错
控制台报错
添加后成功
当然在程序中故意存在10/0错误产生异常,返回的也是fallbackMethod指定的方法
这样如果每个业务方法都对应一个兜底的方法,100个方法就有100个服务降级,会出现代码膨胀问题,我们需要一个统一的 fallbackMethod,将统一和自定义分开。
cloud-consumer-feign-hystrix-order80 service 层
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consume/payment/hystrix/ok/{id}")
String paymentInfo_OK(@PathVariable("id") Integer id){
return paymentHystrixService.paymentInfo_OK(id);
}
@GetMapping("/consume/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
})
@HystrixCommand
String paymentInfo_TimeOut(@PathVariable("id") Integer id){
return paymentHystrixService.paymentInfo_TimeOut(id);
}
String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){
return "我是消费者80,对方payment系统繁忙请10秒种后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
/**
* 全局fallback方法
* @return
*/
String payment_Global_FallbackMethod(){
return "Global 处理异常信息请稍后再试...";
}
}
80端是在 service 层通过 OpenFeign 调用8001端的方法,而现在 fallback 方法写在 controller 层,针对客户端接口做一些处理,把它调用的所有微服务方法进行降级,就可以解决业务逻辑混在一起导致混乱的问题。这个案例服务降级处理是在客户端80完成的,与服务端8001没有关系,只需要为 Feign 客户端定义的接口添加一个服务降级处理的实现类即可实现解耦。根据 cloud-consumer-feign-hystrix-order80 已经有的 PaymentHystrixService 接口,重新新建一个类 PaymentFallbackService 实现接口,统一为接口里面的方法进行异常处理。
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
/**
* 正常访问
*/
@GetMapping("/payment/hystrix/ok/{id}")
String paymentInfo_OK(@PathVariable("id") Integer id);
/**
* 超时访问
*/
@GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
PaymentFallbackService
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "-----PaymentFallbackService fall back:paymentInfo_OK";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "-----PaymentFallbackService fall back:paymentInfo_TimeOut";
}
}
application.yml 在 feign 中开启 hystrix
feign:
hystrix:
enabled: true
在 PaymentHystrixService 接口中的注解中注入实现类
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。会自动去检测到该节点微服务调用响应正常后,恢复调用链路。
在 SpringCloud 框架里,熔断机制通过 Hystrix 实现,Hystrix 会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是 @HystrixCommand。
8001端service 层添加方法
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_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"),
})
public String paymentCircuitBreaker(@PathParam("id") Integer id){
if(id < 0){
throw new RuntimeException("RuntimeException: id 不能为负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName()+"\t调用成功,流水号:"+serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathParam("id") Integer id){
return "id 不能为负数,请稍后再试...id:"+id;
}
参数类参考 HystrixCommandProperties
8001端 controller 层添加方法
@GetMapping(value = "/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
String result = paymentService.paymentCircuitBreaker(id);
log.info("-----result:"+result);
return result;
}
熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态
熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
熔断关闭:熔断关闭不会对服务进行调用
设计到断路器的三个重要参数:快照时间窗、请求总数阈值、错误百分比阈值
快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒
请求总数阈值:在快照时间内,必须满足请求总数阈值才有资格熔断。默认为20,意味着在10秒内,如果该 hystrix 命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开
错误百分比阈值:当请求总数在快照时间窗内超过阈值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阈值情况下,这时候就会将断路器打开
除了隔离依赖服务的调用意外,Hystrix 还提供了准实时的调用监控(Hystrix Dashboard),Hystrix 会持续的记录所有通过 Hystrix 发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix 通过 hystrix-metrics-event-stream 项目实现了对以上指标的监控。SpringCloud 也提供了 Hystrix Dashboard 的整合,对监控内容转化成可视化界面。
创建 module cloud-consumer-hystrix-dashboard9001
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</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>
application.yml
server:
port: 9001
HystrixDashboardMain9001
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args){
SpringApplication.run(HystrixDashboardMain9001.class,args);
}
}
所有 provider 微服务提供类都需要监控依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
启动 9001 微服务访问 localhost:9001/hystrix,出现下面图标搭建成功
新版本 Hystrix 需要在服务端主启动类中指定监控路径
PaymentHystrixMain8001
/**
* 此配置是为了服务监控而配置,与服务容错本身无关,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;
}
输入地址
访问 http://localhost:8001/payment/circuit/1
多次访问 http://localhost:8001/payment/circuit/-1 导致服务熔断