为什么需要降级
在开发高并发系统时,有很多方法来保护系统,如:缓存、降级、限流等。下面将介绍一下降级的策略。当系统访问量增多,服务响应时间长或者非核心服务影响了核心服务的性能时。这是如果需要保证核心服务的可用性,就需要对非核心业务执行一些降级处理。系统可以根据关键数据进行自动降级,也可以配置开关进行人工降级。
降级策略分类
降级按照是否可以自动化分为:自动开关降级和人工开关降级。按照读写功能可以分为:读服务降级和写服务降级。当从用户访问的整条链路来看,将会有以下多级
降级策略:
- 页面降级:当在大促时,某些页面占用了稀缺资源,可以对整个页面进行降级;当页面上会异步加载推荐信息等一些非核心的业务时,此时如果响应变慢,则可以进行降级处理。
- 服务读降级:一般情况下,分布式应用当中都会有缓存,查询频率比较高的数据,一般都会从缓存中获取。但是有一些数据,如果直接从缓存中获取的话,有可能造成客户的投诉。比如用户账户余额,这个一般只从DB里面获取,而且是主库里面去读取。当大促来临时,此时可以降级为从库里面或者缓存里面去获取余额信息。
- 服务写降级:在秒杀抢购业务中,由于并发的数量比较大。除了在各个层面上限流、使用队列等方式应对,还可以对写库进行降级,可以将库存扣减操作在内存中进行,当高峰过去之后,再异步的同步至DB中做扣减操作。
- 其他类型:如在系统繁忙时,可以将爬虫的流量直接丢弃。当高峰过后,再自动恢复。秒杀业务中,风控系统可以识别刷子/机器人,然后可以直接对这些用户执行拒绝服务。
自动开关降级
自动降级是根据系统负载、资源使用情况等指标进行降级操作。
- 超时降级
当访问的数据库/HTTP服务/远程调用响应慢,如果此服务不是核心服务的话,可以在超时后执行自动降级。如商品详情页中的评论信息,可以在查询超时后直接降级,然后前端可以再进行单独的评论信息的查询。
- 统计失败次数降级
当系统依赖外部的接口调用失败达到一定次数时,可以自动降级。然后开启一个异步的线程去探测是否恢复,恢复后执行自动取消降级(银行/第三方渠道系统出现故障)。
- 故障降级
这里面的故障大多是指内部系统的故障。当系统发生故障时,处理的方案可以有:默认值(库存默认有货)、兜底数据(广告系统挂了,可以直接返回之前准备好的静态页面)、缓存等。
- 限流降级
当系统负载过大时,可以使用排队页面、无货或者错误页等。这里面会用到降级的一些技术方案,可以参考笔者的《分布式系统降级策略》。
人工开关降级
在大促期间可以通过线上监控发现一些问题。如果此时系统不能自动降级,就需要人工介入了。例如有些后台的定时任务,会占用一部分的系统资源。此时如果系统处于高负载的运行,则可以手工停掉定时任务。待峰值过去之后再恢复启动。通常情况下开关的配置文件会放到配置中心,配置中心的实现方式可以通过ZooKeeper、Redis、Consul等。
另外,对于新系统在进行灰度测试时,可以通过开关来控制是否引流。当发现灰度环境有问题时,可以通过开关降级,切换到线上环境中去。
使用hystrix进行降级
Hystrix最开始由Netflix(看过美剧的都知道,它是一个美剧影视制作的巨头公司)开源的,后来由Spring Cloud Hystrix基于这款框架实现了断路器、线程隔离等一系列服务保护功能,该框架的目标在于通过控制访问远程系统、服务和第三方库的节点,从而延迟和故障提供更强大的容错能力。hystrix具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。起到了微服务的保护机制,防止某个单元出现故障.从而引起依赖关系引发故障的蔓延,最终导致整个系统的瘫痪。
- 问题的产生
假设我们有一个服务,然后需要去调用订单服务获取订单信息,但是订单服务调用失败(服务瘫痪),此时,不能返回空,或者报错,这样容易造成整个调用链异常,甚至导致整个服务瘫痪,因此这个时候我们可以提供降级,远程请求查询失败(查询db),但我们可以降级去查询缓存。
引入pom:
<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>
- 通过注解的方式实现
controller:
@RestController
public class OrderController {
@Resource
private OrderService orderService;
/**
* 获取订单信息
*/
@GetMapping("/order")
public String getOrderInfo(@RequestParam Long orderId) {
return orderService.orderDetail(orderId);
}
}
Service:
@Service
@EnableHystrix
public class OrderService {
@HystrixCommand(fallbackMethod = "fallbackException")
public String orderDetail(Long orderId) {
//模拟远程调用订单详情服务
return remoteOrderDetail(orderId);
}
/**
* 远程调用订单失败从缓存获取订单详情
*/
public String fallbackException(Long orderId) {
//模拟缓存查询
return "获取缓存订单成功";
}
public String remoteOrderDetail(Long orderId) {
//模拟调用远程订单详情服务
if (orderId < 0) {
throw new IllegalArgumentException("订单号非法");
}
return "获取db订单成功";
}
}
通过注解@EnableHystrix开启降级,然后通过 @HystrixCommand(fallbackMethod = “fallbackException”)注解,指定降级的方法即可。
验证结果:
正常情况:
异常情况:
- 自定义方式实现
- 自定义继承HystrixCommand
package com.lht.boot.distribution.hystrix;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandProperties;
/**
* 自定义
*/
public class HystrixStubbedFallback extends HystrixCommand<String> {
public HystrixStubbedFallback() {
super(setter());
}
private static Setter setter() {
HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties
.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)
//是否 启用降级,如果启用则在超时或异常时调用
.withFallbackEnabled(true)
//fallback方法的信号量配置,如果超过了信号量配置,则不会再调用getFallback方法,而是快速失败
.withFallbackIsolationSemaphoreMaxConcurrentRequests(100)
//隔离策略为THREAD时,当执行线程执行超时时,是否 进行中断处理,即Future#cancel(true) 处理,默认为false
.withExecutionIsolationThreadInterruptOnFutureCancel(true)
//隔离策略为THREAD时,当执行线程执行超时时,是否 进行中断处理,默认为true
.withExecutionIsolationThreadInterruptOnTimeout(true)
//是否启用执行超时机制,默认为true
.withExecutionTimeoutEnabled(true)
//执行超时时间,默认为1000毫秒,如果命令时线程隔离,且配置了withExecutionIsolationThreadInterruptOnTimeout=true
//则执行线程中断处理,如果命令是信号量隔离,则进行终止操作,因为信号量隔离与主线程是在一个线程中执行,其不会中断线程处理,所以要
//根据实际情况来采用是否用信号量隔离,尤其涉及网络访问的情况
.withExecutionTimeoutInMilliseconds(1000);
return Setter
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandPropertiesDefaults(commandProperties);
}
/**
* 触发业务的方法
*/
@Override
protected String run() {
throw new RuntimeException("failure for example"); //模拟异常用于触发回滚
}
/**
* run方法失败后执行
*/
@Override
protected String getFallback() {
return "有货";
}
}
验证结果:
@Test
public void test() {
HystrixStubbedFallback command = new HystrixStubbedFallback();
//执行结果
String result = command.execute();
//执行是否失败了,如抛出了异常
boolean failedExecution = command.isFailedExecution();
//响应是否超时
boolean responseTimedOut = command.isResponseTimedOut();
//是否是getFallback返回的响应
boolean responseFromFallback = command.isResponseFromFallback();
//获取失败后执行的异常。即run方法抛出的异常
Throwable failedExecutionException = command.getFailedExecutionException();
System.out.println("响应是否超时: " + responseTimedOut);
System.out.println("执行是否失败了: " + failedExecution);
System.out.println("是否是getFallback返回的响应: " + responseFromFallback);
System.out.println("返回的结果: " + result);
System.out.println("获取失败后执行的异常: "+failedExecutionException.getMessage());
}
阿里sentinel实现降级
- Sentinel 是什么?
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
- 使用Sentinel
使用前一定要阅读官网 Sentinel官网
这里使用我们利用了nacos将降级规则持久化到nacos。
- pom
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- Sentinel 针对 Nacos 作了适配,底层可以采用 Nacos 作为规则配置数据源 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
- yml配置
spring:
cloud:
sentinel:
transport:
dashboard: XXXXXXXXXX:8070 #配置sentinel dashboard地址
port: 8721 #指定和控制通信的端口,默认值8719,如不设置,会自动从8719开始扫描,依次+1,直到找到未被占用的端口
datasource:
#degrade是数据源名,可以自行随意修改
degrade:
nacos:
server-addr: 10.100.217.145:8848
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
rule-type: degrade
定义一个Controller,通过注解@SentinelResource写上你要定义的规则资源名称,SentinelResource注解支持文档
/**
* 根据id查询
*
* @param id 主键
* @return 查询结果话题
*/
@ApiOperation("根据id查询(降级)")
@ApiImplicitParams({
@ApiImplicitParam(paramType = PATH, dataType = INTEGER, name = "id", value = "主键id", required = true)
})
@GetMapping("/{id}")
@ResponseBody
@SentinelResource(value = "tcly-operations-center-degrade-rules", blockHandlerClass = TopicExceptionUtils.class, blockHandler = "queryException")
public CommonResult<TopicVO> query(@PathVariable Long id) {
if (id <= 0) {
//模拟接口运行时抛出异常
throw new IndexOutOfBoundsException();
}
return CommonResult.success(entityToVo(service.getById(id)));
}
处理异常的代码:
public class TopicExceptionUtils {
/**
* 隐藏构造器
*/
private TopicExceptionUtils(){}
/**
* 话题查询降级处理
* @param id 查询条件
* @param blockException 降级异常
* @return 失败结果
*/
public static CommonResult<TopicVO> queryException(Long id, BlockException blockException) {
return CommonResult.failed("根据id:" + id + "查询失败,进行降级处理");
}
以上就是所有的代码了;
下面通过sentinel dashboard配置规则:
资源名称就是@SentinelResource的value:
在dashboard配置之后,dashboard会通过自动同步到nacos
进行调用测试:
操作失败是正常情况下的返回
出现降级