设计一个秒杀系统之熔断降级

时间:2021-07-26 01:19:05


为什么需要降级

在开发高并发系统时,有很多方法来保护系统,如:缓存、降级、限流等。下面将介绍一下降级的策略。当系统访问量增多,服务响应时间长或者非核心服务影响了核心服务的性能时。这是如果需要保证核心服务的可用性,就需要对非核心业务执行一些降级处理。系统可以根据关键数据进行自动降级,也可以配置开关进行人工降级。

降级策略分类

降级按照是否可以自动化分为:自动开关降级和人工开关降级。按照读写功能可以分为:读服务降级和写服务降级。当从用户访问的整条链路来看,将会有以下多级
降级策略:

  • 页面降级:当在大促时,某些页面占用了稀缺资源,可以对整个页面进行降级;当页面上会异步加载推荐信息等一些非核心的业务时,此时如果响应变慢,则可以进行降级处理。
  • 服务读降级:一般情况下,分布式应用当中都会有缓存,查询频率比较高的数据,一般都会从缓存中获取。但是有一些数据,如果直接从缓存中获取的话,有可能造成客户的投诉。比如用户账户余额,这个一般只从DB里面获取,而且是主库里面去读取。当大促来临时,此时可以降级为从库里面或者缓存里面去获取余额信息。
  • 服务写降级:在秒杀抢购业务中,由于并发的数量比较大。除了在各个层面上限流、使用队列等方式应对,还可以对写库进行降级,可以将库存扣减操作在内存中进行,当高峰过去之后,再异步的同步至DB中做扣减操作。
  • 其他类型:如在系统繁忙时,可以将爬虫的流量直接丢弃。当高峰过后,再自动恢复。秒杀业务中,风控系统可以识别刷子/机器人,然后可以直接对这些用户执行拒绝服务。

自动开关降级

自动降级是根据系统负载、资源使用情况等指标进行降级操作。

  • 超时降级

当访问的数据库/HTTP服务/远程调用响应慢,如果此服务不是核心服务的话,可以在超时后执行自动降级。如商品详情页中的评论信息,可以在查询超时后直接降级,然后前端可以再进行单独的评论信息的查询。

  • 统计失败次数降级

当系统依赖外部的接口调用失败达到一定次数时,可以自动降级。然后开启一个异步的线程去探测是否恢复,恢复后执行自动取消降级(银行/第三方渠道系统出现故障)。

  • 故障降级

这里面的故障大多是指内部系统的故障。当系统发生故障时,处理的方案可以有:默认值(库存默认有货)、兜底数据(广告系统挂了,可以直接返回之前准备好的静态页面)、缓存等。

  • 限流降级

当系统负载过大时,可以使用排队页面、无货或者错误页等。这里面会用到降级的一些技术方案,可以参考笔者的《分布式系统降级策略》。

人工开关降级

在大促期间可以通过线上监控发现一些问题。如果此时系统不能自动降级,就需要人工介入了。例如有些后台的定时任务,会占用一部分的系统资源。此时如果系统处于高负载的运行,则可以手工停掉定时任务。待峰值过去之后再恢复启动。通常情况下开关的配置文件会放到配置中心,配置中心的实现方式可以通过ZooKeeper、Redis、Consul等。

另外,对于新系统在进行灰度测试时,可以通过开关来控制是否引流。当发现灰度环境有问题时,可以通过开关降级,切换到线上环境中去。

使用hystrix进行降级

Hystrix最开始由Netflix(看过美剧的都知道,它是一个美剧影视制作的巨头公司)开源的,后来由Spring Cloud Hystrix基于这款框架实现了断路器、线程隔离等一系列服务保护功能,该框架的目标在于通过控制访问远程系统、服务和第三方库的节点,从而延迟和故障提供更强大的容错能力。hystrix具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。起到了微服务的保护机制,防止某个单元出现故障.从而引起依赖关系引发故障的蔓延,最终导致整个系统的瘫痪。

  1. 问题的产生

假设我们有一个服务,然后需要去调用订单服务获取订单信息,但是订单服务调用失败(服务瘫痪),此时,不能返回空,或者报错,这样容易造成整个调用链异常,甚至导致整个服务瘫痪,因此这个时候我们可以提供降级,远程请求查询失败(查询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”)注解,指定降级的方法即可。

验证结果:

正常情况:

设计一个秒杀系统之熔断降级

异常情况:

设计一个秒杀系统之熔断降级

  • 自定义方式实现
  1. 自定义继承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

使用前一定要阅读官网 Sentinel官网

这里使用我们利用了nacos将降级规则持久化到nacos。

  1. 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>
  1. 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

设计一个秒杀系统之熔断降级

进行调用测试:

操作失败是正常情况下的返回

设计一个秒杀系统之熔断降级

出现降级

设计一个秒杀系统之熔断降级