什么是Sentinel?
作为SpringCloud Alibaba的组件之一,Sentinel被称为是分布式系统的流量防卫兵。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 分为两个部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
核心库不依赖 Dashboard,但是结合 Dashboard 可以取得最好的效果。
Sentinel 功能和设计理念
流量控制
任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。
举个栗子:去吃自助餐时,人太多了就会取号排队,餐厅可容纳客流量达到峰值,就会让用户在外等候,以此达到对客流量的控制,保证餐厅内可以正常运行。
服务的熔断与降级
除了流量控制以外,由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。
熔断有三种状态,分别为OPEN、HALF_OPEN、CLOSED。
状态 | 说明 |
OPEN | 表示熔断开启,拒绝所有请求 |
HALF_OPEN | 探测恢复状态,如果接下来的一个请求顺利通过则结束熔断,否则继续熔断 |
CLOSED | 表示熔断关闭,请求顺利通过 |
Sentinel的流控使用
Sentinel的使用分为控制台【Dashboard】与 核心库【java客户端】 核心库不依赖 Dashboard,但是结合Dashboard 可以取得最好的效果。
可以从 release 页面 下载最新版本的控制台 jar 包。
启动sentinel-dashboard 我这里就在本地启动了。启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。
java -Dserver.port=8088 -jar sentinel-dashboard-1.8.1.jar
默认用户名和密码都是 sentinel
此时的控制台是什么都没有的
控制台启动后,客户端需要按照以下步骤接入到控制台。
pom中引入相关依赖,主要Springboot的版本【本文基于2.2.5.RELEASE】
<!-- com.alibaba.cloud/spring-cloud-starter-alibaba-sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
application.properties
server.port=8081
spring.cloud.sentinel.transport.dashboard=localhost:8088
spring.cloud.sentinel.transport.port=8719
这里的 spring.cloud.sentinel.transport.dashboard 配置控制台的IP和端口
再编写一个控制类
@RestController
public class MemberController {
//限流规则名称
private static final String GETMEMBER_KEY = "getMember";
@RequestMapping("/getMember/{num}")
@SentinelResource(value = GETMEMBER_KEY,blockHandler = "getMemberBlockHandler")
public String memberService(@PathVariable("num") Integer num){
return "会员服务-套餐"+num;
}
public String getMemberBlockHandler(Integer num,BlockException e){
return "当前请求人数过多,请稍后重试"+e.toString();
}
}
启动项目,测试接口
此时再返回sentinel控制台,刷新
@SentinelResource 注解 注解方式埋点不支持 private 方法。
@SentinelResource
用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource
- value:资源名称,必需项(不能为空)
- entryType:entry 类型,可选项(默认为 EntryType.OUT)
- blockHandler / blockHandlerClass
- fallback /fallbackClass
blockHandler 对应处理 BlockException的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。
blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。
fallback 函数签名和位置要求:
- 返回值类型必须与原函数返回值类型一致;
- 方法参数列表需要和原函数一致,或者可以额外多一个
Throwable
类型的参数用于接收对应的异常。 - fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
接下来通过控制台来测试一下流控规则, 注意这里的资源名要和@SentinelResource注解的value值一致
- QPS:每秒请求数,当前调用该api的QPS到达阈值的时候进行限流
- 线程数:当调用该api的线程数到达阈值的时候,进行限流
新增流控规则后,快速访问上文中的接口
如图所示,新增流控规则时还有高级选项,来看看
流控模式: 【默认是直接】
- 直接:当api大达到限流条件时,直接限流
- 关联:当关联的资源到达阈值,就限流自己
- 链路:只记录指定路上的流量,指定资源从入口资源进来的流量,如果达到阈值,就进行限流,api级别的限流
接下来测试一下关联模式【当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。】
修改控制类代码 【此时要做的就是在大批量线程高并发访问/test_ref,看/test是否失效】
@SentinelResource(value = "test1", blockHandler = "exceptionHandler")
@GetMapping("/test1")
public String test1()
{
System.out.println(Thread.currentThread().getName() + "\t" + "...test1");
return "-------hello baby,i am test1";
}
// Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
public String exceptionHandler(BlockException ex)
{
ex.printStackTrace();
System.out.println(Thread.currentThread().getName() + "\t" + "...exceptionHandler");
return String.format("error: test1 is not OK");
}
@SentinelResource(value = "test1_ref")
@GetMapping("/test1_ref")
public String test1_ref()
{
System.out.println(Thread.currentThread().getName() + "\t" + "...test1_related");
return "-------i am test1_ref";
}
通过JMeter来模拟高并发测试
启动测试时,访问/test1
链路类型的关联也类似,就不再演示了。
【流控效果】
Warm Up(预热):当流量突然增大的时候,如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值。
默认 coldFactor 为 3,即请求 QPS 从 阈值/ 3 开始,经预热时长逐渐升至设定的 QPS 阈值。
【比如我们这里就是 qps达到 10/3 也就是3的时候,开始预热,预热5秒后将阈值提升至10】
修改控制层代码
@SentinelResource(value = "testWarmUp",blockHandler = "exceptionHandlerOfWarmUp")
@GetMapping("/testWarmUp")
public String testWarmUp(){
return "------testWarmUp";
}
public String exceptionHandlerOfWarmUp(BlockException e){
return "预热预热预热";
}
根据上文提到的预热规则,刚开始刷/testWarmUP,会出现默认错误,预热时间到了后,阈值增加,没超过阈值刷新,请求正常。
如秒杀系统在开启瞬间,会有很多流量上来,很可能把系统打死,预热方式就是为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
排队等待:匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。阈值必须设置为QPS。
Sentinel会以固定的间隔时间让请求通过, 访问资源。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过;反之,则马上抛出阻塞异常。
修改控制层
@SentinelResource(value = "testLineUp",blockHandler = "exceptionHandlerOfLineUp")
@GetMapping("/testLineUp")
public String testLineUp(){
return "------testLineUp";
}
public String exceptionHandlerOfLineUp(BlockException e){
return "请排队";
}
Sentinel的熔断降级使用
文章开头已经解释了什么是熔断降级,接下来在控制台中新增降级规则,具体看一下这些设置项的意思。
资源名:仍然是对应注解@SentinelResource中value的值
熔断策略:
慢调用比例
属性 | 说明 |
最大RT | 需要设置的阈值,超过该值则为慢应用【单位是毫秒】 |
比例阈值 | 慢调用占所有的调用的比率,范围:[0~1] |
熔断时长 | 在这段时间内发生熔断、拒绝所有请求 |
最小请求数 | 即允许通过的最小请求数,在该数量内不发生熔断 |
执行逻辑
熔断(OPEN):请求数大于最小请求数并且慢调用的比率大于比例阈值则发生熔断,熔断时长为用户自定义设置。
探测(HALFOPEN):
- 当熔断过了定义的熔断时长,状态由熔断(OPEN)变为探测(HALFOPEN)。
- 如果接下来的一个请求小于最大RT,说明慢调用已经恢复,结束熔断,状态由探测(HALF_OPEN)变更为关闭(CLOSED)
- 如果接下来的一个请求大于最大RT,说明慢调用未恢复,继续熔断,熔断时长保持一致
修改控制类代码进行测试,这里为了达到RT条件,我们让线程休眠一段时间。
@RequestMapping("/getMemberThread")
@SentinelResource(value = "getMemberThread",blockHandler = "getMemberThreadBlockHandler")
public String getMemberThread(){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "会员服务-线程套餐";
}
public String getMemberThreadBlockHandler(BlockException e){
return "当前接口已被熔断";
}
访问接口,按F5快速刷新
异常比例
通过计算异常比例与设置阈值对比的一种策略。
属性 | 说明 |
异常比例阈值 | 异常比例=发生异常的请求数÷请求总数取值范围:[0~1] |
熔断时长 | 在这段时间内发生熔断、拒绝所有请求 |
最小请求数 | 即允许通过的最小请求数,在该数量内不发生熔断 |
修改控制类
@RequestMapping("/getMemberException")
@SentinelResource(value = "getMemberException",blockHandler = "getMemberExceptionBlockHandler")
public String getMemberException(){
int a = 1/0;
return "会员服务-线程套餐";
}
public String getMemberExceptionBlockHandler(BlockException e){
return "异常比例处理";
}
异常数道理是一样的,就不赘述了。
总结一下几种熔断策略
Sentinel 提供以下几种熔断策略:
- 慢调用比例 (SLOWREQUESTRATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs,默认为 1s)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
- 异常比例 (ERROR_RATIO):当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是
[0.0, 1.0]
,代表 0% - 100%。 - 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
热点规则
热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的数据,并对其访问进行限制。
统计参数中的热点参数,并根据配置的限流阀值与模式,对包含热点参数的资源调用进行限流。
比如这里我们对第一个参数设置热点规则
发现只支持QPS模式
- 参数索引:是 我们controller hot 接口的参数下标
- 单机阈值:在这里是2,所以一秒钟允许通过2个请求
- 窗口时长:是在访问该资源QPS超过阈值,触发该规则的有效期,超过后重新统计
修改Controller
@GetMapping("/testHostKey")
@SentinelResource(value = "testHostKey",blockHandler = "exceptionHostKey")
public String testHostKey(@RequestParam(name = "name",required = false) String name,
@RequestParam(name = "goodId",required = false) Integer goodId){
return "name:"+name+",goodId:"+goodId;
}
public String exceptionHostKey(String name,Integer goodId,BlockException e){
return "热点规则测试:"+e;
}
此时会发现只对第一个参数限流
系统规则
系统保护的目标是 在系统不被拖垮的情况下,提高系统的吞吐率,而不是 load 一定要到低于某个阈值。
系统规则支持以下的模式:
- 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 达到阈值即触发系统保护。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN
)
Sentinel配置持久化
大家在做的时候也会发现,修改控制层代码重启项目后,Sentinel-Dashboard中的规则配置也就不存在了,那么如何对Sentinel的配置进行持久化操作?
官网给出的方案有很多
这里我们使用Nacos来做Sentinel配置持久化
pom中添加
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.0</version>
</dependency>
properties中新增配置
#服务名
spring.application.name=sentinel-nacos
#nacos服务地址
spring.cloud.nacos.discovery.server-addr=192.168.56.1:8848
#Sentinel持久化Nacos配置
spring.cloud.sentinel.datasource.ds1.nacos.server-addr=${spring.cloud.nacos.discovery.server-addr}
spring.cloud.sentinel.datasource.ds1.nacos.data-id=nacos-sentinel
spring.cloud.sentinel.datasource.ds1.nacos.group-id=DEFAULT_GROUP
spring.cloud.sentinel.datasource.ds1.nacos.data-type=json
spring.cloud.sentinel.datasource.ds1.nacos.rule-type=flow
启动项目,访问localhost:8081/getMember/1,请求后再去Sentinel控制台。
Sentinel中添加流控配置
从控制台获取请求参数
在Nacos中新增配置
此时我们重启项目,Sentinel配置仍然存在。