Sentinel系列之流量控制及熔断降级示例
关于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
)不生效。
熔断降级规则(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;
}
运行结果: