前边学习了,服务注册中心:Eureka,consul,服务接口调用:Ribbon,Feign。
本章学习,Spring Cloud的一个重要组件,Hystrix断路器。
一、前言
分布式系统往往,由很多个微服务程序组成,复杂的分布式体系接口中的应用程序个数可能达到数十种,程序互相依赖,协同作业,每个依赖关系在某些时候将不可避免的出现失败。
比如,多个服务之间调用的时候,假设微服务A调用微服务B和C,微服务B和C又调用着其他的微服务,这就是所谓的“扇出”,如果扇出的链路上某个微服务的调用时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的雪崩效应。
对于高流量的应用来说,单一的后端依赖可能会导致所有的服务器上面的资源在几秒钟之内饱和,比失败更糟糕的是这些程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障,这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能够影响到整个应用程序或系统的运行。
为此,spring cloud 的 Hystrix 断路器应运而生,由Hystrix来解决,服务故障过程中的服务降级,服务熔断,以及服务限流。
二、Hystrix 是什么
- Hystrix 是一个用于处理分布式系统的
延迟和容错
的开源库,在分布式系统中,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出现问题的情况下,不会导致整体的服务异常,避免级联故障,以提高分布式的弹性
。 - “断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似于保险丝),
向调用方法返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常
,这样就保证了服务调用方的线程不会长时间的、不必要的被占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。 - 作用:服务降级、服务熔断、接近实时的监控
- 官网:https://github.com/Netflix/Hystrix/wiki/How-To-Use
- 目前已经停止更新:被动修复bugs、不再接受合并请求、不再发布新版本https://github.com/Netflix/Hystrix
三、Hystrix的重要概念
1. 服务降级
就是,如果系统服务出现异常,及时给予友好提示,避免客户端做无用的等待,fallback。
哪些时候会触发服务降级:
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池\信号量打满也会造成服务降级
2. 服务熔断
类似于保险丝达到最大访问以后,直接拒绝访问,拉闸断电,然后调用服务降级方法返回友好提示。
3. 服务限流
秒杀等高并发操作,避免服务一窝蜂过来,导致系统崩溃,进行限流,排队进行,一秒N个。
四、Hystrix实现服务降级 与 熔断
- 首先我们创建eureka注册中心项目:cloud-eureka-server7001,本工程仅仅只是一个服务注册中心,不提供任何借口服务。
pom.xml
<!--eureka server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
application.yml:
server:
port: 7001
#spring:
# application:
# name: cloud-eureka-service #eureka服务注册名称
eureka:
instance:
hostname: eureka7001 #eureka服务注册地址
client:
#false 表示自己就是注册中心,职责就是维护服务实例,不需要去检索服务
fetch-registry: false
#false 表示不想注册中心注册自己
register-with-eureka: false
service-url:
# #设置 eureka server 交互的地址查询服务和注册服务需要依赖的地址
defaultZone: http://eureka7001.com:7001/eureka/
主启动类:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServer7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaServer7001.class,args);
}
}
- 创建服务提供工程:cloud-provider-hystrix-payment8001
pom.xml:需要引入 eureka-client 以及Hystrix 相关依赖。
<!-- 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>
application.yml:
server:
port: 8001
spring:
application:
name: cloud-payment-hystrix-service
eureka:
instance:
hostname: localhost #eureka服务注册地址
instance-id: payment8001 #主机名称 配置过后会在eureka监控页面看到
#配置为 true 可以在网页服务名称下面看到ip地址
prefer-ip-address: true
client:
#是否从eureka Server 抓取已有的注册信息,默认true,单节点无所谓,集群必须为true才能配合ribbon使用负载均衡
fetch-registry: true
#表示是否将自己加入eureka service true代表加入
register-with-eureka: true
service-url:
# #设置 eureka server 交互的地址查询服务和注册服务需要依赖的地址
defaultZone: http://eureka7001.com:7001/eureka
#开启熔断机制
feign:
hystrix:
enabled: true
主启动:
@SpringBootApplication
@EnableHystrix //开启Hystrix
@EnableDiscoveryClient
@EnableEurekaClient
public class HystrixPaymentServiceMain8001 {
public static void main(String[] args) {
SpringApplication.run(HystrixPaymentServiceMain8001.class,args);
}
}
service接口: getInfoError方法 当传入id为负数时抛出异常。
package com.cpown.springcloud.service;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.UUID;
/**
* 服务接口提供类
*/
@Service
public class PayService {
public String getInfo(){
return "服务端口成功:"+ UUID.randomUUID();
}
//服务熔断
@HystrixCommand(fallbackMethod = "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 getInfoError(@PathVariable("id") Long id){
if(id<0){
throw new RuntimeException("服务调用失败 :id 不可以为负数");
}
return "服务端口成功:"+ UUID.randomUUID(); }
/**
* 服务降级回调方法
* @param id
* @return
*/
public String fallback(@PathVariable("id") Long id){
return "id不可以为负数:"+id;
}
}
controller:
package com.cpown.springcloud.controller;
import com.cpown.springcloud.service.PayService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 接口服务访问
*/
@RestController
public class Controller {
@Value("${server.port}")
private String serverport;
@Resource
PayService payService;
@RequestMapping("/get/{id}")
public String getInfo(@PathVariable("id") Long id){
return payService.getInfo();
}
@RequestMapping("/geterr/{id}")
public String getInfo_error(@PathVariable("id") Long id){
return payService.getInfoError(id);
}
}
- 客户端项目:cloud-consumer-histrix-order80
pom.xml:
<!-- 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>
<!-- hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
application.yml:
server:
port: 80
debug: true
spring:
application:
name: cloud-consumer-order-histrix-feign
eureka:
instance:
hostname: localhost #eureka服务注册地址
client:
#是否从eureka Server 抓取已有的注册信息,默认true,单节点无所谓,集群必须为true才能配合ribbon使用负载均衡
fetch-registry: true
#表示是否将自己加入eureka service true代表加入
register-with-eureka: true
service-url:
# #设置 eureka server 交互的地址查询服务和注册服务需要依赖的地址
defaultZone: http://eureka7001.com:7001/eureka
#开启熔断机制
feign:
hystrix:
enabled: true
FeignClient 微服务接口调用类:fallback = PaymentFallbackService.class 此处为服务降级处理类,
package com.cpown.springcloud.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* FeignClient 微服务接口调用类
*/
@FeignClient(value = "cloud-payment-hystrix-service",fallback = PaymentFallbackService.class)
@Component
public interface PaymentService {
@GetMapping("/get/{id}")
String get(@PathVariable("id") Long id);
@GetMapping("/geterr/{id}")
String geterr(@PathVariable("id") Long id);
}
服务降级异常处理类:实现PaymentService 并实现每一个接口的方法,做客户端服务降级处理。
package com.cpown.springcloud.service;
import org.springframework.stereotype.Component;
/**
* 服务降级异常处理类
*/
@Component
public class PaymentFallbackService implements PaymentService{
@Override
public String get(Long id) {
return "服务器异常了:get";
}
@Override
public String geterr(Long id) {
return "服务器异常了:geterr";
}
}
controller:
package com.cpown.springcloud.controller;
import com.cpown.springcloud.dto.CommonResult;
import com.cpown.springcloud.service.PaymentService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class FeignController {
@Resource
private PaymentService paymentService;
/**
* 返回值正确的方法
* @param id
* @return
*/
@GetMapping("/consumer/get/{id}")
public String getInfo(@PathVariable("id") Long id){
return paymentService.get(id);
}
/**
* 有可能会异常的方法
* @param id
* @return
*/
@GetMapping("/consumer/geterr/{id}")
public String geterr(@PathVariable("id") Long id){
return paymentService.geterr(id);
}
}
主启动:
@SpringBootApplication
@EnableHystrix
@EnableDiscoveryClient
@EnableEurekaClient
@EnableFeignClients
public class HystrixOrder80 {
public static void main(String[] args) {
SpringApplication.run(HystrixOrder80.class,args);
}
}
这样我们就完整的搭建了一个服务注册,服务提供,服务调用集群,并对部分接口进行熔断处理。
我们启动项目进行测试:三个项目启动好。
访问:http://localhost/consumer/get/1 是ok的。
访问:http://localhost/consumer/geterr/-1 传递id为负数,服务端8001会报错,并进行服务降级处理:可以看到也没有问题。
下面我们关闭8001服务端:再一次访问接口http://localhost/consumer/geterr/-1
此时服务端服务已经不可用了,会触发消费端80的服务降级:也是没有问题的。
我们重新启动8001服务端:由于我们配置了服务熔断:10秒内 请求达到10次,并且失败率达到60%就会进行服务熔断,停止服务。
我们连续点击多次失败服务:会发现正常的服务也无法提供了,就代表服务已经熔断暂停提供服务了
过一段时间建,服务就会再次恢复:
总结:
//服务熔断
@HystrixCommand(fallbackMethod = "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") //失败率达到多少后跳闸
})
涉及到断路器的三个重要参数:快照时间窗,请求总数阈值,错误百分比阈值。
- 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
- 请求总数阈值:在快照时间窗内,必须满足请求情书阈值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不超过20次,及时所有的请求都超时或者其他原因失败,都不会打开断路器。
- 错误百分比阈值:当请求总数在快照时间内超过了阈值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%错误百分比,在默认50%阈值情况下,短路器会打开。
- 在断路器开启状态:再有请求调用的时候将不会调用主逻辑,直接调用降级方法 fallback,实现了自动发现错误,并将降级逻辑代替主逻辑,减小延迟效果。
- hystrix同时也为我们实现了自动恢复逻辑,当断路器打开,对主逻辑进行熔断之后,hystrix会进入一个休眠窗,在此期间,降级逻辑代替主逻辑。当休眠窗口期一到,断路器进入半开状态,释放一次请求到主逻辑,如果此次请求正常返回,那么断路器将闭合,主逻辑恢复,如果此次请求仍然有问题,断路器继续打开,并重新计算休眠窗。
除此之外,还有一些其他配置: