sentinel (史上最全+入门教程)
文章很长,而且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :
免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《尼恩Java面试宝典 最新版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取
sentinel 基本概念
开发的原因,需要对吞吐量(TPS)、QPS、并发数、响应时间(RT)几个概念做下了解,查自百度百科,记录如下:
- 响应时间(RT)
响应时间是指系统对请求作出响应的时间。直观上看,这个指标与人对软件性能的主观感受是非常一致的,因为它完整地记录了整个计算机系统处理请求的时间。由于一个系统通常会提供许多功能,而不同功能的处理逻辑也千差万别,因而不同功能的响应时间也不尽相同,甚至同一功能在不同输入数据的情况下响应时间也不相同。所以,在讨论一个系统的响应时间时,人们通常是指该系统所有功能的平均时间或者所有功能的最大响应时间。当然,往往也需要对每个或每组功能讨论其平均响应时间和最大响应时间。
对于单机的没有并发操作的应用系统而言,人们普遍认为响应时间是一个合理且准确的性能指标。需要指出的是,响应时间的绝对值并不能直接反映软件的性能的高低,软件性能的高低实际上取决于用户对该响应时间的接受程度。对于一个游戏软件来说,响应时间小于100毫秒应该是不错的,响应时间在1秒左右可能属于勉强可以接受,如果响应时间达到3秒就完全难以接受了。而对于编译系统来说,完整编译一个较大规模软件的源代码可能需要几十分钟甚至更长时间,但这些响应时间对于用户来说都是可以接受的。- 吞吐量(Throughput)
吞吐量是指系统在单位时间内处理请求的数量。对于无并发的应用系统而言,吞吐量与响应时间成严格的反比关系,实际上此时吞吐量就是响应时间的倒数。前面已经说过,对于单用户的系统,响应时间(或者系统响应时间和应用延迟时间)可以很好地度量系统的性能,但对于并发系统,通常需要用吞吐量作为性能指标。
对于一个多用户的系统,如果只有一个用户使用时系统的平均响应时间是t,当有你n个用户使用时,每个用户看到的响应时间通常并不是n×t,而往往比n×t小很多(当然,在某些特殊情况下也可能比n×t大,甚至大很多)。这是因为处理每个请求需要用到很多资源,由于每个请求的处理过程中有许多不走难以并发执行,这导致在具体的一个时间点,所占资源往往并不多。也就是说在处理单个请求时,在每个时间点都可能有许多资源被闲置,当处理多个请求时,如果资源配置合理,每个用户看到的平均响应时间并不随用户数的增加而线性增加。实际上,不同系统的平均响应时间随用户数增加而增长的速度也不大相同,这也是采用吞吐量来度量并发系统的性能的主要原因。一般而言,吞吐量是一个比较通用的指标,两个具有不同用户数和用户使用模式的系统,如果其最大吞吐量基本一致,则可以判断两个系统的处理能力基本一致。- 并发用户数
并发用户数是指系统可以同时承载的正常使用系统功能的用户的数量。与吞吐量相比,并发用户数是一个更直观但也更笼统的性能指标。实际上,并发用户数是一个非常不准确的指标,因为用户不同的使用模式会导致不同用户在单位时间发出不同数量的请求。一网站系统为例,假设用户只有注册后才能使用,但注册用户并不是每时每刻都在使用该网站,因此具体一个时刻只有部分注册用户同时在线,在线用户就在浏览网站时会花很多时间阅读网站上的信息,因而具体一个时刻只有部分在线用户同时向系统发出请求。这样,对于网站系统我们会有三个关于用户数的统计数字:注册用户数、在线用户数和同时发请求用户数。由于注册用户可能长时间不登陆网站,使用注册用户数作为性能指标会造成很大的误差。而在线用户数和同事发请求用户数都可以作为性能指标。相比而言,以在线用户作为性能指标更直观些,而以同时发请求用户数作为性能指标更准确些。- QPS每秒查询率(Query Per Second)
每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。对应fetches/sec,即每秒的响应请求数,也即是最大吞吐能力。 (看来是类似于TPS,只是应用于特定场景的吞吐量)
1、什么是Sentinel:
Sentinel是阿里开源的项目,提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性。
官网:https://github.com/alibaba/Sentinel/wiki
2012年,Sentinel诞生于阿里巴巴,其主要目标是流量控制。2013-2017年,Sentinel迅速发展,并成为阿里巴巴所有微服务的基本组成部分。 它已在6000多个应用程序中使用,涵盖了几乎所有核心电子商务场景。2018年,Sentinel演变为一个开源项目。2020年,Sentinel Golang发布。
Sentinel 具有以下特征:
丰富的应用场景 :Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即
突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控 :Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机
器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
广泛的开源生态 :Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring
Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。
完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快
速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel的生态圈
Sentinel主要特性:
关于Sentinel与Hystrix的区别见:https://yq.aliyun.com/articles/633786/
到这已经学习Sentinel的基本的使用,在很多的特性和Hystrix有很多类似的功能。以下是Sentinel和Hystrix的对比。
Sentinel 的使用
Sentinel 的使用可以分为两个部分:
-
控制台(Dashboard):控制台主要负责管理推送规则、监控、集群限流分配管理、机器发现等。
-
核心库(Java 客户端):不依赖任何框架/库,能够运行于 Java 7 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
在这里我们看下控制台的使用
Sentinel中的管理控制台
2.1 获取 Sentinel 控制台
您可以从 release 页面 下载最新版本的控制台 jar 包。
您也可以从最新版本的源码自行构建 Sentinel 控制台:
- 下载 控制台 工程
- 使用以下命令将代码打包成一个 fat jar:
mvn clean package
2.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 Boot 或 Spring 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 。
从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel 。可以参考 鉴权模块文档 配置用户名和密码。
[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)
启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。
查看机器列表以及健康情况
默认情况下Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包。也可以配置
sentinel.eager=true ,取消Sentinel控制台懒加载。
打开浏览器即可展示Sentinel的管理控制台
客户端能接入控制台
控制台启动后,客户端需要按照以下步骤接入到控制台。
父工程引入 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>
( 2)配置启动参数
在工程的application.yml中添加Sentinel 控制台配置信息
spring:
cloud:
sentinel:
transport:
dashboard: 192.168.180.137:8888 #sentinel控制台的请求地址
这里的 spring.cloud.sentinel.transport.dashboard 配置控制台的请求路径。
Sentinel与Hystrix的区别
迁移方案
Sentinel 官方提供了详细的由Hystrix 迁移到Sentinel 的方法
2 使用 Sentinel 来进行熔断与限流
Sentinel 可以简单的分为 Sentinel 核心库和 Dashboard。核心库不依赖 Dashboard,但是结合
Dashboard 可以取得最好的效果。
使用 Sentinel 来进行熔断保护,主要分为几个步骤:
-
定义资源
资源:可以是任何东西,一个服务,服务里的方法,甚至是一段代码。
-
定义规则
规则:Sentinel 支持以下几种规则:流量控制规则、熔断降级规则、系统保护规则、来源访问控制规则
和 热点参数规则。 -
检验规则是否生效
Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效. 先把可能需要保护的资源定义好,之后再配置规则。
也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保
护,就将之定义为一个资源。
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. 定义规则
规则主要有流控规则、 熔断降级规则、系统规则、权限规则、热点参数规则等:
一段硬编码的方式定义流量控制规则如下:
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); // 修改授权规则
3 sentinel 熔断降级
1 什么是熔断降级
熔断降级对调用链路中不稳定的资源进行熔断降级是保障高可用的重要措施之一。
由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)
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 几种降级策略
我们通常用以下几种降级策略:
-
平均响应时间 (DEGRADE_GRADE_RT):
当资源的平均响应时间超过阈值(DegradeRule 中的 count,以 ms 为单位)之后,资源进入准降级状态。如果接下来 1s 内持续进入 5 个请求(即 QPS >= 5),它们的 RT 都持续超过这个阈值,那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。
注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -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,则结束熔断状态后仍可能再进入熔断状态。
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);
}
具体源码,请参见疯狂创客圈crazy-springcloud 源码工程
5 控制台降级规则
配置
参数
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,即限流规则的作用对象 | |
count | 阈值 | |
grade | 降级模式,根据 RT 降级还是根据异常比例降级 | RT |
timeWindow | 降级的时间,单位为 s |
6 与Hystrix的熔断对比:
Hystrix常用的线程池隔离会造成线程上下切换的overhead比较大;Hystrix使用的信号量隔离对某个资源调用的并发数进行控制,效果不错,但是无法对慢调用进行自动降级;
Sentinel通过并发线程数的流量控制提供信号量隔离的功能;此外,Sentinel支持的熔断降级维度更多,可对多种指标进行流控、熔断,且提供了实时监控和控制面板,功能更为强大。
4 Sentinel 流控(限流)
流量控制(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。
配置
![流控规则](
参数
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,资源名是限流规则的作用对象 | |
count | 限流阈值 | |
grade | 限流阈值类型,QPS 或线程数模式 | QPS 模式 |
limitApp | 流控针对的调用来源 | default ,代表不区分调用来源 |
strategy | 判断的根据是资源自身,还是根据其它关联资源 (refResource ),还是根据链路入口 |
根据资源本身 |
controlBehavior | 流控效果(直接拒绝 / 排队等待 / 慢启动模式) | 直接拒绝 |
测试
频繁刷新请求,1秒访问2次请求,正常,超过设置的阈值,将报默认的错误。
再次的1秒访问2次请求,访问正常。超过2次,访问异常
4.2 关联模式
调用关系包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关系。Sentinel 通过 NodeSelectorSlot
建立不同资源间的调用的关系,并且通过 ClusterBuilderSlot
记录每个资源的实时统计信息。
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。
比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢.
举例来说,read_db
和 write_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);
网页限流规则配置
测试
选择QPS,单机阈值为1,选择关联,关联资源为/test_ref,这里用Jmeter模拟高并发,请求/test_ref。
在大批量线程高并发访问/test_ref,导致/test失效了
链路类型的关联也类似,就不再演示了。多个请求调用同一微服务。
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);
网页限流规则配置
先在单机阈值10/3,3的时候,预热10秒后,慢慢将阈值升至20。刚开始刷/testWarmUP,会出现默认错误,预热时间到了后,阈值增加,没超过阈值刷新,请求正常。
通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
如秒杀系统在开启瞬间,会有很多流量上来,很可能把系统打死,预热方式就是为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
通过jmeter进行测试
4.4 排队等待模式
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。阈值必须设置为QPS。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
某瞬时来了大流量的请求, 而如果此时要处理所有请求,很可能会导致系统负载过高,影响稳定性。但其实可能后面几秒之内都没有消息投递,若直接把多余的消息丢掉则没有充分利用系统处理消息的能力。Sentinel的Rate Limiter模式能在某一段时间间隔内以匀速方式处理这样的请求, 充分利用系统的处理能力, 也就是削峰填谷, 保证资源的稳定性.
Sentinel会以固定的间隔时间让请求通过, 访问资源。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过;反之,则马上抛出阻塞异常。
使用Sentinel的这种策略, 简单点说, 就是使用一个时间段(比如20s的时间)处理某一瞬时产生的大量请求, 起到一个削峰填谷的作用, 从而充分利用系统的处理能力, 下图能很形象的展示这种场景: X轴代表时间, Y轴代表系统处理的请求.
示例
模拟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);
网页限流规则配置
通过jmeter进行测试
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));
网页限流规则配置
5. 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);
网页限流规则配置
6 黑白名单规则
很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的访问控制(黑白名单)的功能。黑白名单根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。
调用方信息通过 ContextUtil.enter(resourceName, origin) 方法中的 origin 参数传入。
访问控制规则 (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));
7、如何定义资源
Sentinel 可以简单的分为 Sentinel 核心库和 Dashboard。
核心库不依赖 Dashboard,可以通过代码手段定义资源。
我们说的资源,可以是任何东西,服务,服务里的方法,甚至是一段代码。
使用 Sentinel 来进行资源保护,主要分为几个步骤:
- 定义资源
- 定义规则
- 检验规则是否生效
先把可能需要保护的资源定义好(埋点),之后再配置规则。
也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。
对于主流的框架,我们提供适配,只需要按照适配中的说明配置,Sentinel 就会默认定义提供的服务,方法等为资源。
方式一:主流框架的默认适配
为了减少开发的复杂程度,我们对大部分的主流框架,例如 Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor 等都做了适配。您只需要引入对应的依赖即可方便地整合 Sentinel。可以参见: 主流框架的适配。
方式二:抛出异常的方式定义资源
SphU
包含了 try-catch 风格的 API。用这种方式,当资源发生了限流之后会抛出 BlockException
。这个时候可以捕捉异常,进行限流之后的逻辑处理。示例代码如下:
// 1.5.0 版本开始可以利用 try-with-resources 特性(使用有限制)
// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
try (Entry entry = SphU.entry("resourceName")) {
// 被保护的业务逻辑
// do something here...
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
// 在此处进行相应的处理操作
}
特别地,若 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
方式三:返回布尔值方式定义资源
SphO
提供 if-else 风格的 API。用这种方式,当资源发生了限流之后会返回 false
,这个时候可以根据返回值,进行限流之后的逻辑处理。示例代码如下:
// 资源名可使用任意有业务语义的字符串
if (SphO.entry("自定义资源名")) {
// 务必保证finally会被执行
try {
/**
* 被保护的业务逻辑
*/
} finally {
SphO.exit();
}
} else {
// 资源访问阻止,被限流或被降级
// 进行相应的处理操作
}
注意:SphO.entry(xxx)
需要与 SphO.exit()方法成对出现,匹配调用,位置正确,否则会导致调用链记录异常,抛出
ErrorEntryFreeException` 异常。
方式四:注解方式定义资源
Sentinel 支持通过 @SentinelResource
注解定义资源并配置 blockHandler
和 fallback
函数来进行限流之后的处理。示例:
// 原本的业务方法.
@SentinelResource(blockHandler = "blockHandlerForGetUser")
public User getUserById(String id) {
throw new RuntimeException("getUserById command failed");
}
// blockHandler 函数,原方法调用被限流/降级/系统保护的时候调用
public User blockHandlerForGetUser(String id, BlockException ex) {
return new User("admin");
}
注意 blockHandler
函数会在原方法被限流/降级/系统保护的时候调用,而 fallback
函数会针对所有类型的异常。请注意 blockHandler
和 fallback
函数的形式要求,更多指引可以参见 Sentinel 注解支持文档。
方式五:异步调用支持
Sentinel 支持异步调用链路的统计。在异步调用中,需要通过 SphU.asyncEntry(xxx)
方法定义资源,并通常需要在异步的回调函数中调用 exit
方法。以下是一个简单的示例:
try {
AsyncEntry entry = SphU.asyncEntry(resourceName);
// 异步调用.
doAsync(userId, result -> {
try {
// 在此处处理异步调用的结果.
} finally {
// 在回调结束后 exit.
entry.exit();
}
});
} catch (BlockException ex) {
// Request blocked.
// Handle the exception (e.g. retry or fallback).
}
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,里面包含了普通资源与异步资源之间的各种嵌套示例。
8 核心组件
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,这个又是什么东东呢 😦,咱们接着往下看:
DefaultNode
Node(关于StatisticNode的讨论放到下一小节)默认实现类DefaultNode,该类还有一个子类EntranceNode;context有一个entranceNode属性,Entry中有一个curNode属性。
- EntranceNode:该类的创建是在初始化Context时完成的(ContextUtil.trueEnter方法),注意该类是针对Context维度的,也就是一个context有且仅有一个EntranceNode。
- DefaultNode:该类的创建是在NodeSelectorSlot.entry完成的,当不存在context.name对应的DefaultNode时会新建(new DefaultNode(resourceWrapper, null),对应resouce)并保存到本地缓存(NodeSelectorSlot中private volatile Map<String, DefaultNode> map);获取到context.name对应的DefaultNode后会将该DefaultNode设置到当前context的curEntry.curNode属性,也就是说,在NodeSelectorSlot中是一个context有且仅有一个DefaultNode。
看到这里,你是不是有疑问?为什么一个context有且仅有一个DefaultNode,我们的resouece跑哪去了呢,其实,这里的一个context有且仅有一个DefaultNode是在NodeSelectorSlot范围内,NodeSelectorSlot是ProcessorSlotChain中的一环,获取ProcessorSlotChain是根据Resource维度来的。总结为一句话就是:针对同一个Resource,多个context对应多个DefaultNode;针对不同Resource,(不管是否是同一个context)对应多个不同DefaultNode。这还没看明白 : (,好吧,我不bb了,上图吧:
public class DefaultNode extends StatisticNode {
private ResourceWrapper id;
/**
* The list of all child nodes.
* 子节点集合
/
private volatile Set
/
* Associated cluster node.
*/
private ClusterNode clusterNode;
}
一个Resouce只有一个clusterNode,多个defaultNode对应一个clusterNode,如果defaultNode.clusterNode为null,则在ClusterBuilderSlot.entry中会进行初始化。
同一个Resource,对应同一个ProcessorSlotChain,这块处理逻辑在lookProcessChain方法中,如下:
ProcessorSlot