Spring Cloud Alibaba Sentinel

2022/4/26 23:43:20

本文主要是介绍Spring Cloud Alibaba Sentinel,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、什么是Sentinel

Sentinel是阿里开源的项目,提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性。
官网:https://github.com/alibaba/Sentinel/wiki

2012年,Sentinel诞生于阿里巴巴,其主要目标是流量控制。2013-2017年,Sentinel迅速发展,并成为阿里巴巴所有微服务的基本组成部分。它已在6000多个应用程序中使用,涵盖了几乎所有核心电子商务场景。2018年,Sentinel演变为一个开源项目。2020年,Sentinel Golang发布。

1.1 Sentinel具有以下特征:

丰富的应用场景Sentinel承接了阿里巴巴近10年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控Sentinel同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至500台以下规模的集群的汇总运行情况。
广泛的开源生态Sentinel提供开箱即用的与其它开源框架/库的整合模块,例如与Spring CloudDubbogRPC的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel
完善的SPI扩展点Sentinel提供简单易用、完善的SPI扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel的生态圈

img

1.2 Sentinel主要特性:

20200529140109886.png

关于SentinelHystrix的区别见:https://yq.aliyun.com/articles/633786/

到这已经学习Sentinel的基本的使用,在很多的特性和Hystrix有很多类似的功能。以下是SentinelHystrix的对比。

1.3 Sentinel的使用

Sentinel的使用可以分为两个部分:

  • 控制台(Dashboard):控制台主要负责管理推送规则、监控、集群限流分配管理、机器发现等。
  • 核心库(Java客户端):不依赖任何框架/库,能够运行于Java 7及以上的版本的运行时环境,同时对Dubbo/Spring Cloud等框架也有较好的支持。

1.3.1 获取Sentinel控制台

您可以从release页面下载最新版本的控制台jar包。

您也可以从最新版本的源码自行构建Sentinel控制台:

  • 下载控制台工程
  • 使用以下命令将代码打包成一个fat jar:mvn clean package

1.3.2 sentinel服务启动

java -server -Xms64m -Xmx256m -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.7.1.jar

开机启动:启动命令可以加入到启动的rc.local配置文件,之后做到开机启动

# 启动sentinel
/usr/bin/su - root -c "nohup java -server -Xms64m -Xmx256m -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.7.1.jar 2>&1 &"

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。

由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出DegradeException)。

关于熔断降级的介绍见:Sentinel熔断降级。

下面就使用基于注解的方式实现Sentinel的熔断降级的demo

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

使用如下命令启动控制台:

nohup java -server -Xms64m -Xmx256m -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.7.1.jar &

其中-Dserver.port=8849用于指定Sentinel控制台端口为8849,这个端口可以按需指定。

Sentinel 1.6.0起,Sentinel控制台引入基本的登录功能,默认用户名和密码都是sentinel。可以参考鉴权模块文档配置用户名和密码。

注:若您的应用为Spring BootSpring Cloud应用,您可以通过Spring配置文件来指定配置,详情请参考Spring Cloud Alibaba Sentinel文档。
(1)获取Sentinel控制台
您可以从官方网站中下载最新版本的控制台jar包,下载地址如下:https://github.com/alibaba/Sentinel/releases/download/1.6.3/sentinel-dashboard-1.7.1.jar
(2)启动
使用如下命令启动控制台:
其中-Dserver.port=8888用于指定Sentinel控制台端口为8888。

[root@192 ~]# java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8888 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.6.3.jar
INFO: log base dir is: /root/logs/csp/
INFO: log name use pid is: false

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.5.RELEASE)

2020-02-08 13:07:29.316  INFO 114031 --- [           main] c.a.c.s.dashboard.DashboardApplication   : Starting DashboardApplication on 192.168.180.137 with PID 114031 (/root/sentinel-dashboard-1.6.3.jar started by root in /root)
2020-02-08 13:07:29.319  INFO 114031 --- [           main] c.a.c.s.dashboard.DashboardApplication   : No active profile set, falling back to default profiles: default
2020-02-08 13:07:29.456  INFO 114031 --- [           main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@59690aa4: startup date [Sat Feb 08 13:07:29 CST 2020]; root of context hierarchy
2020-02-08 13:07:33.783  INFO 114031 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8888 (http)

1991794415.png

查看机器列表以及健康情况
默认情况下Sentinel会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包。也可以配置sentinel.eager=true,取消Sentinel控制台懒加载。打开浏览器即可展示Sentinel的管理控制台

img

1.3.3 客户端接入控制台

控制台启动后,客户端需要按照以下步骤接入到控制台。

父工程引入alibaba实现的SpringCloud

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.1.0.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

子工程中引入sentinel

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

在工程的application.yml中添加Sentinel控制台配置信息

spring:
  cloud:
    sentinel:
      transport:
        dashboard: 192.168.180.137:8888   #sentinel控制台的请求地址

这里的spring.cloud.sentinel.transport.dashboard配置控制台的请求路径。

1.4 Sentinel与Hystrix的区别

image

迁移方案
Sentinel官方提供了详细的由Hystrix迁移到Sentinel的方法

1829785-20200208125155175-979540425.png

二、使用Sentinel来进行熔断与限流

Sentinel可以简单的分为Sentinel核心库和Dashboard。核心库不依赖Dashboard,但是结合Dashboard可以取得最好的效果。
使用Sentinel来进行熔断保护,主要分为几个步骤:

  1. 定义资源

    资源:可以是任何东西,一个服务,服务里的方法,甚至是一段代码。

  2. 定义规则

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

  3. 检验规则是否生效

Sentinel的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效.先把可能需要保护的资源定义好,之后再配置规则。

也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保
护,就将之定义为一个资源。

2.1 定义资源

资源是Sentinel的关键概念。它可以是Java应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,RPC接口方法,甚至可以是一段代码。只要通过Sentinel API定义的代码,就是资源,能够被Sentinel保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。把需要控制流量的代码用Sentinel的关键代码SphU.entry("资源名")和entry.exit()包围起来即可。

实例代码:

Entry entry = null;
    try {
        // 定义一个sentinel保护的资源,名称为test-sentinel-api
        entry = SphU.entry(resourceName);
        // 模拟执行被保护的业务逻辑耗时
        Thread.sleep(100);
        return a;
    } catch (BlockException e) {
        // 如果被保护的资源被限流或者降级了,就会抛出BlockException
        log.warn("资源被限流或降级了", e);
        return "资源被限流或降级了";
    } catch (InterruptedException e) {
        return "发生InterruptedException";
    } finally {
        if (entry != null) {
            entry.exit();
        }
        ContextUtil.exit();
    }
}

在下面的例子中,用try-with-resources来定义资源。参考代码如下:

public static void main(String[] args) {
    // 配置规则.
    initFlowRules();
    while (true) {
        // 1.5.0 版本开始可以直接利用 try-with-resources 特性
        try (Entry entry = SphU.entry("HelloWorld")) {
            // 被保护的逻辑
            System.out.println("hello world");
	    } catch (BlockException ex) {
            // 处理被流控的逻辑
	        System.out.println("blocked!");
	    }
    }
}

资源注解@SentinelResource

也可以使用Sentinel提供的注解@SentinelResource来定义资源,实例如下:

@SentinelResource("HelloWorld")
public void helloWorld() {
    // 资源中的逻辑
    System.out.println("hello world");
}

@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里面排除掉的异常类型)进行处理。
  • defaultFallback(since 1.6.0):默认的fallback函数名称,可选项,通常用于通用的fallback逻辑(即可以用于很多服务或方法)。默认fallback函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。若同时配置了fallback和defaultFallback,则只有fallback会生效。

fallback函数签名和位置要求:

  • 返回值类型必须与原函数返回值类型一致;
  • 方法参数列表需要和原函数一致,或者可以额外多一个Throwable类型的参数用于接收对应的异常。
  • fallback函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定fallbackClass为对应的类的Class对象,注意对应的函数必需为static函数,否则无法解析。

defaultFallback函数签名要求:

  • 返回值类型必须与原函数返回值类型一致;
  • 方法参数列表需要为空,或者可以额外多一个Throwable类型的参数用于接收对应的异常。
  • defaultFallback函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定fallbackClass为对应的类的Class对象,注意对应的函数必需为static函数,否则无法解析。
  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入fallback逻辑中,而是会原样抛出。

2.2 定义规则

规则主要有流控规则、熔断降级规则、系统规则、权限规则、热点参数规则等。一段硬编码的方式定义流量控制规则如下:

private void initSystemRule() {
    List<SystemRule> rules = new ArrayList<>();
    SystemRule rule = new SystemRule();
    rule.setHighestSystemLoad(10);
    rules.add(rule);
    SystemRuleManager.loadRules(rules);
}

加载规则:

FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控规则
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降级规则
SystemRuleManager.loadRules(List<SystemRule> rules); // 修改系统规则
AuthorityRuleManager.loadRules(List<AuthorityRule> rules); // 修改授权规则

三、sentinel熔断降级

3.1 什么是熔断降级

熔断降级对调用链路中不稳定的资源进行熔断降级是保障高可用的重要措施之一。

由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出DegradeException)

3.2 熔断降级规则

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

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 引入)

3.3 降级策略

我们通常用以下几种降级策略:

  • 平均响应时间(DEGRADE_GRADE_RT):当资源的平均响应时间超过阈值(DegradeRule中的count,以ms为单位)之后,资源进入准降级状态。如果接下来1s内持续进入5个请求(即QPS >= 5),它们的RT都持续超过这个阈值,那么在接下的时间窗口(DegradeRule中的timeWindow,以s为单位)之内,对这个方法的调用都会自动地熔断(抛出DegradeException)。

    注意Sentinel默认统计的RT上限是4900ms,超出此阈值的都会算作4900ms,若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt=xxx来配置。

  • 异常比例(DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒异常总数占通过量的比值超过阈值(DegradeRule中的count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule中的timeWindow,以s为单位)之内,对这个方法的调用都会自动地返回。

    异常比率的阈值范围是[0.0,1.0],代表0%-100%。

  • 异常数(DEGRADE_GRADE_EXCEPTION_COUNT):当资源近1分钟的异常数目超过阈值之后会进行熔断。

    注意由于统计时间窗口是分钟级别的,若timeWindow小于60s,则结束熔断状态后仍可能再进入熔断状态。

3.4 熔断降级代码实现

可以通过调用DegradeRuleManager.loadRules()方法来用硬编码的方式定义流量控制规则。

@PostConstruct
public void initSentinelRule() {
    //熔断规则:5s内调用接口出现异常次数超过5的时候, 进行熔断
    List<DegradeRule> degradeRules = new ArrayList<>();
    DegradeRule rule = new DegradeRule();
    rule.setResource("queryGoodsInfo");
    rule.setCount(5);

    rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);//熔断规则
    rule.setTimeWindow(5);
    degradeRules.add(rule);
    DegradeRuleManager.loadRules(degradeRules);
}

3.5 控制台降级规则

配置

20210116194700580.png

参数

Field 说明 默认值
resource 资源名,即限流规则的作用对象
count 阈值
grade 降级模式,根据RT降级还是根据异常比例降级 RT
timeWindow 降级的时间,单位为s

3.6 与Hystrix的熔断对比

Hystrix常用的线程池隔离会造成线程上下切换的overhead比较大;Hystrix使用的信号量隔离对某个资源调用的并发数进行控制,效果不错,但是无法对慢调用进行自动降级;
Sentinel通过并发线程数的流量控制提供信号量隔离的功能;此外,Sentinel支持的熔断降级维度更多,可对多种指标进行流控、熔断,且提供了实时监控和控制面板,功能更为强大。

四、Sentinel流控(限流)

4.1 流量控制

流量控制(Flow Control),原理是监控应用流量的QPS或并发线程数等指标,当达到指定阈值时对流量进行控制,避免系统被瞬时的流量高峰冲垮,保障应用高可用性。
通过流控规则来指定允许该资源通过的请求次数,例如下面的代码定义了资源HelloWorld每秒最多只能通过20个请求。参考的规则定义如下:

private static void initFlowRules(){
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule();
    rule.setResource("HelloWorld");
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    // Set limit QPS to 20.
    rule.setCount(20);
    rules.add(rule);
    FlowRuleManager.loadRules(rules);
}

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

  • resource:资源名,即限流规则的作用对象
  • count: 限流阈值
  • grade: 限流阈值类型(QPS或并发线程数)
  • limitApp: 流控针对的调用来源,若为default则不区分调用来源
  • strategy: 调用关系限流策略
  • controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)

基本的参数

资源名 :唯一名称,默认请求路径
针对来源 :Sentinel可以针对调用者进行限流,填写微服务名,默认为default(不区分来源)

阈值类型/单机阈值:

  1. QPS:每秒请求数,当前调用该api的QPS到达阈值的时候进行限流
  2. 线程数:当调用该api的线程数到达阈值的时候,进行限流

是否集群 :是否为集群

流控的几种strategy

  1. 直接:当api大达到限流条件时,直接限流
  2. 关联:当关联的资源到达阈值,就限流自己
  3. 链路:只记录指定路上的流量,指定资源从入口资源进来的流量,如果达到阈值,就进行限流,api级别的限流

4.1 直接失败模式

使用API进行资源定义

/**
 * 限流实现方式一: 抛出异常的方式定义资源
 *
 * @param orderId
 * @return
 */
@ApiOperation(value = "纯代码限流")
@GetMapping("/getOrder")
@ResponseBody
public String getOrder(@RequestParam(value = "orderId", required = false)String orderId) {

    Entry entry = null;
    // 资源名
    String resourceName = "getOrder";
    try {
        // entry可以理解成入口登记
        entry = SphU.entry(resourceName);
        // 被保护的逻辑, 这里为订单查询接口
        return "正常的业务逻辑 OrderInfo :" + orderId;
    } catch (BlockException blockException) {
        // 接口被限流的时候, 会进入到这里
        log.warn("---getOrder1接口被限流了---, exception: ", blockException);
        return "接口限流, 返回空";
    } finally {
        // SphU.entry(xxx)需要与entry.exit()成对出现,否则会导致调用链记录异常
        if (entry != null) {
            entry.exit();
        }
    }
}

代码限流规则

//限流规则QPS mode,
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource("getOrder");
// QPS控制在2以内
rule1.setCount(2);
// QPS限流
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
FlowRuleManager.loadRules(rules);

网页限流规则配置

选择QPS,直接,快速失败,单机阈值为2。

配置

20210116194515175.png

参数

Field 说明 默认值
resource 资源名,资源名是限流规则的作用对象
count 限流阈值
grade 限流阈值类型,QPS或线程数模式 QPS模式
limitApp 流控针对的调用来源 default,代表不区分调用来源
strategy 判断的根据是资源自身,还是根据其它关联资源 (refResource),还是根据链路入口 根据资源本身
controlBehavior 流控效果(直接拒绝/排队等待/慢启动模式) 直接拒绝

测试

频繁刷新请求,1秒访问2次请求,正常,超过设置的阈值,将报默认的错误。

img

再次的1秒访问2次请求,访问正常。超过2次,访问异常

4.2 关联模式

调用关系包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关系。Sentinel通过NodeSelectorSlot建立不同资源间的调用的关系,并且通过ClusterBuilderSlot记录每个资源的实时统计信息。当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢。举例来说,read_dbwrite_db这两个资源分别代表数据库读写,我们可以给read_db设置限流规则来达到写优先的目的。具体的方法:

设置`strategy`为`RuleConstant.STRATEGY_RELATE`
设置`refResource`为`write_db`。
这样当写库操作过于频繁时,读数据的请求会被限流。

还有一个例子,电商的下订单和支付两个操作,需要优先保障支付,可以根据支付接口的流量阈值,来对订单接口进行限制,从而保护支付的目的。

使用注解进行资源定义

添加2个请求

@SentinelResource(value = "test1", blockHandler = "exceptionHandler")
@GetMapping("/test1")
public String test1() {
    log.info(Thread.currentThread().getName() + "\t" + "...test1");
    return "-------hello baby,i am test1";
}

//Block异常处理函数,参数最后多一个BlockException,其余与原函数一致.
public String exceptionHandler(BlockException ex) {
    // Do some log here.
    ex.printStackTrace();
    log.info(Thread.currentThread().getName() + "\t" + "...exceptionHandler");
    return String.format("error: test1  is not OK");
}

@SentinelResource(value = "test1_ref")
@GetMapping("/test1_ref")
public String test1_ref() {
    log.info(Thread.currentThread().getName() + "\t" + "...test1_related");
    return "-------hello baby,i am test1_ref";
}

代码配置关联限流规则

// 关联模式流控  QPS控制在1以内
String refResource = "test1_ref";
FlowRule rRule = new FlowRule("test1")
        .setCount(1)  // QPS控制在1以内
        .setStrategy(RuleConstant.STRATEGY_RELATE)
        .setRefResource(refResource);

rules.add(rRule);
FlowRuleManager.loadRules(rules);

网页限流规则配置

20210115180339225.png

测试

选择QPS,单机阈值为1,选择关联,关联资源为/test_ref,这里用Jmeter模拟高并发,请求/test_ref。

img

在大批量线程高并发访问/test_ref,导致/test失效了

img

链路类型的关联也类似,就不再演示了。多个请求调用同一微服务。

4.3 Warm up(预热)模式

当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值。Warm Up(冷启动,预热)模式就是为了实现这个目的的。

默认coldFactor为3,即请求QPS从threshold/3开始,经预热时长逐渐升至设定的QPS阈值。

使用注解定义资源

@SentinelResource(value = "testWarmUP", blockHandler = "exceptionHandlerOfWarmUp")
@GetMapping("/testWarmUP")
public String testWarmUP() {
    log.info(Thread.currentThread().getName() + "\t" + "...test1");
    return "-------hello baby,i am testWarmUP";
}

代码限流规则

FlowRule warmUPRule = new FlowRule();
warmUPRule.setResource("testWarmUP");
warmUPRule.setCount(20);
warmUPRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
warmUPRule.setLimitApp("default");
warmUPRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
warmUPRule.setWarmUpPeriodSec(10);

网页限流规则配置

20210115201946419.png

先在单机阈值10/3,3的时候,预热10秒后,慢慢将阈值升至20。刚开始刷/testWarmUP,会出现默认错误,预热时间到了后,阈值增加,没超过阈值刷新,请求正常。

通常冷启动的过程系统允许通过的QPS曲线如下图所示:

img

如秒杀系统在开启瞬间,会有很多流量上来,很可能把系统打死,预热方式就是为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。

通过jmeter进行测试

4.4 排队等待模式

匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。阈值必须设置为QPS。

img

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

某瞬时来了大流量的请求,而如果此时要处理所有请求,很可能会导致系统负载过高,影响稳定性。但其实可能后面几秒之内都没有消息投递,若直接把多余的消息丢掉则没有充分利用系统处理消息的能力。Sentinel的Rate Limiter模式能在某一段时间间隔内以匀速方式处理这样的请求,充分利用系统的处理能力,也就是削峰填谷,保证资源的稳定性.

Sentinel会以固定的间隔时间让请求通过,访问资源。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的timeout时间,则该请求会等待直到预设时间到来通过;反之,则马上抛出阻塞异常。

使用Sentinel的这种策略,简单点说,就是使用一个时间段(比如20s的时间)处理某一瞬时产生的大量请求,起到一个削峰填谷的作用,从而充分利用系统的处理能力,下图能很形象的展示这种场景:X轴代表时间,Y轴代表系统处理的请求.

20190219142010891.png

示例

模拟2个用户同时并发的访问资源,发出100个请求,如果设置QPS阈值为1,拒绝策略修改为Rate Limiter匀速RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER方式,还需要设置setMaxQueueingTimeMs(20*1000)表示每一请求最长等待时间,这里等待时间大一点,以保证让所有请求都能正常通过;

假设这里设置的排队等待时间过小的话,导致排队等待的请求超时而抛出异常BlockException,最终结果可能是这100个并发请求中只有一个请求或几个才能正常通过,所以使用这种模式得根据访问资源的耗时时间决定排队等待时间. 按照目前这种设置,QPS阈值为10的话,每一个请求相当于是以匀速100ms左右通过.

使用注解定义资源

@SentinelResource(value = "testLineUp", blockHandler = "exceptionHandlerOftestLineUp")
@GetMapping("/testLineUp")
public String testLineUp() {
    log.info(Thread.currentThread().getName() + "\t" + "...test1");
    return "-------hello baby,i am testLineUp";
}

代码限流规则

FlowRule lineUpRule = new FlowRule();
lineUpRule.setResource("testLineUp");
lineUpRule.setCount(10);
lineUpRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
lineUpRule.setLimitApp("default");
lineUpRule.setMaxQueueingTimeMs(20 * 1000);
// CONTROL_BEHAVIOR_DEFAULT means requests more than threshold will be rejected immediately.
// CONTROL_BEHAVIOR_DEFAULT将超过阈值的流量立即拒绝掉.
lineUpRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
rules.add(lineUpRule);

网页限流规则配置

20210115201054724.png

通过jmeter进行测试

20210115201802998.png

4.5 热点规则(ParamFlowRule)

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的Top K数据,并对其访问进行限制。比如:

  • 商品ID为参数,统计一段时间内最常购买的商品ID并进行限制
  • 用户ID为参数,针对一段时间内频繁访问的用户ID进行限制热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。使用该规则需要引入依赖:

热点参数规则(ParamFlowRule)类似于流量控制规则(FlowRule):

属性 说明 默认值
resource 资源名,必填
count 限流阈值,必填
grade 限流模式 QPS模式
durationInSec 统计窗口时间长度(单位为秒),1.6.0版本开始支持 1s
controlBehavior 流控效果(支持快速失败和匀速排队模式),1.6.0版本开始支持 快速失败
maxQueueingTimeMs 最大排队等待时长(仅在匀速排队模式生效),1.6.0版本开始支持 0ms
paramIdx 热点参数的索引,必填,对应SphU.entry(xxx, args)中的参数索引位置
paramFlowItemList 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面count阈值的限制。仅支持基本类型和字符串类型
clusterMode 是否是集群参数流控规则 false
clusterConfig 集群流控相关配置

自定义资源

@GetMapping("/byHotKey")
@SentinelResource(value = "byHotKey", blockHandler = "userAccessError")
public String test4(@RequestParam(value = "userId", required = false) String userId,
                    @RequestParam(value = "goodId", required = false) int goodId) {
    log.info(Thread.currentThread().getName() + "\t" + "...byHotKey");
    return "-----------by HotKey: UserId";
}

限流规则代码:

可以通过ParamFlowRuleManager的loadRules方法更新热点参数规则,下面是官方实例:

ParamFlowRule rule = new ParamFlowRule(resourceName)
    .setParamIdx(0)
    .setCount(5);
// 针对int类型的参数PARAM_B,单独设置限流QPS阈值为10,而不是全局的阈值5.
ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B))
    .setClassType(int.class.getName())
    .setCount(10);
rule.setParamFlowItemList(Collections.singletonList(item));
ParamFlowRuleManager.loadRules(Collections.singletonList(rule));

具体的限流代码如下:

ParamFlowRule pRule = new ParamFlowRule("byHotKey")
    .setParamIdx(1)
    .setCount(1);
// 针对参数值1000,单独设置限流QPS阈值为5,而不是全局的阈值1.
ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(1000))
        .setClassType(int.class.getName())
        .setCount(5);
pRule.setParamFlowItemList(Collections.singletonList(item));
ParamFlowRuleManager.loadRules(Collections.singletonList(pRule));

网页限流规则配置

20210116194057665.png

五、Sentinel系统保护

系统保护的目的

在开始之前,我们先了解一下系统保护的目的:

  • 保证系统不被拖垮
  • 在系统稳定的前提下,保持系统的吞吐量

长期以来,系统保护的思路是根据硬指标,即系统的负载(load1)来做系统过载保护。当系统负载高于某个阈值,就禁止或者减少流量的进入;当load开始好转,则恢复流量的进入。这个思路给我们带来了不可避免的两个问题:

  • load是一个“结果”,如果根据load的情况来调节流量的通过率,那么就始终有延迟性。也就意味着通过率的任何调整,都会过一段时间才能看到效果。当前通过率是使load恶化的一个动作,那么也至少要过1秒之后才能观测到;同理,如果当前通过率调整是让load好转的一个动作,也需要1秒之后才能继续调整,这样就浪费了系统的处理能力。所以我们看到的曲线,总是会有抖动。
  • 恢复慢。想象一下这样的一个场景(真实),出现了这样一个问题,下游应用不可靠,导致应用RT很高,从而load到了一个很高的点。过了一段时间之后下游应用恢复了,应用RT也相应减少。这个时候,其实应该大幅度增大流量的通过率;但是由于这个时候load仍然很高,通过率的恢复仍然不高。

系统保护的目标是在系统不被拖垮的情况下,提高系统的吞吐率,而不是load一定要到低于某个阈值。如果我们还是按照固有的思维,超过特定的load就禁止流量进入,系统load恢复就放开流量,这样做的结果是无论我们怎么调参数,调比例,都是按照果来调节因,都无法取得良好的效果。

Sentinel在系统自适应保护的做法是,用load1作为启动自适应保护的因子,而允许通过的流量由处理请求的能力,即请求的响应时间以及当前系统正在处理的请求速率来决定。

系统保护规则的应用

系统规则支持以下的模式:

  • Load自适应(仅对Linux/Unix-like机器生效):系统的load1作为启发指标,进行自适应系统保护。当系统load1超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR阶段)。系统容量由系统的maxQps * minRt估算得出。设定参考值一般是CPU cores * 2.5
  • CPU usage (1.5.0+ 版本):当系统CPU使用率超过阈值即触发系统保护(取值范围0.0-1.0),比较灵敏。
  • 平均RT :当单台机器上所有入口流量的平均RT达到阈值即触发系统保护,单位是毫秒。
  • 并发线程数 :当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口QPS :当单台机器上所有入口流量的QPS达到阈值即触发系统保护。

系统保护规则是从应用级别的入口流量进行控制,从单台机器的load、CPU使用率、平均RT、入口QPS和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如Web服务或Dubbo服务端接收的请求,都属于入口流量。

系统规则的参数说明:

  • highestSystemLoad最大的load1,参考值-1(不生效)
  • avgRt所有入口流量的平均响应时间-1(不生效)
  • maxThread入口流量的最大并发数-1(不生效)
  • qps所有入口资源的QPS -1(不生效)

硬编码的方式定义流量控制规则如下:

List<SystemRule> srules = new ArrayList<>();
SystemRule srule = new SystemRule();
srule.setAvgRt(3000);
srules.add(srule);
SystemRuleManager.loadRules(srules);

网页限流规则配置

20210116194809712.png

六、黑白名单规则

很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用Sentinel的访问控制(黑白名单)的功能。黑白名单根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

调用方信息通过ContextUtil.enter(resourceName, origin)方法中的origin参数传入。

6.1 访问控制规则(AuthorityRule)

授权规则,即黑白名单规则(AuthorityRule)非常简单,主要有以下配置项:

  • resource:资源名,即限流规则的作用对象
  • limitApp:对应的黑名单/白名单,不同origin用,分隔,如appA,appB
  • strategy:限制模式,AUTHORITY_WHITE为白名单模式,AUTHORITY_BLACK为黑名单模式,默认为白名单模式比如我们希望控制对资源test的访问设置白名单,只有来源为appA和appB的请求才可通过,则可以配置如下白名单规则:
AuthorityRule rule = new AuthorityRule();
rule.setResource("test");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));

七、核心组件

Resource

resource是sentinel中最重要的一个概念,sentinel通过资源来保护具体的业务代码或其他后方服务。sentinel把复杂的逻辑给屏蔽掉了,用户只需要为受保护的代码或服务定义一个资源,然后定义规则就可以了,剩下的通通交给sentinel来处理了。并且资源和规则是解耦的,规则甚至可以在运行时动态修改。定义完资源后,就可以通过在程序中埋点来保护你自己的服务了,埋点的方式有两种:

  • try-catch方式(通过SphU.entry(...)),当catch到BlockException时执行异常处理(或fallback)
  • if-else方式(通过SphO.entry(...)),当返回false时执行异常处理(或fallback)

以上这两种方式都是通过硬编码的形式定义资源然后进行资源埋点的,对业务代码的侵入太大,从0.1.1版本开始,sentinel加入了注解的支持,可以通过注解来定义资源,具体的注解为:SentinelResource。通过注解除了可以定义资源外,还可以指定blockHandler和fallback方法。

在sentinel中具体表示资源的类是:ResourceWrapper,他是一个抽象的包装类,包装了资源的Name和EntryType。他有两个实现类,分别是:StringResourceWrapper和MethodResourceWrapper。顾名思义,StringResourceWrapper是通过对一串字符串进行包装,是一个通用的资源包装类,MethodResourceWrapper是对方法调用的包装。

Context

Context是对资源操作时的上下文环境,每个资源操作(针对Resource进行的entry/exit)必须属于一个Context,如果程序中未指定Context,会创建name为"sentinel_default_context"的默认Context。一个Context生命周期内可能有多个资源操作,Context生命周期内的最后一个资源exit时会清理该Context,这也预示这整个Context生命周期的结束。Context主要属性如下:

public class Context {
    // context名字,默认名字 "sentinel_default_context"
    private final String name;
    // context入口节点,每个context必须有一个entranceNode
    private DefaultNode entranceNode;
    // context当前entry,Context生命周期中可能有多个Entry,所有curEntry会有变化
    private Entry curEntry;
    // The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP).
    private String origin = "";
    private final boolean async;
}

注意:一个Context生命期内Context只能初始化一次,因为是存到ThreadLocal中,并且只有在非null时才会进行初始化。

如果想在调用SphU.entry()或SphO.entry()前,自定义一个context,则通过ContextUtil.enter()方法来创建。context是保存在ThreadLocal中的,每次执行的时候会优先到ThreadLocal中获取,为null时会调用MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType())创建一个context。当Entry执行exit方法时,如果entry的parent节点为null,表示是当前Context中最外层的Entry了,此时将ThreadLocal中的context清空。

Context的创建与销毁

首先我们要清楚的一点就是,每次执行entry()方法,试图冲破一个资源时,都会生成一个上下文。这个上下文中会保存着调用链的根节点和当前的入口。

Context是通过ContextUtil创建的,具体的方法是trueEntry,代码如下:

protected static Context trueEnter(String name, String origin) {
    // 先从ThreadLocal中获取
    Context context = contextHolder.get();
    if (context == null) {
        // 如果ThreadLocal中获取不到Context
        // 则根据name从map中获取根节点,只要是相同的资源名,就能直接从map中获取到node
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        DefaultNode node = localCacheNameMap.get(name);
        if (node == null) {
            // 省略部分代码
            try {
                LOCK.lock();
                node = contextNameNodeMap.get(name);
                if (node == null) {
                    // 省略部分代码
                    // 创建一个新的入口节点
                    node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                    Constants.ROOT.addChild(node);
                    // 省略部分代码
                }
            } finally {
                LOCK.unlock();
            }
        }
        // 创建一个新的Context,并设置Context的根节点,即设置EntranceNode
        context = new Context(node, name);
        context.setOrigin(origin);
        // 将该Context保存到ThreadLocal中去
        contextHolder.set(context);
    }
    return context;
}

上面的代码中我省略了部分代码,只保留了核心的部分。从源码中还是可以比较清晰的看出生成Context的过程:

  • 1.先从ThreadLocal中获取,如果能获取到直接返回,如果获取不到则继续第2步
  • 2.从一个static的map中根据上下文的名称获取,如果能获取到则直接返回,否则继续第3步
  • 3.加锁后进行一次double check,如果还是没能从map中获取到,则创建一个EntranceNode,并把该EntranceNode添加到一个全局的ROOT节点中去,然后将该节点添加到map中去(这部分代码在上述代码中省略了)
  • 4.根据EntranceNode创建一个上下文,并将该上下文保存到ThreadLocal中去,下一个请求可以直接获取

那保存在ThreadLocal中的上下文什么时候会清除呢?从代码中可以看到具体的清除工作在ContextUtil的exit方法中,当执行该方法时,会将保存在ThreadLocal中的context对象清除,具体的代码非常简单,这里就不贴代码了。

那ContextUtil.exit方法什么时候会被调用呢?有两种情况:一是主动调用ContextUtil.exit的时候,二是当一个入口Entry要退出,执行该Entry的trueExit方法的时候,此时会触发ContextUtil.exit的方法。但是有一个前提,就是当前Entry的父Entry为null时,此时说明该Entry已经是最顶层的根节点了,可以清除context。

Entry

刚才在Context身影中也看到了Entry的出现,现在就谈谈Entry。每次执行SphU.entry()或SphO.entry()都会返回一个Entry,Entry表示一次资源操作,内部会保存当前invocation信息。在一个Context生命周期中多次资源操作,也就是对应多个Entry,这些Entry形成parent/child结构保存在Entry实例中,entry类CtEntry结构如下:

class CtEntry extends Entry {
    protected Entry parent = null;
    protected Entry child = null;

    protected ProcessorSlot<Object> chain;
    protected Context context;
}
public abstract class Entry implements AutoCloseable {
    private long createTime;
    private Node curNode;
    /**
     * {@link Node} of the specific origin, Usually the origin is the Service Consumer.
     */
    private Node originNode;
    private Throwable error; // 是否出现异常
    protected ResourceWrapper resourceWrapper; // 资源信息
}

Entry实例代码中出现了Node,这个又是什么东东呢



这篇关于Spring Cloud Alibaba Sentinel的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程