服务降级&熔断&限流
服务降级
服务降级指的是当服务器压力剧增的情况下,为了保证核心功能的可用性 ,而选择性的降低一些功能的可用性,或者直接关闭该功能。
服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback 兜底处理。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。
服务熔断
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
熔断和降级是两个比较容易混淆的概念,两者的含义并不相同。降级的目的在于应对系统自身的故障,而熔断的目的在于应对当前系统依赖的外部系统或者第三方系统的故障。
服务限流
限流是从用户访问压力的角度来考虑如何应对系统故障。
限流为了对服务端的接口接受请求的频率进行限制,防止服务挂掉。比如某一接口的请求限制为 100 个每秒, 对超过限制的请求放弃处理或者放到队列中等待处理。限流可以有效应对突发请求过多。
Hystrix
核心概念
分布式系统面临的问题:微服务架构中,一个应用会根据业务来拆分成一个个的服务,服务与服务之间可以相互调用(RPC),这些服务之间相互依赖,依赖关系错综复杂。当微服务系统的一个服务出现故障时,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet 容器的线程资源会被消耗完毕,导致服务瘫痪。由于服务与服务之间的依赖性,故障会沿着服务的调用链路在系统中蔓延,最终导致整个微服务系统的瘫痪,这就是“雪崩效应”。为了防止此类事件的发生,微服务架构引入了“熔断器”的一系列服务容错和保护机制。
熔断器本身是一种开关装置,当某个服务发生故障后,通过断路器的故障监控,向服务调用方返回一个符合预期的、可处理的降级响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常。这样就保证了服务调用方的线程不会被长时间、不必要地占用,避免故障在微服务系统中的蔓延,防止系统雪崩效应的发生。
Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,提供了熔断器功能。在分布式系统里,许多依赖服务不可避免的会调用失败,比如超时、异常等。Hystrix 能够保证在一个依赖服务出现问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。Spring Cloud Hystrix 具有服务降级、服务熔断、线程隔离、请求缓存、请求合并以及实时故障监控等强大功能。
Hystrix 提供了服务降级功能,能够保证当前服务不受其他服务故障的影响,提高服务的健壮性。
服务降级的使用场景有以下 2 种:
- 在服务器压力剧增时,根据实际业务情况及流量,对一些不重要、不紧急的服务进行有策略地不处理或简单处理,从而释放服务器资源以保证核心服务正常运作。
- 当某些服务不可用时,为了避免长时间等待造成服务卡顿或雪崩效应,而主动执行备用的降级逻辑立刻返回一个友好的提示,以保障主体业务不受影响。
Hystrix 服务降级 FallBack 既可以放在服务端进行,也可以放在客户端进行。
Hystrix 会在以下场景下进行服务降级处理:程序运行异常、服务超时、服务宕机。
熔断机制是为了应对雪崩效应而出现的一种微服务链路保护机制。
当微服务系统中的某个微服务不可用或响应时间太长时,为了保护系统的整体可用性,熔断器会暂时切断请求对该服务的调用,并快速返回一个友好的错误响应。这种熔断状态不是永久的,在经历了一定的时间后,熔断器会再次检测该微服务是否恢复正常,若服务恢复正常则恢复其调用链路。
在熔断机制中涉及了三种熔断状态:
- 熔断关闭状态 (Closed) :当服务访问正常时,熔断器处于关闭状态,服务调用方可以正常地进行服务调用。
- 熔断开启状态 (Open) :默认情况下,在固定时间内接口调用出错比率达到一个阈值(例如 50%),熔断器会进入熔断开启状态。进入熔断状态后,后续对该服务的调用都会被切断,熔断器会执行本地的降级 (FallBack) 方法。
- 半熔断状态 (Half-Open) :在熔断开启一段时间之后,熔断器会进入半熔断状态。在半熔断状态下,熔断器会尝试恢复服务调用方对服务的调用,允许部分请求调用该服务,并监控其调用成功率。如果成功率达到预期,则说明服务已恢复正常,熔断器进入关闭状态;如果成功率仍旧很低,则重新进入熔断开启状态。
微服务搭建
支付微服务搭建
业务类测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Service public class PaymentHystrixService { public String paymentInfo_OK(Integer id) { return "线程池: " + Thread.currentThread().getName() + ", paymentInfo_OK, id = " + id; } public String paymentInfo_TimeOut(Integer id) { int timeout = 3; TimeUnit.SECONDS.sleep(timeout); return "线程池: " + Thread.currentThread().getName() + ", paymentInfo_TimeOut, id = " + id + ", 耗时" + timeout + "秒"; } }
|
Controller 测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Slf4j @RestController @RequestMapping("/payment") public class PaymentHystrixController { @Resource private PaymentHystrixService paymentHystrixService;
@Value("${server.port}") private String serverPort;
@GetMapping("/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_OK(id); log.info("result: " + result); return result; }
@GetMapping("/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentInfo_TimeOut(id); log.info("result: " + result); return result; } }
|
订单微服务搭建
引入依赖,yml 与主启动类不变。
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
|
OpenFeign 接口
1 2 3 4 5 6 7 8 9
| @Component @FeignClient(value = "PROVIDER-PAYMENT") public interface PaymentFeignService { @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 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Slf4j @RestController @RequestMapping("/order") public class OrderFeignHystrixController { @Resource private PaymentFeignService paymentFeignService;
@GetMapping("/hystrix/ok/{id}") public String paymentInfo_OK(@PathVariable("id") Integer id) { String result = paymentFeignService.paymentInfo_OK(id); log.info("result: " + result); return result; }
@GetMapping("/hystrix/timeout/{id}") public String paymentInfo_TimeOut(@PathVariable("id") Integer id) { String result = paymentFeignService.paymentInfo_TimeOut(id); log.info("result: " + result); return result; } }
|
服务降级
降级容错解决的维度要求
- 超时导致服务器变慢(转圈) - 超时不再等待
- 出错(宕机或程序运行出错) - 出错要有兜底
解决:
- 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级。
- 对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级。
- 对方服务(8001)OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级。
支付服务 fallback(服务端降级)
服务提供方先从自身找问题,设置自身调用超时时间的上限,上限内可以正常运行,超过了需要有兜底的方法处理,作服务降级 fallback。
修改业务类,使用 @HystrixCommand
与 @HystrixProperty
注解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Service public class PaymentHystrixService {
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = { //规定3秒钟以内就不报错,正常运行,超过3秒就报错 @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000") }) public String paymentInfo_TimeOut(Integer id) { int timeout = 5; TimeUnit.SECONDS.sleep(timeout); return "线程池: " + Thread.currentThread().getName() + ", paymentInfo_TimeOut, id = " + id + ", 耗时" + timeout + "秒"; }
public String paymentInfo_TimeOutHandler(Integer id) { return "线程池: " + Thread.currentThread().getName() + ", paymentInfo_TimeOutHandler, id = " + id + "系统繁忙或运行报错,请稍后重试"; } }
|
主启动类激活熔断器
1 2 3 4 5
| @SpringBootApplication @EnableEurekaClient @EnableDiscoveryClient @EnableCircuitBreaker public class PaymentApplication {
|
订单服务 fallback(客户端降级)
80 订单微服务,也可以用降级保护,更好的保护自己。实际业务一般服务降级都放在这种消费者端/客户端。
application.yml 中添加以下配置,开启客户端的 Hystrix 功能。
1 2 3 4
| feign: hystrix: enabled: true
|
主启动类启用 Hystrix。
1 2 3 4 5
| @SpringBootApplication @EnableEurekaClient @EnableFeignClients @EnableHystrix public class OrderFeignApplication {
|
Feign 调用降级。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @RestController @RequestMapping("/order") public class OrderFeignHystrixController { @Resource private PaymentFeignService paymentFeignService;
@GetMapping("/hystrix/timeout/{id}") @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = { //规定使用feign调用服务,1.5秒以内返回就不报错,正常运行;超过1.5秒就报错,调用兜底降级方法 @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500") }) public String paymentInfo_TimeOut(@PathVariable("id") Integer id) { return paymentFeignService.paymentFeignTimeout(); } public String paymentTimeOutFallbackMethod(Integer id) { return "我是订单消费者80,对方支付系统繁忙,请稍后重试"; } }
|
全局服务降级
目前问题 1:每个业务方法对应一个兜底的方法,代码膨胀。
除了个别重要核心业务有专属降级方法,其它普通的可以通过 @DefaultProperties(defaultFallback = “”)
统一跳转到统一处理结果页面。通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量。
在类名上标注 @DefaultProperties
注解,并通过其 defaultFallback
属性指定一个全局的降级方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @RestController @RequestMapping("/order") @DefaultProperties(defaultFallback = "payment_Global_FallbackMethod") public class OrderFeignHystrixController { @Resource private PaymentFeignService paymentFeignService;
@GetMapping("/hystrix/timeout/{id}") @HystrixCommand public String paymentInfo_TimeOut(@PathVariable("id") Integer id) { int i = 1 / 0; return paymentFeignService.paymentFeignTimeout(); }
public String payment_Global_FallbackMethod() { return "全局异常处理信息,请稍后再试~"; } }
|
注意:全局降级方法的优先级较低,只有业务方法没有指定其降级方法,并且加上 @HystrixCommand
注解时,服务降级时才会触发全局降级方法。若业务方法指定它自己的降级方法,那么在服务降级时,就只会直接触发它自己的降级方法,而非全局降级方法。
注意:降级(FallBack)方法必须与其对应的业务方法在同一个类中,否则无法生效。
全局降级方法默认超时时间为 1 秒。可以直接指定 commandProperties。
1 2 3 4 5 6 7 8
| @GetMapping("/hystrix/timeout/{id}") @HystrixCommand(commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000") }) public String paymentInfo_TimeOut(@PathVariable("id") Integer id) { return paymentFeignService.paymentInfo_TimeOut(id); }
|
解耦降级逻辑
目前问题 2:不管是业务方法指定的降级方法还是全局降级方法,它们都必须和业务方法在同一个类中才能生效,业务逻辑与降级逻辑耦合度极高。
只需要为 Feign 客户端定义的接口添加一个服务降级处理的实现类即可实现对业务逻辑与降级逻辑的解耦。
1 2 3 4 5 6 7 8 9 10 11
| @Component public class PaymentFallbackService implements PaymentFeignService{ @Override public String paymentInfo_OK(Integer id) { return "paymentInfo_OK PaymentFallbackService fallback"; } @Override public String paymentInfo_TimeOut(Integer id) { return "paymentInfo_TimeOut PaymentFallbackService fallback"; } }
|
@FeignClient 指定 fallback 类。
1 2 3 4 5 6 7 8 9
| @Component @FeignClient(value = "PROVIDER-PAYMENT", fallback = PaymentFallbackService.class) public interface PaymentFeignService { @GetMapping("/payment/hystrix/ok/{id}") String paymentInfo_OK(@PathVariable("id") Integer id); @GetMapping("/payment/hystrix/timeout/{id}") String paymentInfo_TimeOut(@PathVariable("id") Integer id); }
|
服务熔断
支付微服务配置(订单服务也一样)
业务类配置服务熔断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Service public class PaymentHystrixService {
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = { //以下参数在 HystrixCommandProperties 类中有默认配置 //是否开启熔断器 @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), //统计时间窗(默认10秒) @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1000"), //统计时间窗内请求次数 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), //休眠时间窗口期 @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), //在统计时间窗口期以内,请求失败率达到60%时进入熔断状态 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), }) public String paymentCircuitBreaker(Integer id) { if (id < 0) { throw new RuntimeException("id不能为负数"); } return Thread.currentThread().getName() + "调用成功,流水号:" + UUID.randomUUID(); } public String paymentCircuitBreaker_fallback(Integer id) { return "请稍后再试, id = " + id; } }
|
在以上代码中,共涉及到了 4 个与 Hystrix 熔断机制相关的重要参数,这 4 个参数的含义如下表。
参数 |
描述 |
timeInMilliseconds |
统计时间窗(单位毫秒),默认10秒。 |
sleepWindowInMilliseconds |
休眠时间窗,熔断开启状态持续一段时间后,熔断器会自动进入半熔断状态,这段时间就被称为休眠窗口期。 |
requestVolumeThreshold |
请求总数阀值。 在统计时间窗内,请求总数必须到达一定的数量级,Hystrix 才可能会将熔断器打开进入熔断开启转态,而这个请求数量级就是 请求总数阀值。Hystrix 请求总数阈值默认为 20,这就意味着在统计时间窗内,如果服务调用次数不足 20 次,即使所有的请求都调用出错,熔断器也不会打开。 |
errorThresholdPercentage |
错误百分比阈值。 当请求总数在统计时间窗内超过了请求总数阀值,且请求调用出错率超过一定的比例,熔断器才会打开进入熔断开启转态,而这个比例就是错误百分比阈值。错误百分比阈值设置为 50,就表示错误百分比为 50%,如果服务发生了 30 次调用,其中有 15 次发生了错误,即超过了 50% 的错误百分比,这时候将熔断器就会打开。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @RestController @RequestMapping("/payment") public class PaymentHystrixController { @Resource private PaymentHystrixService paymentHystrixService;
@GetMapping("/hystrix/circuit/{id}") public String paymentCircuitBreaker(@PathVariable("id") Integer id) { String result = paymentHystrixService.paymentCircuitBreaker(id); log.info("result: " + result); return result; } }
|
断路器开启或者关闭的条件:
- 到达以下阀值,断路器将会开启:
- 当请求总数达到阀值的时候(默认10秒内超过20个请求次数)
- 当请求失败率达到阈值的时候(默认10秒内超过50%的请求失败)
- 当断路器开启的时候,所有请求都将不会调用主逻辑,而是直接走降级方法。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果。
- 一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。
Hystrix 工作流程图
Hystrix 故障监控
Hystrix 还提供了准实时的调用监控(Hystrix Dashboard)功能,Hystrix 会持续地记录所有通过 Hystrix 发起的请求的执行信息,并以统计报表的形式展示给用户,包括每秒执行请求的数量、成功请求的数量和失败请求的数量等。
新开微服务 9001 端口。
1 2 3 4 5 6 7 8
| <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>
|
1 2 3
| @SpringBootApplication @EnableHystrixDashboard public class HystrixDashboardApplication {
|
9001 监控 8001:http://localhost:8001/hystrix.stream
需要在被监控的 8001 端口的服务增加如下配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Configuration public class HystrixDashboardConfig {
@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; } }
|