Sentinel熔断降级

sentinel流量控制

Sentinel流量控制&服务熔断降级介绍

流量控制介绍

image-20210303225557431

在这里我用景区的例子解释一下

一个旅游景点日接待游客数量为8K,8K以后的游客就无法买票进去景区。

对应编程来说就是,一个接口QPS(每秒请求数)最大为100,在QPS100之后的请求我们就要限制其访问,并给出友好提示。

不限制QPS无限的次数就会造成服务器的宕机。

服务熔断降级

在调用系统的时候,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积,进而导致级联错误

image-20210304102311130

而熔断降级就可以解决这个问题,所谓的熔断降级就是当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障

sentinel流量控制入门

创建本地应用

整体流程分析

  1. 创建springBoot项目
  2. 导入sentinel的坐标(此处使用为gradle)
  3. 创建Controller测试接口,定义限流规则同时引用限流规则

导入核心依赖

image-20210304103811187

implementation("com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel")

本地Sentinel控制台

下载sentinel控制台jar包

下载地址

启动Sentinel控制台

启动Sentinel控制台需要JDK版本为1.8及以上版本。

使用一下命令启动

java -Dserver.port=端口号 -jar sentinel-dashboard-1.8.0.jar

访问控制台

通过浏览器访问//localhost:端口号即可访问控制台,默认用户名密码都是sentinel

image-20210304121702160

Sentinel自定义资源方式

  1. 抛出异常的方式定义资源
  2. 返回布尔值方式定义资源
  3. 异步调用支持
  4. 注解方式定义资源
  5. 主流框架的默认适配

抛出异常的方式定义资源

Sentinel种的SphU包含了try-catch风格的API。用这种方式,当资源发生了限流之后会抛出BlockException

这个时候就可以捕获异常,进行限流之后的逻辑操作,在入门案例种就是使用此方式实现的,关键代码:

fun hello(): String {
        //使用限流规则
        try {
            SphU.entry("Hello")
        } catch (e: Exception) {
            return "系统繁忙,请稍后再试!!"
        }
        return "Hello Sentinel"
    }

特别地,若 entry 的时候传入了热点参数,那么 exit 的时候也一定要带上对应的参数(exit(count, args)),否则可能会有统计错误。这个时候不能使用 try-with-resources 的方式。另外通过 Tracer.trace(ex) 来统计异常信息时,由于 try-with-resources 语法中 catch 调用顺序的问题,会导致无法正确统计异常数,因此统计异常信息时也不能在 try-with-resources 的 catch 块中调用 Tracer.trace(ex)

手动 exit 示例:

Entry entry = null;
// 务必保证 finally 会被执行
try {
  // 资源名可使用任意有业务语义的字符串,注意数目不能太多(超过 1K),超出几千请作为参数传入而不要直接作为资源名
  // EntryType 代表流量类型(inbound/outbound),其中系统规则只对 IN 类型的埋点生效
  entry = SphU.entry("自定义资源名");
  // 被保护的业务逻辑
  // do something...
} catch (BlockException ex) {
  // 资源访问阻止,被限流或被降级
  // 进行相应的处理操作
} catch (Exception ex) {
  // 若需要配置降级规则,需要通过这种方式记录业务异常
  Tracer.traceEntry(ex, entry);
} finally {
  // 务必保证 exit,务必保证每个 entry 与 exit 配对
  if (entry != null) {
    entry.exit();
  }
}

热点参数埋点示例:

Entry entry = null;
try {
    // 若需要配置例外项,则传入的参数只支持基本类型。
    // EntryType 代表流量类型,其中系统规则只对 IN 类型的埋点生效
    // count 大多数情况都填 1,代表统计为一次调用。
    entry = SphU.entry(resourceName, EntryType.IN, 1, paramA, paramB);
    // Your logic here.
} catch (BlockException ex) {
    // Handle request rejection.
} finally {
    // 注意:exit 的时候也一定要带上对应的参数,否则可能会有统计错误。
    if (entry != null) {
        entry.exit(1, paramA, paramB);
    }
}

SphU.entry() 的参数描述:

参数名 类型 解释 默认值
entryType EntryType 资源调用的流量类型,是入口流量(EntryType.IN)还是出口流量(EntryType.OUT),注意系统规则只对 IN 生效 EntryType.OUT
count int 本次资源调用请求的 token 数目 1
args Object[] 传入的参数,用于热点参数限流

注意SphU.entry(xxx) 需要与 entry.exit() 方法成对出现,匹配调用,否则会导致调用链记录异常,抛出 ErrorEntryFreeException 异常。常见的错误:

  • 自定义埋点只调用 SphU.entry(),没有调用 entry.exit()
  • 顺序错误,比如:entry1 -> entry2 -> exit1 -> exit2,应该为 entry1 -> entry2 -> exit2 -> exit1

返回布尔值方式定义资源

Sentinel种的SphO包含了if-else风格的API。用这种方式,当资源发生了限流之后会返回 false ,这时候可以根据返回值,进行限流之后的逻辑处理。

  1. 在项目中创建TestBooleanController中使用返回booelan的方式定义资源

     @GetMapping("sentinel_boolean")
        fun hello(): Boolean {
            //配置限流入口,在这里是使用控制台进行配置限流规则
            return if (SphO.entry("Boolean")) {
                try {//被保护的资源
                    println("Hello Sentinel Boolean")
                    true
                } finally {
                    //限流出口
                    SphO.exit()
                }
            } else {
                //限流或者降级处理
                println("系统繁忙,请稍后重试")
                false
            }
        }
    

**在控制台进行流控规则的配置 **

image-20210304153534536

注意 SphO.entry(xxx)需要于SphO.exit()方法成对出现,匹配调用,位置正确,否则就会导致调用链记录异常,抛出ErrorEntryFreeException异常

异步调用支持

Sentinel 支持异步调用链路的统计。在异步调用中,需要通过 SphU.asyncEntry(xxx) 方法定义资源,并通常需要在异步的回调函数中调用 exit 方法。

  1. 启动类开启异步调用 @EnableAsync
  2. 开启方法的异步调用 @Async

SphU.asyncEntry(xxx) 不会影响当前(调用线程)的 Context,因此以下两个 entry 在调用链上是平级关系(处于同一层),而不是嵌套关系:

// 调用链类似于:
// -parent
// ---asyncResource
// ---syncResource
asyncEntry = SphU.asyncEntry(asyncResource);
entry = SphU.entry(normalResource);

若在异步回调中需要嵌套其它的资源调用(无论是 entry 还是 asyncEntry),只需要借助 Sentinel 提供的上下文切换功能,在对应的地方通过 ContextUtil.runOnContext(context, f) 进行 Context 变换,将对应资源调用处的 Context 切换为生成的异步 Context,即可维持正确的调用链路关系。示例如下:

public void handleResult(String result) {
    Entry entry = null;
    try {
        entry = SphU.entry("handleResultForAsync");
        // Handle your result here.
    } catch (BlockException ex) {
        // Blocked for the result handler.
    } finally {
        if (entry != null) {
            entry.exit();
        }
    }
}

public void someAsync() {
    try {
        AsyncEntry entry = SphU.asyncEntry(resourceName);

        // Asynchronous invocation.
        doAsync(userId, result -> {
            // 在异步回调中进行上下文变换,通过 AsyncEntry 的 getAsyncContext 方法获取异步 Context
            ContextUtil.runOnContext(entry.getAsyncContext(), () -> {
                try {
                    // 此处嵌套正常的资源调用.
                    handleResult(result);
                } finally {
                    entry.exit();
                }
            });
        });
    } catch (BlockException ex) {
        // Request blocked.
        // Handle the exception (e.g. retry or fallback).
    }
}

此时的调用链就类似于:

-parent
---asyncInvocation
-----handleResultForAsync

更详细的示例可以参考 Demo 中的 AsyncEntryDemo,里面包含了普通资源与异步资源之间的各种嵌套示例。

注解方式定义资源

Sentinel 支持通过 @SentinelResource 注解定义资源并配置 blockHandlerfallback 函数来进行限流之后的处理。使用 Sentinel Annotation AspectJ Extension 的时候需要引入以下依赖

implementation("com.alibaba.csp:sentinel-annotation-aspectj:1.7.2")

@RestController
class TestAnnController {
    /**
     * SentinelResource: 定义支援
     * value:设置资源的名称
     * blockHandler:设置降级或者限流的处理函数
     */
    @SentinelResource(value = "sentinel_ann", blockHandler = "exceptionHandler")
    @GetMapping("ann")
    fun hello(): String{
        return "hello Sentinel"
    }

    /**
     * 限流或者降级的函数
     */
    fun exceptionHandler(ex: BlockException) :String {
        ex.printStackTrace()
        return "系统繁忙,请稍后"
    }
}

@SentinelResource 注解

注意:注解方式埋点不支持 private 方法。

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

  • value:资源名称,必需项(不能为空)
  • entryType:entry 类型,可选项(默认为 EntryType.OUT
  • blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • fallback/fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

1.8.0 版本开始,defaultFallback 支持在类级别进行配置。

注:1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException)进行处理,不能针对业务异常进行处理

特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandlerfallbackdefaultFallback,则被限流降级时会将 BlockException 直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException)。

示例:

public class TestService {

    // 原函数
    @SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback")
    public String hello(long s) {
        return String.format("Hello at %d", s);
    }
    
    // Fallback 函数,函数签名与原函数一致或加一个 Throwable 类型的参数.
    public String helloFallback(long s) {
        return String.format("Halooooo %d", s);
    }

    // Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
    public String exceptionHandler(long s, BlockException ex) {
        // Do some log here.
        ex.printStackTrace();
        return "Oops, error occurred at " + s;
    }

    // 这里单独演示 blockHandlerClass 的配置.
    // 对应的 `handleException` 函数需要位于 `ExceptionUtil` 类中,并且必须为 public static 函数.
    @SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
    public void test() {
        System.out.println("Test");
    }
}

从 1.4.0 版本开始,注解方式定义资源支持自动统计业务异常,无需手动调用 Tracer.trace(ex) 来记录业务异常。Sentinel 1.4.0 以前的版本需要自行调用 Tracer.trace(ex) 来记录业务异常。

配置

Spring Cloud Alibaba

若您是通过 Spring Cloud Alibaba 接入的 Sentinel,则无需额外进行配置即可使用 @SentinelResource 注解。

Spring AOP

若您的应用使用了 Spring AOP(无论是 Spring Boot 还是传统 Spring 应用),您需要通过配置的方式将 SentinelResourceAspect 注册为一个 Spring Bean:

@Configuration
public class SentinelAspectConfiguration {

    @Bean
    public SentinelResourceAspect sentinelResourceAspect() {
        return new SentinelResourceAspect();
    }
}

我们提供了 Spring AOP 的示例,可以参见 sentinel-demo-annotation-spring-aop

AspectJ

若您的应用直接使用了 AspectJ,那么您需要在 aop.xml 文件中引入对应的 Aspect:

<aspects>
    <aspect name="com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect"/>
</aspects>

主流框架的默认适配

image-20210304170247125

文档地址://github.com/alibaba/Sentinel/wiki/主流框架的适配#spring-cloud

规则的种类

Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效。同时 Sentinel 也提供相关 API,供您来定制自己的规则策略。

Sentinel 支持以下几种规则:流量控制规则熔断降级规则系统保护规则来源访问控制规则热点参数规则

流量控制规则 (FlowRule)

流量规则的定义

重要属性:

Field 说明 默认值
resource 资源名,资源名是限流规则的作用对象
count 限流阈值
grade 限流阈值类型,QPS 模式(1)或并发线程数模式(0) QPS 模式
limitApp 流控针对的调用来源 default,代表不区分调用来源
strategy 调用关系限流策略:直接、链路、关联 根据资源本身(直接)
controlBehavior 流控效果(直接拒绝/WarmUp/匀速+排队等待),不支持按调用关系限流 直接拒绝
clusterMode 是否集群限流

同一个资源可以同时有多个限流规则,检查规则时会依次检查。

通过代码定义流量控制规则

理解上面规则的定义之后,我们可以通过调用 FlowRuleManager.loadRules() 方法来用硬编码的方式定义流量控制规则,比如:

private void initFlowQpsRule() {
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule(resourceName);
    // set limit qps to 20
    rule.setCount(20);
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    rule.setLimitApp("default");
    rules.add(rule);
    FlowRuleManager.loadRules(rules);
}