官方文档地址
https://github.com/alibaba/Sentinel/wiki/%E6%96%B0%E6%89%8B%E6%8C%87%E5%8D%97
引入 Sentinel 依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.0</version>
</dependency>
定义规则
private static final String USER_KEY = "user";
/**
* 初始化限流
*/
private static void initFlowRule() {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule = new FlowRule();
rule.setResource(USER_KEY); //资源名称, 自定义
// 降级模式 QPS或者并发数
// QPS 每秒可以同时查询的数量
// 并发线程数,是指同一时刻最大的并发数
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(1); // QPS 为5
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
限流测试
public static void main(String[] args) {
// 初始化限流
initFlowRule();
while (true){
// 1.5.0 版本开始可以直接利用 try-with-resources 特性,自动 exit entry
try (Entry entry = SphU.entry(USER_KEY)){
// 被保护的逻辑
System.out.println("hello world");
} catch (BlockException e) {
// 处理被流控的逻辑
System.out.println("Handle rejected request.");
}
}
}
Spring Cloud Alibaba Sentinel
引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
服务级限流简单使用
@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 逻辑中,而是会原样抛出。
@Service
public class TestService {
/**
* value : 表示资源名称,自定义
* @param name
* @return
*/
@SentinelResource(value = "sayHello")
public String sayHello(String name) {
return "Hello, " + name;
}
/**
* 初始化限流
*/
@PostConstruct
private static void initFlowRule() {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule = new FlowRule();
rule.setResource("sayHello"); //资源名称, 自定义
// 降级模式 QPS或者并发数
// QPS 每秒可以同时查询的数量
// 并发线程数,是指同一时刻最大的并发数
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(1); // QPS 为5
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
}
@RestController
public class TestController {
@Autowired
private TestService service;
@GetMapping(value = "/hello/{name}")
public String apiHello(@PathVariable String name) {
return service.sayHello(name);
}
}
测试
刷新快一点就会触发熔断
触发降级函数
/**
* value : 表示资源名称,自定义
* @param name
* @return
*/
@SentinelResource(value = "sayHello",blockHandler = "blockHandler")
public String sayHello(String name) {
return "Hello, " + name;
}
/**
* blockHandler / blockHandlerClass:
* blockHandler 对应处理 BlockException 的函数名称,可选项。
* blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,
* 参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。
* blockHandler 函数默认需要和原方法在同一个类中。
* 若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,
* 注意对应的函数必需为 static 函数,否则无法解析。
* @param name
* @param e
* @return
*/
public String blockHandler(String name, BlockException e){
return "服务被限流了";
}
测试
触发熔断
/**
* value : 表示资源名称,自定义
* @param name
* @return
*/
@SentinelResource(value = "sayHello",blockHandler = "blockHandler",fallback = "fallback")
public String sayHello(String name) {
return "Hello, " + name;
}
/**
* blockHandler / blockHandlerClass:
* blockHandler 对应处理 BlockException 的函数名称,可选项。
* blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,
* 参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。
* blockHandler 函数默认需要和原方法在同一个类中。
* 若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,
* 注意对应的函数必需为 static 函数,否则无法解析。
* @param name
* @param e
* @return
*/
public String blockHandler(String name, BlockException e){
return "服务被限流了";
}
/**
* fallback / fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。
* fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。
* fallback 函数签名和位置要求:
* 1. 返回值类型必须与原函数返回值类型一致;
* 2. 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
* 3. fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,
* 注意对应的函数必需为 static 函数,否则无法解析。
* @param name
* @return
*/
public String fallback(String name){
return "服务被降级了";
}
注意: 如果 blockHandler 和 fallback 同时配置,只会触发 blockHandler
sentinel SPI 扩展
- 上面我们使用的是 @PostConstruct 加载规则,下面我们是用 SPI 初始化规则
- 实现 InitFunc 接口
public class FlowRuleInitFunc implements InitFunc {
@Override
public void init() throws Exception {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule = new FlowRule();
rule.setResource("sentinelSpi"); //资源名称, 自定义
// 降级模式 QPS或者并发数
// QPS 每秒可以同时查询的数量
// 并发线程数,是指同一时刻最大的并发数
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(3); // QPS 为5
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
}
-
在 resource 目录下创建目录 META-INF,在 META-INF 下载创建 services 目录,在 services 目录下创建问文件,文件名为 InitFunc 接口全路径,文件内容为实现 InitFunc 接口的 FlowRuleInitFunc 类的全路径,这样 Springboot 启动的时候就会去自动加载
image.png - service 中加一个方法
@SentinelResource(value = "sentinelSpi")
public String sentinelSpi(){
return "sentitnel spi ......";
}
- controller 中加一个方法
@GetMapping(value = "/initSpi")
public String sentinel(){
return service.sentinelSpi();
}
-
测试
image.png
QPS (TPS)
基于QPS/并发数的流量控制
- 我们可以通过下面的命令查看实时统计信息:
curl http://localhost:8719/cnode?id=resourceName
- 输出内容格式如下:
idx id thread pass blocked success total Rt 1m-pass 1m-block 1m-all exception
2 abc647 0 46 0 46 46 1 2763 0 2763 0
- 其中
thread: 代表当前处理该资源的并发数;
pass: 代表一秒内到来到的请求;
blocked: 代表一秒内被流量控制的请求数量;
success: 代表一秒内成功处理完的请求;
total: 代表到一秒内到来的请求以及被阻止的请求总和;
RT: 代表一秒内该资源的平均响应时间;
1m-pass: 则是一分钟内到来的请求;
1m-block: 则是一分钟内被阻止的请求;
1m-all: 则是一分钟内到来的请求和被阻止的请求的总和;
exception: 则是一秒内业务本身异常的总和。
并发线程数控制
并发数控制用于保护业务线程池不被慢调用耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对太多线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置。
QPS流量控制
当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的效果包括以下几种:直接拒绝、Warm Up、匀速排队。对应 FlowRule 中的 controlBehavior 字段。
直接拒绝
直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时
Warm Up
Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮
- warmupFlowRule() 通过设置下列的参数,构建了一个流控规则:
FlowRule rule = new FlowRule();
rule.setResource(resourceName);
rule.setCount(20);
rule.setGrade(RuleConstant.GRADE_QPS);
rule.setLimitApp("default");
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
rule.setWarmUpPeriodSec(10);
- 其中,CONTROL_BEHAVIOR_WARM_UP 表示启用冷启动模式,warmUpPeriodSec 代表期待系统进入稳定状态的时间(即预热时长)。coldSystem() 则是通过调节 qpsInColdPeriod 的大小,让系统长时间的处于冷的状态。simulateTraffic() 则通过调节 threadCount 的大小,来模拟大流量的情况
- 观察输出
Large amount of traffic is coming
88 send qps is: 2061
1528883307808,total:2061, pass:9, block:2053
87 send qps is: 3699
1528883308808,total:3699, pass:7, block:3692
86 send qps is: 3898
1528883309808,total:3898, pass:7, block:3893
85 send qps is: 3713
1528883310808,total:3713, pass:7, block:3708
84 send qps is: 3756
1528883311808,total:3756, pass:8, block:3749
83 send qps is: 3750
1528883312808,total:3750, pass:9, block:3741
82 send qps is: 3492
1528883313806,total:3492, pass:10, block:3482
81 send qps is: 3923
1528883314808,total:3923, pass:11, block:3913
80 send qps is: 3176
1528883315820,total:3176, pass:13, block:3163
79 send qps is: 3729
1528883316821,total:3729, pass:22, block:3708
78 send qps is: 3534
1528883317820,total:3534, pass:20, block:3514
可以看到第 10 秒的时候,系统开始稳定的接受 20 个请求
匀速排队
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法
- paceFlowRule() 这个函数里面,通过设置下面几个参数让匀速器生效:
rule.setGrade(RuleConstant.GRADE_QPS);
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER); // 流控效果:匀速排队模式
rule.setCount(10);
rule.setMaxQueueingTimeMs(20 * 1000); // 最长排队等待时间:20s
注意,当匀速器生效的时候,规则的限流类型一定是 RuleConstant.GRADE_QPS,否则该规则将不生效。当 count 设为 10 的时候,则代表一秒匀速的通过 10 个请求,也就是每个请求平均间隔恒定为 1000 / 10 = 100 ms,每一个请求的最长等待时间(maxQueueingTimeMs)为 20 * 1000ms = 20s。
- simulatePulseFlow() 这个函数,模拟了几乎同时到来的 100 个请求。
- 我们来看一下运行结果
下面的输出以逗号分隔,第一栏位通过的时间,第二栏为等待的时间
....
1528872403887 pass, cost 9348
1528872403986 pass, cost 9469
1528872404087 pass, cost 9570
1528872404187 pass, cost 9642
1528872404287 pass, cost 9770
1528872404387 pass, cost 9848
1528872404487 pass, cost 9970
done
pass:100 block:0
我们可以看到,这 100 个请求,都以匀速 100 ms 的速度依次通过,并且没有阻塞
- 和默认的行为对比
我们通过 defaultFlowRule() 里,将该规则的流控效果设为 CONTROL_BEHAVIOR_DEFAULT(默认效果,快速失败),再发起同样的流量。这个时候检查结果,将会发现同样的流量,默认行为仅会通过 10 个
基于调用关系的流量控制
调用关系包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关系。Sentinel 通过 NodeSelectorSlot 建立不同资源间的调用的关系,并且通过 ClusterBuilderSlot 记录每个资源的实时统计信息。
有了调用链路的统计信息,我们可以衍生出多种流量控制手段。
根据调用方限流
- ContextUtil.enter(resourceName, origin) 方法中的 origin 参数标明了调用方身份。这些信息会在 ClusterBuilderSlot 中被统计。可通过以下命令来展示不同的调用方对同一个资源的调用数据:
curl http://localhost:8719/origin?id=nodeA
- 调用数据示例:
id: nodeA
idx origin threadNum passedQps blockedQps totalQps aRt 1m-passed 1m-blocked 1m-total
1 caller1 0 0 0 0 0 0 0 0
2 caller2 0 0 0 0 0 0 0 0
- 上面这个命令展示了资源名为 nodeA 的资源被两个不同的调用方调用的统计。
流控规则中的 limitApp 字段用于根据调用来源进行流量控制。该字段的值有以下三种选项,分别对应不同的场景:
- default:表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。
- {some_origin_name}:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者caller1的规则,那么当且仅当来自 caller1 对 NodeA 的请求才会触发流量控制。
- other:表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源NodeA配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1 对 NodeA 的调用,都不能超过 other 这条规则定义的阈值。
同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default
根据调用链路入口限流:链路限流
NodeSelectorSlot 中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root 的虚拟节点,调用链的入口都是这个虚节点的子节点。
一棵典型的调用树如下图所示:
machine-root
/ \
/ \
Entrance1 Entrance2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)
- 上图中来自入口 Entrance1 和 Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 strategy 为 RuleConstant.STRATEGY_CHAIN,同时设置 refResource 为 Entrance1 来表示只有从入口 Entrance1 的调用才会记录到 NodeA 的限流统计当中,而不关心经 Entrance2 到来的调用。
- 调用链的入口(上下文)是通过 API 方法 ContextUtil.enter(contextName) 定义的,其中 contextName 即对应调用链路入口名称
具有关系的资源流量控制:关联流量控制
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 strategy 为 RuleConstant.STRATEGY_RELATE 同时设置 refResource 为 write_db。这样当写库操作过于频繁时,读数据的请求会被限流。
Sentinel 控制台
jar 包下载,最下面找到 sentinel-dashboard-1.8.0.jar
https://github.com/alibaba/Sentinel/releases/tag/v1.8.0
上传至服务器,运行
-Dserver.port=8080 用于指定 Sentinel 控制台端口为 8080
sentinel.log 2>&1 & 用于执行 log 文件
nohup java \
-Dserver.port=8080 \
-Dcsp.sentinel.dashboard.server=localhost:8080 \
-Dproject.name=sentinel-dashboard \
-jar \
sentinel-dashboard-1.8.0.jar >> sentinel.log 2>&1 &
- 启动完成,浏览器输入 http://ip:prot,默认用户名 sentinel 密码 sentinel
image.png
SpringCloud 集成 Sentinel 控制台
application.yml
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:8080
这里的 spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了一个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中
-
狂刷几次请求, Sentinel 控制面板每十秒会更新一次
image.png
动态规则,从远程服务器加载规则
非 springCloud 工程集成
- 添加依赖
<!-- 使用 Nacos 配置规则 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.0</version>
</dependency>
- 配置规则,创建 NacosDataSource 并将其注册至对应的 RuleManager 上即可
public class FlowRuleInitFunc implements InitFunc {
private final String remoteAddress = "192.168.245.131:8848";
private final String groupId = "SENTINEL_GROUP";
private final String dataId = "springboot-sentinel";
@Override
public void init() throws Exception {
/*List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule = new FlowRule();
rule.setResource("sentinelSpi"); //资源名称, 自定义
// 降级模式 QPS或者并发数
// QPS 每秒可以同时查询的数量
// 并发线程数,是指同一时刻最大的并发数
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(3); // QPS 为5
rules.add(rule);
FlowRuleManager.loadRules(rules);*/
// remoteAddress 代表 Nacos 服务端的地址
// groupId 和 dataId 对应 Nacos 中相应配置
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource <List<FlowRule>>(remoteAddress, groupId, dataId,
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
}
}
-
添加 nacos 对应配置
image.png -
测试
image.png
springCloud 工程,不需要上面的配置,springCloud 已经集成好了
- 添加对应配置
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: 192.168.245.131:8080
eager: true # 配置 true 时,取消 Sentinel 控制台懒加载功能
datasource:
ds1:
nacos:
serverAddr: 192.168.245.131:8848
dataId: springboot-sentinel
groupId: SENTINEL-GROUP
dataType: json
ruleType: flow
username: nacos
password: nacos