SpringCloud Alibaba Sentinel实现熔断与限流
安装Sentinel控制台
点击选择版本,进入下载页面,页面最下方含有下载连接。点击下载
改控制台是由SpringBoot编写,内嵌tomcat。下载完成后保证java8环境OK,8080端口不能被占用
点击或者使用java -jar
命令运行即可
启动成功后访问http://localhost:8080
登录账号密码均为sentinel
环境搭建
创建模块cloudalibaba-sentinel-service8401
pom
<dependencies>
<!--sentinel-datasource-nacos持久化-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
#Nacos 服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard
dashboard: localhost:8080
#默认8719,假如被占用了会自动从8719开始依次+1扫描。直至找到未被占用的端口
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
主启动类
创建MainApp8401
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
controller
创建FlowLimitController
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------testA";
}
@GetMapping("/testB")
public String testB() {
return "------testB";
}
}
测试
启动Sentinel8080 Nacos Server 8848 启动微服务8401 启动8401微服务后查看sentienl控制台http://localhost:8080/#/dashboard
空空如也,啥都没有,这是因为Sentinel采用的懒加载说明 我们需要先执行一次访问即可http://localhost:8401/testA
http://localhost:8401/testB
此时便会出现页面效果
sentinel8080正在监控微服务8401!
流控规则
基本介绍
资源名:唯一名称,默认请求路径
针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
阈值类型/单机阈值
- QPS(每秒钟的请求数量):当调用该api的QPS达到阈值的时候,进行限流
- 线程数:当调用该api的线程数达到阈值的时候,进行限流
是否集群:不需要集群
流控模式:
- 直接:api达到限流条件时,直接限流
- 关联:当关联的资源达到阈值时,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
流控效果:
- 快速失败:直接失败,抛异常
- Warm Up:根据codeFactor(冷加载因子,默认3)的值,从阈值coderFactor,经过预热时长,才达到设置的QPS阈值
添加方式。可以在簇点链路添加,也可以在流控规则中添加
流控模式
直接
QPS
为/testA
新增流控规则,流控模式为直接,流控效果为快速失败,阈值类型为QPS 单机阈值为1
添加完成后可以流控规则中查看
此时的阈值是1,也就是每秒钟最多访问一次。进行测试http://localhost:8401/testA
当超过阈值报错Blocked by Sentinel (flow limiting)
,这就是直接快速失败,系统默认。
线程数
修改/testA
流控规则,流控模式为直接,流控效果为快速失败,阈值类型为线程数 单机阈值为1
线程数:当调用该api的线程数达到阈值的时候,进行限流。也就是有超过一个个线程在访问/testA
进行限流,为了看到效果,修改一些controller代码
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
//暂停
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "------testA";
}
@GetMapping("/testB")
public String testB() {
return "------testB";
}
}
重新启动8401,打开两个页面访问http://localhost:8401/testA
,当线程数超过两个的时候触发了快速失败的流控效果,为一个的时候正常访问
直接调用默认报错信息,技术方面OK ,但是是否应该有我们自己的后续处理?
关联
当关联的资源达到阈值时,就限流自己,当与A关联的资源B达到阈值后,就限流自己
配置A流控规则,关联资源/testB
阈值类型为QPS 单机阈值为1,线程停止1秒注释
postman模拟并发密集访问testB,启动postman
访问testB
期间,访问http://localhost:8401/testA
,testA出现限流
当对testB的测试完成后
再次访问http://localhost:8401/testA
,访问正常
链路
多个请求调用了同一个微服务。只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
修改流控规则
当我们多次点击超过QPS阈值时,报错
流控效果
快速失败
直接失败限流,抛出异常 源码com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
Warm Up(预热)
阈值除以coldFactor(默认值为3),经过预热时长后才会达到阈值
默认coldFactor为3,即请求QPS从threshold(阈值)/3开始,经预热时长逐渐升至设定的QPS阈值
当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值。Warm Up(冷启动,预热)模式就是为了实现这个目的的。这个场景主要用于启动需要额外开销的场景,例如建立数据库连接等
com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
配置流控规则
此配置系统初始的阈值为 10/3 约等于3,即阈值刚开始为3,然后在这5秒内阈值慢慢上升至10
测试访问http://localhost:8401/testB
,当我们不停访问的时候
前面几秒它的阈值是3慢慢上升的,我们快速点击访问超过了它的阈值发生限流,随着后面阈值慢慢上升,我们的的一秒内访问的次数已经小于它的阈值了,所以限流次数也就慢慢减少,经过5秒它的阈值就变成了10.此时只有一秒钟访问10次以上才能触发限流。
应用场景如秒杀系统在开启的瞬间,会有很多流量上来,很有可能吧系统打死,预热方式就是为了保护系统,慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
排队等待
匀速排队,阈值必须设置为QPS,也只能设置为QPS
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考 流量控制 - 匀速器模式,具体的例子可以参见 PaceFlowDemo。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
/testA每秒1次请求,超过的化就排队等待,等待的超时时间为20000毫秒
修改一下controller,打印日志
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
//暂停
//try {
// TimeUnit.MILLISECONDS.sleep(1000);
//} catch (InterruptedException e) {
// e.printStackTrace();
//}
return "------testA";
}
@GetMapping("/testB")
public String testB() {
log.info(Thread.currentThread().getName()+"\t"+"....testB");
return "------testB";
}
}
查看控制台打印效果
可以看见以1ms的访问速度,却在控制台是以一秒的速度排队输出打印语句的,说明我们配置的排队等待生效了!
降级规则
基本介绍
RT(平均响应时间。秒级)
- 平均响应时间 超出阈值 且 在时间窗口内通过的请求>=5,两个条件同时满足后触发降级
- 窗口期过后关闭断路器
- RT最大4900(更大的需要通过
-Dcsp.sentinel.statistic.max.rt=xxx
配置生效)
异常比例(秒级)
- QPS >= 5 且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
异常数(分钟级)
- 异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
Sentinel的断路器是没有半开状态的,半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用。具体可以参考Hystrix
RT
平均响应时间 (DEGRADE_GRADE_RT
):当 1s 内持续进入 N 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count
,以 ms 为单位),那么在接下的时间窗口(DegradeRule
中的 timeWindow
,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException
)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx
来配置。
controller中添加代码,重启
@GetMapping("/testD")
public String testD() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("testD 测试RT");
return "------testD";
}
Jmeter测试
此时http://localhost:8401/testD
按照上述配置,永远一秒钟打进来10个线程(大于5个)调用testD,RT为200毫秒,但是testD中又睡眠了1秒,所以200毫秒RT中是处理不完的。在未来的1秒钟的时间窗口内,断路器打开(保险跳闸)微服务不可用,保险丝跳闸断电。后续停止Jmeter,没有每秒10个线程的访问量,不再大于5,断路器关闭(保险丝恢复)微服务恢复
异常比例
异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO
):当资源的每秒请求量 >= N(可配置),并且每秒异常总数占通过量的比值超过阈值(DegradeRule
中的 count
)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule
中的 timeWindow
,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0]
,代表 0% - 100%。
修改代码,重新启动
此时当我们访问的每秒QPS>=5 且异常比例大于配置的0.2(我们百分百出错)时服务降级。
异常数
异常数 (DEGRADE_GRADE_EXCEPTION_COUNT
):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow
小于 60s,则结束熔断状态后仍可能再进入熔断状态。
异常数是按照分钟统计的,时间窗口一定要大于等于60秒
controller添加代码,重新启动访问
@GetMapping("/testE")
public String testE(){
log.info("testE 测试异常数");
int age = 10/0;
return "----testE 测试异常数";
}
访问http://localhost:8401/testE
1分钟内,前面5次访问都能访问testE,而testE百分之百报错,第五次时异常数已经大于5,第六次还是异常报错,异常数6>5,触发降级
热点参数限流
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
兜底方法分为系统默认和客户自定义两种,之前的案例,限流出问题后,都是永sentinel系统默认的提示:Blocked by Sentinel (flow limiting)
我们能不能自定义?类型hystrix,某个方法出问题了,就找对应的兜底降级方法?
结论:从@HystrixCommand到@SentinelResource
基本配置
controller中添加代码,重启
@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){
return "------deal_testHotKey,o(╥﹏╥)o";
}
使用@SentinelResource
,value
属性值可以随便取,保证唯一即可。blockHandler
值为兜底方法名称。
当我们访问http://localhost:8401/testHotKey?p1=1
当我们携带参数p1访问的时候,只有QPS超过了阈值设定的1就出现了限流,并走的是我们自定义的兜底方法。
注意如果@SentinelResource
没有配置blockHandler
,重新启动
此时出现了限流,是直接显示异常,异常打到了前台用户界面看不到,不友好
我们虽然对参数p1进行了限流但是只要不携带参数p1,或者p1参数QPS不超过阈值都可以正常访问。我们访问http://localhost:8401/testHotKey?p2=1
怎么访问都没事,因为并没有对p2参数进行限流配置。
参数例外项
上述案例演示了第一个参数p1,当QPS超过1秒1次点击后马上被限流。但是我们期望p1参数当它是某个特殊值时,它的限流值和平时不一样。假如当p1的值等于5时,它的阈值可以达到200
热点参数的注意点,参数必须是基本类型或者String
对已有热点规则点击编辑
点击高级选项,配置
点击添加
保存,例外项数目相应加1
访问http://localhost:8401/testHotKey?p1=1
,依旧按照配置限流
访问http://localhost:8401/testHotKey?p1=5
特列
多次访问,都没有出现限流,说明配置生效!当p1等于5的时候,阈值变为200
其他测试
当我们在controller中的请求方法添加异常,重新启动
访问http://localhost:8401/testHotKey
并不会触发兜底方法
这是因为@SentinelResource
处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理,@SentinelResource主管配置出错,运行出错该走异常走异常
系统自适应限流
在开始之前,我们先了解一下系统保护的目的:
- 保证系统不被拖垮
- 在系统稳定的前提下,保持系统的吞吐量
长期以来,系统保护的思路是根据硬指标,即系统的负载 (load1) 来做系统过载保护。当系统负载高于某个阈值,就禁止或者减少流量的进入;当 load 开始好转,则恢复流量的进入。这个思路给我们带来了不可避免的两个问题:
- load 是一个“结果”,如果根据 load 的情况来调节流量的通过率,那么就始终有延迟性。也就意味着通过率的任何调整,都会过一段时间才能看到效果。当前通过率是使 load 恶化的一个动作,那么也至少要过 1 秒之后才能观测到;同理,如果当前通过率调整是让 load 好转的一个动作,也需要 1 秒之后才能继续调整,这样就浪费了系统的处理能力。所以我们看到的曲线,总是会有抖动。
- 恢复慢。想象一下这样的一个场景(真实),出现了这样一个问题,下游应用不可靠,导致应用 RT 很高,从而 load 到了一个很高的点。过了一段时间之后下游应用恢复了,应用 RT 也相应减少。这个时候,其实应该大幅度增大流量的通过率;但是由于这个时候 load 仍然很高,通过率的恢复仍然不高。
TCP BBR 的思想给了我们一个很大的启发。我们应该根据系统能够处理的请求,和允许进来的请求,来做平衡,而不是根据一个间接的指标(系统 load)来做限流。最终我们追求的目标是 在系统不被拖垮的情况下,提高系统的吞吐率,而不是 load 一定要到低于某个阈值。如果我们还是按照固有的思维,超过特定的 load 就禁止流量进入,系统 load 恢复就放开流量,这样做的结果是无论我们怎么调参数,调比例,都是按照果来调节因,都无法取得良好的效果。
Sentinel 在系统自适应保护的做法是,用 load1 作为启动自适应保护的因子,而允许通过的流量由处理请求的能力,即请求的响应时间以及当前系统正在处理的请求速率来决定。
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN
),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式:
- 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 达到阈值即触发系统保护。
新增规则
访问没有任何配置的/testA
。http://localhost:8401/testA
当QPS超过系统配置规则1的时候出现了限流!
@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
里面排除掉的异常类型)进行处理。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 逻辑中,而是会原样抛出。
注:1.6.0 之前的版本 fallback 函数只针对降级异常(
DegradeException
)进行处理,不能针对业务异常进行处理。
特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException
时只会进入 blockHandler
处理逻辑。若未配置 blockHandler
、fallback
和 defaultFallback
,则被限流降级时会将 BlockException
直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException
)。
模块cloudalibaba-sentinel-service8401
引入cloud-api-commons依赖,用于测试
<dependency>
<groupId>com.kylin</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
创建RateLimitController
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
}
public CommonResult handleException(BlockException exception) {
return new CommonResult(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
}
}
启动8401
按资源名称限流
访问http://localhost:8401/byResource
1秒钟点击1下,OK。超过上述问题,疯狂点击,返回了自己定义的限流处理信息,限流发送
按照Url地址限流
通过访问的URL来限流,会返回Sentinel自带默认的限流处理信息
controller中添加代码,重新启动
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl() {
return new CommonResult(200, "按url限流测试OK", new Payment(2020L, "serial002"));
}
测试访问http://localhost:8401/rateLimit/byUrl
1秒钟点击1下,OK。超过上述问题,疯狂点击,返回了Sentinel自带的的限流处理信息,限流发送
结论:按资源名称限流和URI地址限流效果配置效果都一样,但是按照URI地址配置限流是不能自定义限流处理信息的,也就是不能配置兜底方法,即使使用了blockHandler
属性配置,也不生效
客户自定义限流处理逻辑
上面兜底方法面临的问题:
- 系统默认的没有体现我们自己的业务要求。
- 依照现有条件,我们自定义的处理方法又和业务代码耦合在一切,不直观。
- 每个业务方法都添加一个兜底的,代码膨胀加剧。
- 全局统一的处理方法没有体现
必须使用@SentinelResource
资源名才能自定义限流处理逻辑
创建CustomerBlockHandler
类用于自定义限流处理逻辑
在这个类中创建两个方法,规定只能是static静态方法,方法参数类型是BlockException
。
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception) {
return new CommonResult(444, "按客户自定义,global,handlerException----1");
}
public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(444, "按客户自定义,global,handlerException----2");
}
}
controller中添加代码
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "CustomerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200, "按客戶自定义", new Payment(2020L, "serial003"));
}
blockHanlderClass是自定义处理的类
此时的blockHanlder值是自定义处理类
中的哪一个方法
启动测试
访问http://localhost:8401/rateLimit/customerBlockHandler
一秒一次访问ok,超过QPS阈值1开始限流,显示我们自定义的限流处理逻辑,成功!
核心API
- SphU定义资源
- Tracer定义统计
- ContextUtil定义了上下文
了解即可
服务熔断功能
Ribbon系列
环境搭建
提供者
新建cloudalibaba-provider-payment9003/9004
pom.xml
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.kylin</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yml,只要端口号不同
server:
port: 端口号
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
主启动类PaymentMain9003``PaymentMain9004
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class, args);
}
}
新建controllerPaymentController
@RestController
public class PaymentController
{
@Value("${server.port}")
private String serverPort;
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static{
hashMap.put(1L,new Payment(1L,"28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2L,new Payment(2L,"bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3L,new Payment(3L,"6ua8c1e3bc2742d8848569891xt92183"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200,"from mysql,serverPort: "+serverPort,payment);
return result;
}
}
启动访问测试http://localhost:9003/paymentSQL/1
和http://localhost:9004/paymentSQL/1
搭建成功!
消费者
新建模块cloudalibaba-consumer-nacos-order84
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.kylin</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
service-url:
nacos-user-service: http://nacos-payment-provider
主启动类OrderNacosMain84
@EnableDiscoveryClient
@SpringBootApplication
public class OrderNacosMain84
{
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
RestTemplate配置类
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
创建controllerCircleBreakerController
package com.kylin.config.controller;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.kylin.entities.CommonResult;
import com.kylin.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@RestController
@Slf4j
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
//@SentinelResource(value = "fallback") //没有配置
//@SentinelResource(value = "fallback",fallback = "handlerFallback") //fallback只负责业务异常
//@SentinelResource(value = "fallback",blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规
//@SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler",
// exceptionsToIgnore = {IllegalArgumentException.class})
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id, CommonResult.class,id);
if (id == 4) {
throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");
}else if (result.getData() == null) {
throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
//fallback
public CommonResult handlerFallback(@PathVariable Long id,Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(444,"兜底异常handlerFallback,exception内容 "+e.getMessage(),payment);
}
//blockHandler
public CommonResult blockHandler(@PathVariable Long id, BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment);
}
}
测试访问http://localhost:84/consumer/fallback/1
访问成功,负载均衡成功!此时访问id为4的时候http://localhost:84/consumer/fallback/4
出现异常
id为5http://localhost:84/consumer/fallback/5
都是报的我们在controller对id进行判断而抛出的异常,环境搭建成功.但是都是给客户error页面,不友好
只配置fallback
此时访问http://localhost:84/consumer/fallback/4
次数访问http://localhost:84/consumer/fallback/5
fallback管运行异常
只配置blockHandler
blockHandler只负责sentinel控制台配置违规
此时访问http://localhost:84/consumer/fallback/5
访问两次出现异常,第三次访问出现异常,异常数大于阈值2触发服务降级,调用我们blockHandler中配置的方法
fallback和blockHandler都配置
重新启动
此时访问http://localhost:84/consumer/fallback/1
QPS小于2时正常访问,QPS大于2时触发服务降级,调用blockHandler中配置的方法
此时访问http://localhost:84/consumer/fallback/5
程序出现异常,调用fallback配置的方法处理。
如果以QPS大于2的情况访问会出现程序异常的http://localhost:84/consumer/fallback/4
,触发服务降级,是调用fallback还是blockHandler呢?
当业务异常sentinel控制台配置违规都发生时只会进入blockHandler处理逻辑
忽略属性
通过exceptionsToIgnore忽略了id等于4抛出的IllegalArgumentException
重新启动此时访问http://localhost:84/consumer/fallback/4
出现报错页面,而不是走fallback方法,因为被忽略了
Feign系列
修改84模块,pom引入openfeign依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
yml配置文件中开启对Feign的支持feign.sentinel.enabled=true
#对Feign的支持
feign:
sentinel:
enabled: true
主启动类加上@EnableFeignClients
启动Feign的功能
@EnableDiscoveryClient
@SpringBootApplication
//启动Feign的功能
@EnableFeignClients
public class OrderNacosMain84
{
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
创建PaymentService
接口
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
创建PaymentFallbackService
实现接口
@Service
public class PaymentFallbackService implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444, "服务降级返回,---PaymentFallbackService", new Payment(id, "errorSerial"));
}
}
controller添加代码
@Resource
PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
重新启动,访问http://localhost:84/consumer/paymentSQL/1
测试84调用9003,9004,此时故意关闭微服务提供者,看84消费侧自动降级,不会被耗死
配置成功。这个测试时针对Feign的独有特性的测试。具体服务熔断功能和上文中的Ribbon测试一致只是换了一下服务调用方式,都是使用@SentinelResource
的fallback
属性,blockHandler
属性,和Sentinel控制台的配置
Sentinel持久化
为了多次测试,我们多次重启了应用,但是一旦我们重启应用,Sentinel规则将消失,生产环境需要将配置规则进行持久化。
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上Sentinel上的流控规则持续有效。
修改cloudalibaba-sentinel-service8401
的pom添加sentinel-datasource-nacos
依赖
<!--sentinel-datasource-nacos持久化-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
修改配置文件添加Nacos数据源配置
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
#Nacos 服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard
dashboard: localhost:8080
#默认8719,假如被占用了会自动从8719开始依次+1扫描。直至找到未被占用的端口
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
添加Nacos业务规则配置,访问`http:
图片中的reta因为rate 配置内容
[
{
"resource": "/rateLimit/byUrl",
"limitApp": "default",
"grade": 1,
"count": 1,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
- resource:资源名称
- limitApp:来源应用
- grade:阈值类型,0表示线程数,1表示QPS
- count:单机阈值
- strategy:流控模式,0表示直接,1表示关联,2表示链路
- controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待
重新启动8401,查看
此时已经自动的把我们配置内容转换成相应的流控规则了,访问测试http://localhost:8401/rateLimit/byUrl
限流规则也生效。每次重启应用后都会自动按照配置在Nacos中的配置文件自动配置相应的流控规则。(感觉没想象中的好用这个功能)
转载自:https://juejin.cn/post/7241877544478670906