分布式系统面临的问题:
服务雪崩
多个微服务之间调用的时候,假如微服务 A 调用微服务 B 和微服务 C ,微服务 B 和微服务 C 又调用其它的微服务,这就是所谓的
扇出
。如果扇出
的链路上某个微服务的调用时间过长或者不可用,对微服务 A 的调用就会占用越来越多的系统资源,进而引起系统崩溃,产生所谓的雪崩效应
。对于高流量的应用来说,单一的后端依赖可能会导致所有的服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其它系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然会接收流量,然后这个有问题的模块还调用了其它模块,这样就会发生级联故障,或者叫雪崩。
Hystrix 是什么
Hystrix 是一个用于处理分布式系统的
延迟
和容错
的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等。Hystrix 能够保证在一个依赖出现问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
断路器
本身是一种开关装置,当某个服务单元发生故障后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的预备响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,
这样就保证了服务调用方的线程不会被长时间、不必要的占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
服务降级(fallback)
服务繁忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback;
那些情况会触发服务降级:
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
服务熔断(break)
类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示;
服务的降级 --> 进而熔断 --> 恢复链路调用
服务限流(flowlimit)
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
注册中心沿用之前的 Eureka
引入 pom 依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
创建 yml 配置文件,
application-one.yml
与application-two.yml
由于两个配置几乎相同,我们这里只给出application-one.yml
的全部配置与application-two.yml
的不同部分的配置。
application-one.yml
server:
port: 8015
spring:
application:
name: hystrix-provider-service
security:
# 配置spring security登录用户名和密码
user:
name: akieay
password: 1qaz2wsx
eureka:
client:
#表示是否将自己注册进 Eureka Server服务 默认为true
register-with-eureka: true
#f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
# defaultZone: http://localhost:7001/eureka
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
instance:
instance-id: hystrix-provider-8015
## 当调用getHostname获取实例的hostname时,返回ip而不是host名
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
lease-renewal-interval-in-seconds: 10
# Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
lease-expiration-duration-in-seconds: 30
application-two.yml
server:
port: 8016
eureka:
instance:
instance-id: hystrix-provider-8016
主启动
@SpringBootApplication
@EnableEurekaClient
public class HystrixProviderApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixProviderApplication.class, args);
}
}
业务类,为了方便测试我们这里就不连接数据库了,之间模拟了两个请求处理逻辑。
@Service
public class PaymentService {
/**
* 模拟简单业务逻辑
* @param id
* @return
*/
public String paymentInfo_OK(Integer id) {
return "线程池: " + Thread.currentThread().getName() + " paymentInfo_OK, id: " + id + "\t" + "(*^_^*)哈哈~";
}
/**
* 模拟复杂的业务逻辑,处理时间 > 3秒
* @param id
* @return
*/
public String paymentInfo_TimeOut(Integer id) {
int timeOut = 3;
try {
TimeUnit.SECONDS.sleep(timeOut);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池: " + Thread.currentThread().getName() + " paymentInfo_TimeOut, id: " + id + "\t" + "(*^_^*)哈哈~" + "耗时(秒): " + timeOut;
}
}
@RestController
@Slf4j
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@Resource
PaymentService paymentService;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfo_OK(id);
log.info("*******result: " + result + " 端口: " + serverPort);
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 + " 端口: " + serverPort);
return result;
}
}
创建两个启动服务,创建的步骤在前面的模块都有介绍就不介绍细节了;创建完成后启动这两个服务,即可在注册中心中看到这两个服务
引入 pom 依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
添加 application.yml 配置文件
server:
port: 80
spring:
application:
name: hystrix-concumer-service
security:
# 配置spring security登录用户名和密码
user:
name: akieay
password: 1qaz2wsx
eureka:
client:
#表示是否将自己注册进 Eureka Server服务 默认为true
register-with-eureka: true
#f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
# defaultZone: http://localhost:7001/eureka
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
instance:
instance-id: hystrix-concumer-80
## 当调用getHostname获取实例的hostname时,返回ip而不是host名
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
lease-renewal-interval-in-seconds: 10
# Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
lease-expiration-duration-in-seconds: 30
logging:
level:
#feign日志以什么级别监控哪个接口
com.akieay.cloud.hystrix.concumer.service.*: debug
# 设置服务的 ribbon 配置
HYSTRIX-PROVIDER-SERVICE:
ribbon:
ConnectTimeout: 5000 #服务请求连接超时时间(毫秒)
ReadTimeout: 5000 #服务请求处理超时时间(毫秒)
OkToRetryOnAllOperations: true #对超时请求启用重试机制
MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
MaxAutoRetries: 1 # 切换实例后重试最大次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法
主启动
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class HystrixConcumerApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixConcumerApplication.class, args);
}
}
业务类
@FeignClient(name = "HYSTRIX-PROVIDER-SERVICE")
public interface PaymentService {
/**
* 模拟简单业务逻辑
* @param id
* @return
*/
@GetMapping("/payment/hystrix/ok/{id}")
String paymentInfo_OK(@PathVariable("id") Integer id);
/**
* 模拟复杂业务逻辑 > 3秒处理时间
* @param id
* @return
*/
@GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
@RestController
@Slf4j
public class OrderController {
@Resource
private PaymentService paymentService;
@GetMapping("/consumer/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfo_OK(id);
return result;
}
@GetMapping("/consumer/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfo_TimeOut(id);
return result;
}
}
启动服务,即可在 Eureka 中看到该服务已注册成功,并且调用:http://localhost/consumer/hystrix/ok/13 与 http://localhost/consumer/hystrix/timeout/13 发下都能正常调用,而且是采用我们自己指定的
随机
负载均衡策略来调用 8015 与 8016 的,如下图:
至此,我们的基础演示模块已经建立完成,后续的关于 服务降级、服务熔断、服务限流 的演示都将基于这个模块修改并演示。
服务降级–超时
设置自身服务调用的超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作为服务降级 fallback。
- 主启动添加
@EnableCircuitBreaker
,表示开启基于注解的服务断路器;- 修改 paymentInfo_TimeOut 方法,使其延时 5 秒,并添加
@HystrixCommand
注解开启断路器。
/**
* 模拟复杂的业务逻辑,处理时间 > 3秒
* @param id
* @return
*/
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutFallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentInfo_TimeOut(Integer id) {
int timeOut = 5;
try {
TimeUnit.SECONDS.sleep(timeOut);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池: " + Thread.currentThread().getName() + " paymentInfo_TimeOut, id: " + id + "\t" + "(*^_^*)哈哈~" + "耗时(秒): " + timeOut;
}
/**
* 服务降级回调方法
* @param id
* @return
*/
public String paymentInfo_TimeOutFallback(Integer id) {
return "线程池: " + Thread.currentThread().getName() + "系统繁忙或运行报错,请稍后再试, id: " + id + "\t" + "/(ㄒoㄒ)/~~";
}
- fallbackMethod:标名发生服务降级后的回调方法名称 fallback
- commandProperties: 表示触发条件,
- execution.isolation.thread.timeoutInMilliseconds:表示该方法调用的最大时长(单位:毫秒),操作该时间就会发生服务降级,调用回调方法
以上的配置指定:当方法运行时间超过 3 秒时,会触发服务降级;回调
paymentInfo_TimeOutFallback
返回友好提示。
重启服务提供者,浏览器访问:http://localhost:8015/payment/hystrix/timeout/13 ,当服务调用超时时会触发服务降级,回调我们指定的处理方法返回友好提示。
服务降级–异常
修改 paymentInfo_TimeOut 方法,故意制造一个异常。
public String paymentInfo_TimeOut(Integer id) {
// int timeOut = 5;
// try {
// TimeUnit.SECONDS.sleep(timeOut);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
int age = 10 / 0;
return "线程池: " + Thread.currentThread().getName() + " paymentInfo_TimeOut, id: " + id + "\t" + "(*^_^*)哈哈~" + "耗时(秒): ";
}
重启服务提供者,浏览器访问:http://localhost:8015/payment/hystrix/timeout/13 ,当服务调用发生异常时也会触发服务降级,回调我们指定的处理方法返回友好提示。
综上:当服务调用超时或者发生异常等照成
服务不可用
时,会触发服务降级,调用我们指定的兜底方法进行处理,并返回响应信息【前面介绍了服务降级的4种触发情况,这里就先演示这两种了。】;
为了测试消费端的服务降级,首先需要将服务端恢复正常调用;如下:修改 paymentInfo_TimeOut 将接口调用的最大超时时间改为 5 秒,且接口业务延时 3秒;
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutFallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
})
public String paymentInfo_TimeOut(Integer id) {
int timeOut = 3;
try {
TimeUnit.SECONDS.sleep(timeOut);
} catch (InterruptedException e) {
e.printStackTrace();
}
// int age = 10 / 0;
return "线程池: " + Thread.currentThread().getName() + " paymentInfo_TimeOut, id: " + id + "\t" + "(*^_^*)哈哈~" + "耗时(秒): ";
}
浏览器访问:http://localhost:8015/payment/hystrix/timeout/13 ,可以看到现在能够正常访问。
修改服务消费者,主要修改 yml 配置文件 与 OrderController 的 paymentInfo_TimeOut 方法
# 开启feign对hystrix的支持
feign:
hystrix:
enabled: true
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutFallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
})
@GetMapping("/consumer/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfo_TimeOut(id);
return result;
}
/**
* 降级回调
* @param id
* @return
*/
public String paymentInfo_TimeOutFallback(Integer id) {
return "我是消费者80,对方支付服务繁忙,请稍后再试;或自身运行出错请检查自己,/(ㄒoㄒ)/~~";
}
重启服务,访问:http://localhost/consumer/hystrix/timeout/15 ,即可发现:由于 服务提供者的方法调用需要 3 秒,而 paymentInfo_TimeOut 支持的最大超时时间为 1.5 秒,服务调用超时,触发服务降级,调用服务降级回调方法返回响应;如下图:
与服务提供者一样,我们测试了超时再测试下异常的处理;主要修改如下:修改服务的超时时间为 4 秒避免产生超时,并模拟服务调用中产生的异常;
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutFallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "4000")
})
@GetMapping("/consumer/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
int a = 10 / 0;
String result = paymentService.paymentInfo_TimeOut(id);
return result;
}
重启服务,并访问:http://localhost/consumer/hystrix/timeout/15 ,可以看到立刻就返回了 服务降级回调方法的提示:
上面的
@HystrixCommand
虽然能够处理服务降级,但是如果多个接口都采用同一种服务降级的处理逻辑,在每个接口上都写一遍@HystrixCommand
的所有配置明显是不合理的;这时我们可以使用 ‘@DefaultProperties’ 来指定全局通用的降级处理逻辑;
修改 OrderController 添加
@DefaultProperties
来指定默认降级回调方法。
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_Fallback")
public class OrderController {
@Resource
private PaymentService paymentService;
@GetMapping("/consumer/hystrix/ok/{id}")
@HystrixCommand
public String paymentInfo_OK(@PathVariable("id") Integer id) {
int a = 10 / 0;
String result = paymentService.paymentInfo_OK(id);
return result;
}
@GetMapping("/consumer/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutFallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "4000")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
int a = 10 / 0;
String result = paymentService.paymentInfo_TimeOut(id);
return result;
}
/**
* 降级回调
* @param id
* @return
*/
public String paymentInfo_TimeOutFallback(Integer id) {
return "我是消费者80,对方支付服务繁忙,请稍后再试;或自身运行出错请检查自己,/(ㄒoㄒ)/~~";
}
public String payment_Global_Fallback() {
return "payment 支付服务通用的服务降级回调方法,/(ㄒoㄒ)/~~ 失败了";
}
}
以上:
defaultFallback
指定的payment_Global_Fallback
方法为默认的服务降级回调方法,凡事添加了@HystrixCommand
并且没指定fallbackMethod
的都将采用默认的回调方法,如上面的paymentInfo_OK
方法;
测试,重启服务,并且访问:http://localhost/consumer/hystrix/ok/15 与 http://localhost/consumer/hystrix/timeout/15 对比响应结果;
注意:
没有添加 @HystrixCommand 注解的方法是不会触发服务降级并回调兜底方法的,只有添加了注解且未指定回调方法的业务才会使用默认的回调方法处理。
若是指定的自己的 fallback 方法,则会调用自己的 fallback 方法。同样全局服务降级也可以指定触发条件:如下面的
@DefaultProperties(defaultFallback = "payment_Global_Fallback", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000") })
以上提供的服务降级的处理方法虽然都能实现我们想要的处理,但是也带来了两个新的问题:每个方法配置一个会引起代码膨胀【虽然有默认降级方法处理,但是针对同一个服务提供者的服务调用多次这种情况又显得不足】,以及服务降级的处理方法与业务逻辑混合在一起会造成代码的混乱【耦合】;针对这些情况,我们下面就来处理这些问题。
在@FeignClient 上 声明服务降级处理类( fallback)
@FeignClient(name = "HYSTRIX-PROVIDER-SERVICE", fallback = PaymentFallbackService.class)
public interface PaymentService {
/**
* 模拟简单业务逻辑
* @param id
* @return
*/
@GetMapping("/payment/hystrix/ok/{id}")
String paymentInfo_OK(@PathVariable("id") Integer id);
/**
* 模拟复杂业务逻辑 > 3秒处理时间
* @param id
* @return
*/
@GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
PaymentFallbackService 实现了 PaymentService,其实现的方法即为 服务降级调用的回调方法
@Component
public class PaymentFallbackService implements PaymentService {
/**
* paymentInfo_OK 方法的降级处理方法
* @param id
* @return
*/
@Override
public String paymentInfo_OK(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_OK;≡(▔﹏▔)≡";
}
/**
* paymentInfo_TimeOut 的降级处理方法
* @param id
* @return
*/
@Override
public String paymentInfo_TimeOut(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_TimeOut;≡(▔﹏▔)≡";
}
}
以上:创建 PaymentFallbackService 继承 PaymentService;使用
@FeignClient
注解的fallback
属性将 PaymentFallbackService 指定为 PaymentService 的 服务降级fallback 的处理类,PaymentFallbackService 其中 每个实现方法都是对应的 PaymentService 中的接口的 fallback 处理逻辑;例如:PaymentFallbackService 中的 paymentInfo_OK 方法就是 PaymentService 中 paymentInfo_OK 发生服务降级时的 fallback 处理方法。
另外注意修改 yml 配置,设置HystrixCommand执行的超时时间【默认1秒】,将其改为 5 秒,否则我们的 paymentInfo_TimeOut 由于其调用时长 > 1秒 将一直处于超时状态。
hystrix:
command: #用于控制HystrixCommand的行为
default:
execution:
isolation:
strategy: THREAD #控制HystrixCommand的隔离策略,THREAD->线程池隔离策略(默认),SEMAPHORE->信号量隔离策略
thread:
timeoutInMilliseconds: 5000 #配置HystrixCommand执行的超时时间,执行超过该时间会进行服务降级处理
关闭服务提供者端的服务降级处理并重启服务,将 OrderController 中模拟的异常删除,并重启服务访问:
http://localhost/consumer/hystrix/ok/15 于 http://localhost/consumer/hystrix/timeout/1 服务能正常调用。
关掉 服务提供者模块,重启服务消费者,再次调用:http://localhost/consumer/hystrix/ok/15 于 http://localhost/consumer/hystrix/timeout/1 此时将返回服务降级的回调处理的信息;
这种方法主要用来处理 服务提供者 崩溃了【挂了】,时的回调处理;注意:若是同时在服务方法上指定了
@HystrixCommand
的回调方法 并且在@FeignClient
指定了回调方法;当服务提供者没宕机时,服务异常或超时触发的是@HystrixCommand
指定的回调方法,当服务提供者宕机了或崩溃了时触发的是FeignClient
指定的回调方法【至少我在测试时是这种现象】。
特别注意:
FeignClient 中指定的降级处理主要是用于处理服务提供方不能正常提供服务时的降级处理;如服务提供方发生:异常、调用超时、服务降级等;FeignClient 不会处理服务消费方的问题,如:异常,服务调用超时等,这些需要在服务消费方的方法上指定 @HystrixCommand 来处理。
类比保险丝,达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法返回友好提示。
服务的降级 -> 进而熔断 -> 回复调用链路
熔断机制是应对 雪崩效应 的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
在 SpringCloud 框架里,熔断机制通过 Hystrix 实现。Hystrix 会监控微服务间调用状况,当失败的调用达到一定阈值,缺省为 5 秒 20 次调用失败,就会启动熔断机制,熔断机制的注解也是
@HystrixCommand
。
涉及到断路器的几个重要参数
fallbackMethod:为服务降级回调方法
commandProperties: 表示触发条件
- enabled:表示是否开启断路器功能
- sleepWindowInMilliseconds:
快照时间窗
,断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗
,默认是最近的10秒。- requestVolumeThreshold:
请求量阈值
,在快照时间窗内,必须满足请求量达到指定阈值才有资格熔断,默认为 20 ;意味着在指定的时间窗内,如果请求量达不到指定的阈值,即使请求都超时或者其它原因失败,断路器都不会打开。- errorThresholdPercentage:
错误百分比阈值
【默认50%】:当请求量
在快照时间窗
内超过了阈值,并且异常比例
达到错误百分比阈值
,则触发服务降级
;比如:如果 10 内秒发生了 30 次调用,并且在这 30 次调用中,有 15 次发生了超时异常,也就是达到 50% 的错误率,在默认设定 50% 错误阈值的情况下,这时候将会打开断路器,进入服务熔断。
修改 OrderController 增加服务熔断的方法
@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"), //失败率达到多少后跳闸
})
@GetMapping("/consumer/payment/circuitBreaker/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
if (id < 0) {
throw new RuntimeException("*********id 不能为负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(Integer id) {
return "id 不能为负数,请稍后再试,≡(▔﹏▔)≡ id: " + id;
}
以上配置表示:当在 10 秒内,请求次数达到 10 次以上,且 异常比例达到 60% 时,断路器开启,触发服务熔断;
重启服务,分别访问:http://localhost/consumer/payment/circuitBreaker/15 和 http://localhost/consumer/payment/circuitBreaker/-15 可以看到 参数为 15 时访问正常,参数为 -15 时抛出我们定义的异常 触发服务降级 调用回调方法返回提示;
我们疯狂访问: http://localhost/consumer/payment/circuitBreaker/-15 直到达到服务熔断的条件【10秒内访问10次 且 错误次数为 60% 以上】触发服务熔断,这时我们访问:http://localhost/consumer/payment/circuitBreaker/15 结果如下:
可以看到 本来我们正常的服务 在发生服务熔断后,请求将直接触发服务降级返回 fallback 提示,并不会执行方法体;我们继续调用 http://localhost/consumer/payment/circuitBreaker/15 在大概 5 秒后,可以发现服务恢复正常调用,如下图:
熔断流程
- 1】当满足一定的阈值的时候(默认 10 秒内超过 20 个请求次数)
- 2】当失败率达到一定的时候(默认 10 秒内超过 50% 的请求失败)
- 3】到达以上阈值,断路器将会开启,进而熔断服务
- 4】当开启的时候,所有请求都不会进行转发,直接失败返回 fallback 的响应
- 5】一段时间之后(默认是 5 秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。重复 4 和 5。
两个问题:
断路器打开后会怎样?
断路器打开之后,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级 fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟效果。
原来的主逻辑要如何恢复呢?
对于这一问题,hystrix 也为我们实现了自动恢复功能。当断路器打开,对主逻辑进行熔断之后,hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑;当休眠时间窗到期,断路器将会进入半开状态,释放一次请求到原来的主逻辑上;如果此次请求正常返回,那么断路器将闭合,主逻辑恢复;如果这次请求依旧有问题,断路器继续进入打开状态,休眠时间窗重新计时。
图解:
熔断状态
官网断路器流程图
蓝色:调用路径 红色:返回路径 绿色:是步骤 |:是执行完成 X:执行是错误
官方介绍:
个人理解:
- 第一步:构造一个 HystrixCommand 或 HystrixObservableCommand;
- 第二步:选择上述方法之一来获得命令的结果,HystrixCommand 命令有两个方法
.execute()
和.queue()
,HystrixObservableCommand 也有两个方法.observe()
和.toObservable()
;- 第三步:查看是否启动缓存,若是启用了缓存,有缓存并且命中了,缓存结果将会立刻的回应;
- 第四步:检查断路器是否处于打开状态,若是处于打开状态,进入
第八步
;- 第五步:检查系统是否存在可用的资源来执行我们的命令【信号量,线程池】,若是资源【信号量,线程池】满了,则直接进入
第八步
;- 第六步:执行构造方法或 run 方法,执行业务逻辑;若是执行失败或超时,则进入
第八步
,若是执行成功,则进入第九步
; 并异步执行第七步
汇报业务执行情况;- 第七步:无论业务执行成功或失败,业务的执行结果【成功/失败】都将会汇报给断路器,断路器则根据这些数据决定是否打开或关闭断路器。
- 第八步:执行 fallback 方法,放回服务降级处理结果。
- 第九步:服务调用成功,返回响应结果。
在微服务架构中我们服务端的请求承载量是有限的,我们希望在客户端请求时,限制请求的并发数,防止请求数量过大对服务端产生压力。
相关参数:
commandProperties = { //当隔离策略选择信号池格式的时候,用来设置信号池的大小(最大并发数) @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10") } threadPoolProperties = { //该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量。 @HystrixProperty(name = "coreSize", value = "10"), //该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列, // 否则使用 LinkedBlockingQueue 实现的队列。 @HystrixProperty(name = "maxQueueSize", value = "-1"), //该参数用来为队列设置拒绝阈值。通过该参数,即使队列没有达到最大值也能拒绝请求。该参数主要是对 //LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue 队列不能动态修改它的对象大小,而通过 //该属性就可以调整拒绝请求的队列大小了。 @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5") }
我们可以通过
semaphore.maxConcurrentRequests
、coreSize
、maxQueueSize
、ueueSizeRejectionThreshold
来设置限流策略。
服务限流–线程池
修改服务消费者的 OrderController 添加服务限流的演示方法
@HystrixCommand( fallbackMethod = "limitFallback", threadPoolKey = "limit",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "2"), //执行命令线程池的核心线程数
@HystrixProperty(name = "maxQueueSize", value = "1") //线程池的最大队列大小
})
@GetMapping("/consumer/test/limit")
public String testLimit() {
String serialNumber = IdUtil.simpleUUID();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return Thread.currentThread().getName() + "\t" + "限流服务调用成功,流水号: " + serialNumber;
}
threadPoolKey
是线程池唯一标识, hystrix 会拿你这个标识去计数,看线程占用是否超过设置的阈值, 超过了就会直接降级该次调用;比如,以上的配置表示:该方法执行的核心线程为 2 个,并且方法执行时间为 3 秒;这就意味着:当 3 秒内有超过 2 个请求进来的话,剩下的请求则全部降级。重启服务消费者,访问:http://localhost/consumer/test/limit ,当请求频率超过 3 秒 2次时,即会触发服务降级,如下图:
服务限流–信号量
修改服务消费者的 OrderController 添加信号量服务限流的演示方法
@HystrixCommand( fallbackMethod = "limitFallback", commandKey = "limit", commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE"),
@HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "4") //信号池的大小
})
@GetMapping("/consumer/semaphore/limit")
public String testLimitSemaphore() {
String serialNumber = IdUtil.simpleUUID();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info(serialNumber);
return Thread.currentThread().getName() + "\t" + "Semaphore 限流服务调用成功,流水号: " + serialNumber;
}
以上配置表示:同时只允许有 4 个 tomcat 线程来访问服务 A,其它的请求都会被拒绝限流;
这里我们需要使用
jmeter
作为压测工具;
以上我们使用
jmeter
配置了 1 秒 3个请求访问 我们的服务,由于我们设置的是 只能同时处理 4个请求,并且每个服务的执行时间都大于 1 秒;这是我们通过浏览器访问:http://localhost/consumer/semaphore/limit 当频率为 1 秒一个请求时,可以发现请求正常;当我们疯狂点击时,由于请求 1 秒中可能超过了 4 个 多余的请求将限流降级,返回 fallback 的提示信息,如下图:
适用场景:
@HystrixCommand(
fallbackMethod = "指定服务降级处理方法",
groupKey = "分组名称,Hystrix会根据不同的分组来统计命令的告警及仪表盘信息",
commandKey = "命令名称,用于区分不同的命令",
threadPoolKey = "线程池名称,用于划分线程池",
//忽略某些异常,不发生服务降级
ignoreExceptions = Exception.class,
commandProperties = {
//设置隔离策略,THREAD:表示线程池,SEMAPHORE:信号池隔离
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
//当隔离策略选择信号池格式的时候,用来设置信号池的大小(最大并发数)
@HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
//配置命令执行的超时时间
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "10"),
//执行超时的时候是否中断
@HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"),
//执行被取消的时候是否中断
@HystrixProperty(name = "execution.isolation.thread.interruptOnFutureCancel", value = "true"),
//允许回调方法执行的最大并发数
@HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"),
//服务降级是否启动,是否执行回调函数
@HystrixProperty(name = "fallback.enabled", value = "true"),
//是否启动超时时间
@HystrixProperty(name = "execution.timeout.enabled", value = "true"),
//是否启用断路器
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
//该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,
// 如果滚动时间窗(默认10秒)内仅收到 19 个请求,即便这 19 个请求都失败了,断路器也不会打开。
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
//该属性用来设置当断路器打开之后的休眠时间窗。休眠时间窗结束之后,会将断路器设置为 "半开" 状态,
//尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态,如果成功就设置为 "关闭" 状态。
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000"),
//该属性用来设置异常比例,表示在滚动时间窗中,在请求数量超过 `circuitBreaker.requestVolumeThreshold`
//的情况下,如果错误请求的百分比超过 50%,就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
//断路器强制打开
@HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
//断路器强制关闭
@HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),
//滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "10000"),
//该属性用来设置滚动时间窗统计指标信息时划分 "桶" 的数量,断路器在收集指标信息的时候会根据设置的
// 时间窗长度拆分成多个 "桶" 来累计各度量值,每个 "桶" 记录了一段时间内的采集指标。比如:10 秒内
// 拆分成 10 个 "桶" 收集采样,所以 timeInMilliseconds 必须被 numBuckets 整除,否则会抛出异常。
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
//该属性用来设置对命令执行的延迟是否采用百分位数来跟踪和计算,如果设置为 false,
// 那么所有的概要统计都将返回 -1.
@HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
//该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
@HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
//该属性用来设置百分位统计滚动窗口中使用 "桶" 的数量。
@HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
//该属性用来设置在执行过程中每个 "桶" 中保留的最大执行次数。如果在滚动时间窗内发生过超过该设定值的
// 执行次数,就从最初的位置开始重写。例如:将该值设置为 100,滚动窗口为 10 秒,若在 10 秒内一个 "桶"
// 中发生了 500 次执行,那么该 "桶" 中只保留最后的 100 次执行的统计。另外,增加该值的大小将会增加内存
// 的消耗,并增加排序百分位数所需的计算时间。
@HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
//该属性用来设置采集影响断路器状态的健康快照(请求的成功,错误百分比)的间隔等待时间。
@HystrixProperty(name = "metrics.healthSnapshot.intervalInMilliseconds", value = "500"),
//是否开启请求缓存
@HystrixProperty(name = "requestCache.enabled", value = "true"),
//HystrixCommand 的执行和事件是否打印日志到 HystrixRequestLog 中
@HystrixProperty(name = "requestLog.enabled", value = "true")
},
threadPoolProperties = {
//该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量。
@HystrixProperty(name = "coreSize", value = "10"),
//该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列,
// 否则使用 LinkedBlockingQueue 实现的队列。
@HystrixProperty(name = "maxQueueSize", value = "-1"),
//该参数用来为队列设置拒绝阈值。通过该参数,即使队列没有达到最大值也能拒绝请求。该参数主要是对
//LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue 队列不能动态修改它的对象大小,而通过
//该属性就可以调整拒绝请求的队列大小了。
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "5")
}
)
hystrix:
command: #用于控制HystrixCommand的行为
default:
execution:
isolation:
strategy: THREAD #控制HystrixCommand的隔离策略,THREAD->线程池隔离策略(默认),SEMAPHORE->信号量隔离策略
thread:
timeoutInMilliseconds: 1000 #配置HystrixCommand执行的超时时间,执行超过该时间会进行服务降级处理
interruptOnTimeout: true #配置HystrixCommand执行超时的时候是否要中断
interruptOnCancel: true #配置HystrixCommand执行被取消的时候是否要中断
timeout:
enabled: true #配置HystrixCommand的执行是否启用超时时间
semaphore:
maxConcurrentRequests: 10 #当使用信号量隔离策略时,用来控制并发量的大小,超过该并发量的请求会被拒绝
fallback:
enabled: true #用于控制是否启用服务降级
circuitBreaker: #用于控制HystrixCircuitBreaker的行为
enabled: true #用于控制断路器是否跟踪健康状况以及熔断请求
requestVolumeThreshold: 20 #超过该请求数的请求会被拒绝
forceOpen: false #强制打开断路器,拒绝所有请求
forceClosed: false #强制关闭断路器,接收所有请求
requestCache:
enabled: true #用于控制是否开启请求缓存
collapser: #用于控制HystrixCollapser的执行行为
default:
maxRequestsInBatch: 100 #控制一次合并请求合并的最大请求数
timerDelayinMilliseconds: 10 #控制多少毫秒内的请求会被合并成一个
requestCache:
enabled: true #控制合并请求是否开启缓存
threadpool: #用于控制HystrixCommand执行所在线程池的行为
default:
coreSize: 10 #线程池的核心线程数
maximumSize: 10 #线程池的最大线程数,超过该线程数的请求会被拒绝
maxQueueSize: -1 #用于设置线程池的最大队列大小,-1采用SynchronousQueue,其他正数采用LinkedBlockingQueue
queueSizeRejectionThreshold: 5 #用于设置线程池队列的拒绝阀值,由于LinkedBlockingQueue不能动态改版大小,使用时需要用该参数来控制线程数
除了隔离依赖服务的调用以外,Hystrix 还提供了
准实时的调用监控(hystrix Dashboard)
。Hystrix 会持续的记录所有通过 Hystrix 发起的请求的执行信息,并以统计报表和图形化的形式展现给用户,包含每秒执行多少次、请求多少成功,多少失败等。 Netflix 通过 hystrix-metrics-event-stream 项目实现了对以上指标的监控。Spring Cloud 也提供了 Hystrix Dashboard 的整合,对监控内容转化为可视化界面。
引入 pom 依赖,注意:
spring-boot-starter-actuator 包是必须的,所有被监控的微服务也必须引入该jar包。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
添加 yml 配置
server:
port: 9001
spring:
application:
name: hystrix-dashboard-service
security:
# 配置spring security登录用户名和密码
user:
name: akieay
password: 1qaz2wsx
eureka:
client:
#表示是否将自己注册进 Eureka Server服务 默认为true
register-with-eureka: true
#f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
# defaultZone: http://localhost:7001/eureka
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
instance:
instance-id: hystrix-dashboard-9001
## 当调用getHostname获取实例的hostname时,返回ip而不是host名
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
lease-renewal-interval-in-seconds: 10
# Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
lease-expiration-duration-in-seconds: 30
hystrix:
dashboard:
proxy-stream-allow-list: "*"
主启动
@SpringBootApplication
@EnableHystrixDashboard
@EnableEurekaClient
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
启动服务,访问:http://localhost:9001/hystrix
至此 hystrix-dashboard 监控平台已经搭建好。
这里我们已服务消费者为例;在消费者启动类中 添加下面实例:
/**
* 此配置是为了服务监控而配置,与服务容错本身无关,spring cloud 升级后的坑
* ServletRegistrationBean 因为 spring boot的默认路径不是 "/hystrix.stream",
* 只要在自己的项目里配置上下面的servlet就可以了
* @return
*/
@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;
}
注意:以上的配置必须加载被监控的服务中,否则监控服务会出现如下报错;
重启服务消费者,并在 hystrix-bashboard 中输入被监控的服务,如下图:
进入监控界面
由于现在还没有请求,所以显示加载中;多次访问:http://localhost/consumer/payment/circuitBreaker/14 与 http://localhost/consumer/payment/circuitBreaker/-14 即可在控制台看到监控信息。
以下为正常状态下的监控信息:
以下为服务熔断状态下的监控信息:
先访问正确地址,再访问错误地址,再正确地址,会发现图示断路器都是慢慢放开的
如何看这个图
七色 一圈 一线
- 七色
一圈
实心圆:共两种含义。它通过颜色的变化代表了实例的健康程度,他的健康度从 绿色<黄色<程色<红色 递减。该实心圆除了颜色变化外,他的大小也会根据实例的请求流量发生变化,流量越大实心圆越大。所有通过该实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压实例。
一线
曲线:用来记录2分钟内流量的相对变化,通过它来观察到流量的上升和下降趋势。
整图说明
这里我们使用 Turbine 来聚合 hystrix-provider-service 服务的监控信息,然后我们的 hystrix-dashboard-service 服务就可以从 Turbine 获取聚合好的监控信息展示给我们了。
导入 pom 依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-turbineartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
创建 yml 配置文件
server:
port: 9011
spring:
application:
name: hystrix-turbine-service
security:
# 配置spring security登录用户名和密码
user:
name: akieay
password: 1qaz2wsx
eureka:
client:
#表示是否将自己注册进 Eureka Server服务 默认为true
register-with-eureka: true
#f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
# defaultZone: http://localhost:7001/eureka
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
instance:
instance-id: hystrix-turbine-9011
## 当调用getHostname获取实例的hostname时,返回ip而不是host名
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
lease-renewal-interval-in-seconds: 10
# Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
lease-expiration-duration-in-seconds: 30
turbine:
instanceUrlSuffix: /hystrix.stream
# 指定需要收集信息的服务名称
app-config: HYSTRIX-PROVIDER-SERVICE
# 指定服务所属集群
cluster-name-expression: new String('default')
# 以主机名和端口号区分服务
combine-host-port: true
主启动 添加
@EnableTurbine
来启用Turbine相关功能:
@SpringBootApplication
@EnableEurekaClient
@EnableTurbine
public class HystrixTurbineApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixTurbineApplication.class, args);
}
}
注意再启动服务之前,先修改服务提供者
HYSTRIX-PROVIDER-SERVICE
的主启动,添加如下配置:
/**
* 此配置是为了服务监控而配置,与服务容错本身无关,spring cloud 升级后的坑
* ServletRegistrationBean 因为 spring boot的默认路径不是 "/hystrix.stream",
* 只要在自己的项目里配置上下面的servlet就可以了
* @return
*/
@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;
}
重启两个服务提供者节点,启动聚合服务模块;
设置 Hystrix Dashboard 的监控信息,注意:添加的是 hystrix-turbine-service 的监控端点地址
多次访问:http://localhost:8015/payment/hystrix/ok/15 和 http://localhost:8016/payment/hystrix/ok/15 即可看到监控信息如下:可以看到我们的 Hystrix 监控的主机实例数量变成了两个
源码Git地址:https://gitee.com/ak_learning/akieay-spring-cloud.git