一、前言
接下来是开展一系列的 SpringCloud 的学习之旅,从传统的模块之间调用,一步步的升级为 SpringCloud 模块之间的调用,此篇文章为第十四篇,即介绍 SpringCloud Alibaba Sentinel 创建流控规则。
二、基本介绍
我们在 sentinel 的管理界面上为我们的请求设置流量监控的规则,有两种方式,如下图:
点击 + 号,配置流控规则,界面如下图,接下来详细介绍下可以配置的标签
2.1 资源名
唯一名称,默认请求路径。即工程里面 controller 层方法上面的请求路径。
2.2 针对来源
Sentinel 可以针对调用者进行限流,填写微服务名,默认 default (不区分来源)。
2.3 阈值类型
QPS(每秒钟的请求数量):当调用该 api 的 QPS 达到阀值的时候,进行限流。
线程数:当调用该 api 的线程数达到阀值的时候,进行限流。
2.4 单机阈值
一个具体的数字,为阀值类型所使用。
2.5 是否集群
不需要集群。
2.6 流控模式
1、直接:api 达到限流条件时,直接限流。
2、关联:当关联的资源达到闻值时,就限流自己
3、链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到闻值,就进行限流)[api 级别的针对来源]。
2.7 流控效果
1、快速失败: 直接失败,抛异常
2、Warm Up:根据 codeFactor ( 冷加载因子,默认 3 )的值,从阀值 / codeFactor,经过预热时长,才达到设置的 QPS 阀值。
3、排队等待:匀速排队,让请求以匀速的速度通过,闻值类型必须设置为 QPS,否则无效。
三、流控模式
3.1 直接
接下来我们配置一个系统默认的流控规则,即 QPS -直接-快速失败,如下图,表示 1s 内查询一次是没有问题的,但是在 1s 内点击超过一次就直接快速失败,报默认错误。
快速点击访问 http://localhost:8401/testB,如下图,可以看到,第一次请求是没有任何问题的,
但是当我们狂点的时候,就会返回给我们限流的提醒了,如下图:
修改我们刚刚创建的流控规则,将 QPS -直接-快速失败 模式修改为 并发线程数 -直接-快速失败 ,如下图:
快速点击访问 http://localhost:8401/testB,如下图,可以看到,一直没有报错信息
这是因为 QPS 和并发线程数是两种不同的流控效果,即 QPS 有流控效果,而并发线程数没有流控效果,从上面配置时的两张图片对比就可以看的出来。
当有一堆请求发送过来时(高并发场景),QPS 流控就会将请求全部拦截在门外,根本接触不到里面的路径,而并发流控则是将请求放进来,但是只能有一个请求得到处理,其他的请求直接失败报错。
上面一直调用成功的原因是后台处理请求在一瞬间就完成了,为了演示流控的效果,我们修改下 FlowLimitController 的代码,延长下 testB() 方法的处理时间,代码如下:
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA()
{
return "------testA";
}
@GetMapping("/testB")
public String testB()
{
try {
Thread.sleep(800);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "------testB";
}
}
打开两个浏览器窗口,分别调用 http://localhost:8401/testB,如下图,第一次调用没有任何问题,如下图:
第二次调用就返回异常信息了(点击的速度得快,要不演示不出来),如下:
3.2 关联
当关联的资源达到阈值时,就限流自己,当与 A 关联的资源 B 达到阀值后,就限流 A 自己,举个简单的例子就是支付模块快爆炸了,就限流下订单模块。
在 sentinel 的管理界面配置一个关联的限流规则,如下,当关联资源 /testB 的 qps 阀值超过 1 时,就限流 /testA 的 Rest 访问地址,当关联资源到阈值后限制配置好的资源名。
测试之前先把 FlowLimitController 的代码改回来,如下:
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA()
{
return "------testA";
}
@GetMapping("/testB")
public String testB()
{
return "------testB";
}
}
为了演示模拟的效果,我们需要使用 postman 进行并发密集访问 /testB,打开 postman,创建一个新的 collection,如下图:
接下来创建一个新的请求,记得保存这个请求,如下图:
接下来配置运行的参数,如下图:
配置一个线程组,包含 20 个线程,每隔 0.3s 访问一次
然后调用 http://localhost:8401/testA ,可以发现 testA 挂了,如下图:
3.3 链路
假设一个 controller 里面的两个服务都会调用同一个 service 层里面的服务,这样就会形成两条链路,但是由于 service 层的资源是有限的,我们可以通过配置链路的流控模式来保证某一条链路一直可以正常使用,但另一条链路可能就会多担待一点。
它和我们的关联模式有点像,也是保证一个接口可以正常使用,而牺牲了另外一个接口。接下来我们使用代码来演示下。
首先需要创建一个 service 层,并编写一个方法,这个方法是给 controller 层的两个方法提供服务的,并在方法上添加 @SentinelResource 注解,这个注解用于标记需要被流量控制、熔断降级以及系统保护等功能覆盖的方法或类。这个注解允许开发者自定义资源名称,并且可以配置异常处理逻辑和 fallback 函数。代码如下:
package com.springcloud.service;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.stereotype.Service;
@Service
public class FlowLimitService {
@SentinelResource("todoService")
public void todoService(){
System.out.println("....todoService....");
}
}
接下来需要在我们的配置文件里面添加一些配置,配置文件里面默认采用的是聚合的方式,我们这里改成 false,采用展开的模式,即我们有多少个链路都把它展示出来。
接下来修改 controller 类的代码,让其调用 service 层的方法,如下:
@RestController
public class FlowLimitController {
@Resource
FlowLimitService flowLimitService;
@GetMapping("/testA")
public String testA()
{
flowLimitService.todoService();
return "------testA";
}
@GetMapping("/testB")
public String testB()
{
flowLimitService.todoService();
return "------testB";
}
}
接下来打开 sentinel 的管理界面,首先调用 testA 和 testB 服务,如下图:
如果我们没有在配置文件里面配置 web-context-unify: false 这个标签,或者配置完了这个标签出现下面的这种情况。
原因可能是因为 spring-cloud-starter-alibaba-sentinel 版本太低,将其改成下面的版本即可。
<!-- 流量控制sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
接下来配置链路的流控,我们对 testA 的链路进行限流,如下图:
此时疯狂调用 http://localhost:8401/testB,如下图,没有任何反应,可以正常返回
疯狂调用 http://localhost:8401/testA,如果频率高一些就会报错,如下图:
证明我们的链路调用限流成功了。
四、流控效果
4.1 快速失败
这种流控效果上面我们做了大量的例子,就是直接失败,抛出异常,如下图:
4.2 Warm Up 预热
当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值。Warm Up(冷启动,预热)模式就是为了实现这个目的的。
这个场景主要用于启动需要额外开销的场景,例如建立数据库连接等。
它有一个公式:初始阈值 = (配置的阈值)除以( codeFactor 默认值为 3),
经过预热时长后才会达到配置的阈值。这样讲很抽象,我们配置一个来展示下,首先把 controller 代码恢复成原来的样子,如下图:
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA()
{
return "------testA";
}
@GetMapping("/testB")
public String testB()
{
return "------testB";
}
}
配置预热的流控效果,如下图,我希望每秒钟可以承受 10 个 QPS,但是我给你慢慢的预热起来,由于冷加载默认因子是 3,10/3 =3 ,那么一开始单机的阈值就是 3,但是给了你一个缓冲预热的时间 5s,让你 5s 内,慢慢的从 3 过度到 10。
此时疯狂调用 http://localhost:8401/testB,可以发现一开始有成功有失败,但到后来就都是成功的了。
其使用场景是:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值
4.3 排队等待
匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
注意:匀速排队模式暂时不支持 QPS > 1000 的场景。
修改下 controller 类的代码,加一个日志打印的功能,如下:
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping("/testA")
public String testA()
{
return "------testA";
}
@GetMapping("/testB")
public String testB()
{
log.info(Thread.currentThread().getName()+"\t"+"testB");
return "------testB";
}
}
配置排队等待的流控效果,如下图,设置单机阈值为 1,超时时间为 2s。
接下来使用 postman 进行多线程调用测试,和 5.2.2 小节用的方式一样,调用 collenction,如下图:
查看控制台的日志信息,如下,可以看到,都是 1s 内调用一次,很舒服
五、降级规则
5.1 简介
降级规则又称为熔断降级,除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
本文档针对 Sentinel 1.8.0 及以上版本。1.8.0 版本对熔断降级特性进行了全新的改进升级,请使用最新版本以更好地利用熔断降级的能力。
Sentinel 和 Hystrix 的原则是一致的:当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。
5.2 熔断策略
熔点策略分为三种:慢调用比例、异常比例和异常数三种。
5.3 慢调用比例
慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
在 FlowLimitController 类中新增一个测试方法 testD,代码如下:
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------testA";
}
@GetMapping("/testB")
public String testB() {
log.info(Thread.currentThread().getName() + "\t" + "testB");
return "------testB";
}
@GetMapping("/testD")
public String testD() {
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("testD 测试RT");
return "------testD";
}
}
在 sentinel 中配置降级规则,如下图:
设定允许的最大响应时间 RT = 900ms,统计最近 10000ms 内的请求,若请求超过 5 次,并且慢调用的比例超过 0.5,则触发熔断,熔断时长为 2s,然后进入 half-open 状态,放行一次请求做测试。
接下来我们使用 postman 进行测试,使用 10 个线程调用我们的方法,如下图:
等 postman 调用结束后,测试当前 testD 的状态,如下图,可以发现处于熔断状态了。
等到 2s 之后,再次刷新浏览器,可以看到,又恢复正常了,如下图:
5.4 异常比例
异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
修改 FlowLimitController 类中的测试方法 testD,代码如下:
@GetMapping("/testD")
public String testD() {
log.info("testD 测试异常调用");
int age = 10 / 0;
return "------testD";
}
在 sentinel 中配置降级规则,如下图:
统计最近 10000ms 内的请求,若请求超过 5 次,并且异常的比例超过 0.5,则触发熔断,熔断时长为 3s,然后进入 half-open 状态,放行一次请求做测试。
我们先在浏览器访问一下方法,如下,可以看到返回的是错误的界面。
接下里我们使用 postman 进行测试,使用 10 个线程调用我们的方法,如下图:
调用结束后再次在浏览器访问 testD,可以看到,返回的是限流的界面,如下图:
等到 3s 之后,再次访问,可以看到,又出现了错误的界面,如下:
5.5 异常数
异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
在 FlowLimitController 类中的新增测试方法 testE,代码如下:
@GetMapping("/testE")
public String testE() {
log.info("testE 测试异常数");
int age = 10 / 0;
return "------testE";
}
在 sentinel 中配置降级规则,如下图:
统计最近 10000ms 内的请求,若请求超过 5 次,并且异常数超过 5 个,则触发熔断,熔断时长为 3s,然后进入 half-open 状态,放行一次请求做测试。
在浏览器多次访问 testE,可以发现,前五次返回的页面如下:
等到第六次,返回的就是流控的界面了,如下:
六、热点参数限流
6.1 简介
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
1、商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制。
2、用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
6.2 案例演示
在 FlowLimitController 类中的新增两个方法,代码如下,@SentinelResource 注解我们在 上面提到过,用于标记需要被流量控制、熔断降级以及系统保护等功能覆盖的方法或类。
它里面有个 blockHandler 属性,用于指定兜底的方法,兜底方法分为系统默认和客户自定义两种,在之前的案例中,限流出问题后,都是用 sentinel 系统默认的提示:Blocked by Sentinel (flow limiting)。这次我们指定兜底的方法为下面的 deal_testHotKey
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2) {
return "------testHotKey";
}
public String deal_testHotKey (String p1, String p2, BlockException exception){
// sentinel系统默认的提示:Blocked by Sentinel (flow limiting)
return "------deal_testHotKey,o(╥﹏╥)o";
}
接下里我们在 sentinel 中配置热点规则,如下:
限流模式只支持 QPS 模式,固定写死了。@SentinelResource 注解的方法参数索引,0 代表第一个参数,1 代表第二个参数,以此类推。单机阀值以及统计窗口时长表示在此窗口时间超过阀值就限流。
上面配置的规则为:1s 的 QPS 为 1,超过就限流,限流后调用 dealHandler testHotKey 支持方法。
在浏览器访问:http://localhost:8401/testHotKey,怎么点击都不会出现限流的效果,因为并没有传输参数。
在浏览器访问:http://localhost:8401/testHotKey?p1=a,如果 1s 点击一下就会出现下面的现象:
如果点击频繁,就会出现限流的效果,如下:
在浏览器访问:http://localhost:8401/testHotKey?p2=b,随便点,也不会出现限流的效果,因为我们没有对第二个参数进行配置,所以不用限流,如下:
6.3 特殊情况
上述案例演示了第一个参数 p1,当 QPS 超过 1s 点击一次后马上被限流,但是我们期望当 p1 参数是某个特殊值时,它的限流值和平时不一样,比如说当 p1 的值等于 5 时,它的阈值可以达到 200,那么我们该如何配置呢?
修改我们刚才配置的 sentinel 热点规则,打开参数例外项,需要注意的是:参数类型必须是基本类型或者 String,如下图:
在浏览器访问:http://localhost:8401/testHotKey?p1=5,可以看到,无论点击多么频繁都不会出现限流的现象,如下图:
在浏览器访问:http://localhost:8401/testHotKey?p1=4,可以看到,超过 1s 点击一次就会出现限流的现象,如下图,当 p1 不等于 5 的时候,阈值就是平常的 1。
6.4 异常情况
修改 FlowLimitController 类,手动添加一个异常试试,如下:
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2) {
int age = 10/0;
return "------testHotKey";
}
public String deal_testHotKey (String p1, String p2, BlockException exception){
// sentinel系统默认的提示:Blocked by Sentinel (flow limiting)
return "------deal_testHotKey,o(╥﹏╥)o";
}
在浏览器访问:http://localhost:8401/testHotKey,可以看到,直接就报错了,如下图:
这是因为 @SentinelResource 处理的是 Sentinel 控制台配置的违规情况,有 blockHandler 方法配置的兜底处理。而 int age = 10/0,这个是 java 运行时报出的运行时异常 RunTimeException,@SentinelResource 不管。
总结起来就是 @SentinelResource 主管配置出错,运行出错该走异常走异常。
七、系统规则
7.1 简介
系统规则又称为系统自适应限流,是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。前面我们所配置的限流都是方法级别的限流,而系统规则则是对整个应用进行限流的操作。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量,比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
7.2 分类
1、Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
2、CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
3、平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
4、并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
5、入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
7.3 案例演示
在 sentinel 控制台新增一个入口 QPS 的系统规则,如下图:
在浏览器访问 http://localhost:8401/testB,当 1s 点击一下时,效果如下图,没有任何问题
当频繁点击时就会出现限流的效果,如下图: