Sentinel系列之流量控制及熔断降级示例

发布时间 2023-10-02 12:45:12作者: kingsleylam

关于Sentinel的介绍网上很多,不再复制粘贴。

本文主要演示Sentinel的两个重点功能:流量控制和熔断降级。

示例基于Sentinel 1.8.6, 同时使用JMeter进行并发请求(Postman无法并发)。当然也可以通过main方法,但这样就无法重复触发,并且无法学习Sentinel与Spring框架的集成

另外需要注意的是规则要一次载入

@PostConstruct
    private void initFlowRules() {
        List<FlowRule> rules = new ArrayList<>();
        rules.add(getQPSGradeFlowRule());
        rules.add(getQPSGradeRateLimiterFlowRule());
        rules.add(getQPSGradeWarmupFlowRule());
        rules.add(getThreadGradeFlowRule());
        rules.add(getThreadGradeRateLimiterFlowRule());
        /**
         * 加载规则,注意只能加载一次,否则会互相覆盖。最后会调到{@link com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager.FlowPropertyListener#configUpdate(java.util.List)}
         */
        FlowRuleManager.loadRules(rules);
    }

我认为既然已经学习到Sentinel,应该不需要手把手指导如何运行代码。

1. 流量控制

介绍

Sentinel提供了两大类的流量控制方式:基于QPS/并发数和基于调用关系,这里着重研究前者,因为后者是在前者的基础上细分了调用者、流量入口等,掌握了前者有助于理解后者。

限流的直接表现是在执行 Entry nodeA = SphU.entry(资源名字) 的时候抛出 FlowException 异常。FlowException 是 BlockException 的子类,可以捕捉 BlockException 来自定义被限流之后的处理逻辑。

同一个资源可以对应多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。

一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

  • resource:资源名,即限流规则的作用对象
  • count: 限流阈值
  • grade: 限流阈值类型,QPS 或线程数
  • strategy: 根据调用关系选择策略,有STRATEGY_DIRECT(默认值),STRATEGY_RELATE(基于关联关系),STRATEGY_CHAIN(基于调用链路)
  • controlBehavior:流量控制的手段,即超过阈值后,Sentinel的做法,包括直接拒绝(默认),冷启动,匀速器

实例

为方便演示,使用硬编码的方式设置流量规则。注意FlowRuleManager#loadRules方法只能全局调用一次,否则会互相覆盖。

@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性:

  • value:资源名称,必需项(不能为空)

  • entryType:entry 类型,可选项(默认为 EntryType.OUT)

    资源调用的流量类型,是入口流量(EntryType.IN)还是出口流量(EntryType.OUT),注意系统规则只对 IN 生效

  • blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

  • fallback:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。

    fallback 函数签名和位置要求:返回值类型必须与原函数返回值类型一致;方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。

    fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

  • defaultFallback(since 1.6.0):和fallback差不多,区别是全局的。

  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

示例1 QPS模式,直接拒绝

该方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。

    /**
     * 模拟场景:前端同时并发5个请求
     * 预期:2个接受,3个失败
     *
     * @return 响应
     */
    @SentinelResource(value = QPS_DEFAULT_RESOURCE, blockHandler = "testQPSBlocked")
    @GetMapping("/qps/default")
    public String testQPS() {
        logger.info("Visit QPS-Grade api successfully.");
        return "OK";
    }

    private FlowRule getQPSGradeFlowRule() {
        FlowRule flowRule = new FlowRule();
        //设置流控的资源
        flowRule.setResource(QPS_DEFAULT_RESOURCE);
        //设置流控规则-QPS
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        //每秒只能访问两次
        flowRule.setCount(2);
        //默认行为是直接拒绝
        flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
        return flowRule;
    }

    public String testQPSBlocked(BlockException blockException) {
        logger.info("QPS-Grade blocked!");
        return "QPS-Grade blocked!";
    }

运行结果:

示例2 QPS模式,排队

这种方式严格控制了请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。

    /**
     * 模拟场景:前端同时并发5个请求
     * 预期:2个接受,3个排队
     *
     * @return 响应
     */
    @SentinelResource(value = QPS_RATE_LIMITER_RESOURCE, blockHandler = "testQPSBlocked")
    @GetMapping("/qps/ratelimiter")
    public String testQPSRateLimiter() throws InterruptedException {
        logger.info("Visit QPS-Grade RateLimiter api successfully.");
        return "OK";
    }

    private FlowRule getQPSGradeRateLimiterFlowRule() {
        FlowRule flowRule = new FlowRule();
        //设置流控的资源
        flowRule.setResource(QPS_RATE_LIMITER_RESOURCE);
        //设置流控规则-QPS
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        //每秒只能访问两次
        flowRule.setCount(2);
        // 排队
        flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
        // 排队时间,默认是500毫秒,超过排队时间会抛出异常
        flowRule.setMaxQueueingTimeMs(3_000);
        return flowRule;
    }

运行结果,没有直接拒绝,而是排队,最后都通过了。

示例3 QPS模式,冷启动

该方式有多种叫法,预热、Warmup、冷启动。主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。

   /**
     * 模拟场景:前端不断地发送请求
     * 预期:在控制台看到明显的爬坡过程
     *
     * @return 响应
     */
    @SentinelResource(value = QPS_WARM_UP_RESOURCE, blockHandler = "testQPSBlocked")
    @GetMapping("/qps/warmup")
    public String testQPSWarmup() throws InterruptedException {
        logger.info("Visit QPS-Grade Warmup api successfully.");
        return "OK";
    }

    private FlowRule getQPSGradeWarmupFlowRule() {
        FlowRule flowRule = new FlowRule();
        //设置流控的资源
        flowRule.setResource(QPS_WARM_UP_RESOURCE);
        //设置流控规则-QPS
        flowRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        //根据机器性能调整,每秒通过数和被拒绝数不能相差太多,否则影响曲线图的展示
        flowRule.setCount(5000);
        // 预热
        flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
        flowRule.setWarmUpPeriodSec(10);
        return flowRule;
    }

这里需要使用Dashboard观察效果。

可以看到有一个明显的缓慢爬坡过程。如果不用这种方式,则是相对陡峭的曲线。

示例4 Thread模式

线程数限流用于保护业务线程数不被耗尽。Sentinel线程数限流简单统计当前请求上下文的线程个数,如果超出阈值,新的请求会被立即拒绝。

这种模式和JUC的信号量一样,与时间窗无关。

	/**
     * 模拟场景:5秒内,前端每隔1秒,发送1个请求,共5个请求
     * 预期:5秒内可以接受2个请求,其余的失败
     *
     * @return 响应
     * @throws InterruptedException
     */
    @SentinelResource(value = THREAD_DEFAULT_RESOURCE, blockHandler = "testThreadBlocked")
    @GetMapping("/thread/default")
    public String testThreadRateLimiter() throws InterruptedException {
        logger.info("Enter Thread-Grade.");
        // 模拟耗时操作,前端每隔一秒发来一次请求,被限流。和JUC的semaphore一样。
        Thread.sleep(3_000L);
        logger.info("Visit Thread-Grade api successfully.");
        return "OK";
    }

    private FlowRule getThreadGradeFlowRule() {
        FlowRule flowRule = new FlowRule();
        //设置流控的资源
        flowRule.setResource(THREAD_DEFAULT_RESOURCE);
        //设置流控规则-Thread
        flowRule.setGrade(RuleConstant.FLOW_GRADE_THREAD);
        //同一时间只能有一个线程访问
        flowRule.setCount(1);
        //默认行为是直接拒绝
        return flowRule;
    }

运行结果如下:

2. 熔断降级

介绍

一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。

因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

Sentinel 提供以下几种熔断策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。

image-20230910152525511

熔断降级规则(DegradeRule)包含下面几个重要的属性:

Field 说明 默认值
resource 资源名,即规则的作用对象
grade 熔断策略,支持慢调用比例/异常比例/异常数策略 慢调用比例
count 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
timeWindow 熔断时长,单位为 s
minRequestAmount 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) 5
statIntervalMs 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) 1000 ms
slowRatioThreshold 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

实例

示例1 慢调用熔断

	/**
     * 模拟场景:
     * 1. 并发5个请求,全部超过耗时阈值,以及比例阈值,触发熔断
     * 2. 在熔断期内,并发5个请求,预期:全部熔断
     * 3. 熔断期(10秒)结束,并发5个请求,预期:先放开一个试试,并不会全部通过
     * 4. 再次并发5个请求,预期:由于第3步已经尝试一个成功,那么熔断器关闭
     *
     * @return
     * @throws InterruptedException
     */
    @SentinelResource(value = SLOW_REQUEST_RATIO_RESOURCE, blockHandler = "handleBlockException")
    @GetMapping("/slowrequestratio")
    public String testSlowRequestRatio() throws InterruptedException {
        int counter = slowRequestRatioCounter.incrementAndGet();
        if (counter <= 5) {
            Thread.sleep(2_000L);
        } else {
            logger.info("Enter!");
            Thread.sleep(500L);
        }
        logger.info("Visit SlowRequestRatio api successfully.");
        return "OK";
    }

    private DegradeRule getSlowRequestRatioDegradeRule() {
        DegradeRule degradeRule = new DegradeRule();
        //设置流控的资源
        degradeRule.setResource(SLOW_REQUEST_RATIO_RESOURCE);
        // 统计耗时的模式
        degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
        // 响应时间超过1秒,认为系统卡顿
        degradeRule.setCount(1_000L);
        // 一分钟内有90%慢就要熔断
        degradeRule.setSlowRatioThreshold(0.9);
        degradeRule.setStatIntervalMs(60_000);
        // 熔断时间,注意这里是秒
        degradeRule.setTimeWindow(10);
        return degradeRule;
    }

运行结果:

源码分析:com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.AbstractCircuitBreaker

如果熔断器是关闭状态,直接通过。

如果是开启状态,通过CAS确保只有一个请求能把开关设置为半开状态,并尝试调用;CAS失败认为不能通过。

如果是半开启状态,说明已经有别的请求在尝试调用了,并且还没完成,所以也认为是不能通过。

示例2 异常比例

 	/**
     * 模拟场景:
     * 1. 并发5个请求,3个异常,2个正常,超过阈值0.4,触发熔断
     * 2. 在熔断期内,并发5个请求,预期:全部熔断
     * 3. 熔断期(30秒)结束,并发5个请求,预期:先放开一个试试,并不会全部通过
     * 4. 再次并发5个请求,预期:由于第3步已经尝试一个成功,那么熔断器关闭
     *
     * @return
     * @throws InterruptedException
     */
    @SentinelResource(value = EXCEPTION_RATIO_RESOURCE, blockHandler = "handleBlockException")
    @GetMapping("/exceptionratio")
    public String testExceptionRatio() throws InterruptedException {
        int counter = exceptionRatioCounter.incrementAndGet();
        if (counter <= 5) {
            // 奇数抛出异常,共抛出3次,占60%,超过阈值
            if (counter % 2 == 1) {
                throw new RuntimeException("Unknown exception.");
            }
        }
        logger.info("Visit ExceptionRatio api successfully.");
        return "OK";
    }


    private DegradeRule getExceptionRatioDegradeRule() {
        DegradeRule degradeRule = new DegradeRule();
        //设置流控的资源
        degradeRule.setResource(EXCEPTION_RATIO_RESOURCE);
        // 统计耗时的模式
        degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
        // 40%以上的请求失败,触发熔断
        degradeRule.setCount(0.4);
        degradeRule.setStatIntervalMs(60_000);
        // 熔断时间,注意这里是秒
        degradeRule.setTimeWindow(30);
        return degradeRule;
    }

    public String fallback(Throwable throwable) {
        logger.error("Exception occurs!");
        return "Exception";
    }

运行结果:

示例3 异常数

	/**
     * 模拟场景:
     * 1. 并发5个请求,全部抛出异常,触发熔断
     * 2. 在熔断期内,并发5个请求,预期:全部熔断
     * 3. 熔断期(10秒)结束,并发5个请求,预期:先放开一个试试,并不会全部通过
     * 4. 再次并发5个请求,预期:由于第3步已经尝试一个成功,那么熔断器关闭
     *
     * @return
     * @throws InterruptedException
     */
    @SentinelResource(value = EXCEPTION_COUNT_RESOURCE, blockHandler = "handleBlockException", fallback = "fallback")
    @GetMapping("/exceptioncount")
    public String testExceptionCount() throws InterruptedException {
        int counter = exceptionCountCounter.incrementAndGet();
        if (counter <= 5) {
            throw new RuntimeException("Unknown exception.");
        }
        logger.info("Visit ExceptionCount api successfully.");
        return "OK";
    }


    private DegradeRule getExceptionCountDegradeRule() {
        DegradeRule degradeRule = new DegradeRule();
        //设置流控的资源
        degradeRule.setResource(EXCEPTION_COUNT_RESOURCE);
        // 统计耗时的模式
        degradeRule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
        // 5个以上的请求失败,触发熔断。注意,这里是开区间,必须是大于
        degradeRule.setCount(4);
        degradeRule.setMinRequestAmount(1);
        degradeRule.setStatIntervalMs(60_000);
        // 熔断时间,注意这里是秒
        degradeRule.setTimeWindow(10);
        return degradeRule;
    }

运行结果: