Sentinel实现熔断与限流
- 一、Sentinel
- 二、安装Sentinel控制台
- 三、初始化演示工程
- 四、流控规则
- 五、降级规则
- 六、热点key限流
- 七、系统规则
- 八、@SentinelResource
- 九、服务熔断
- 十、规则持久化
一、Sentinel
面向云原生微服务的高可用流控防护组件
和以前学过的Hystrix 对比
Hystrix | Sentinel |
---|---|
1 需要我们程序员自己手工搭建监控平台 2 没有一套web界面可以给我们更加细粒度化的配置 流控、速率控制、服务熔断。服务降级… |
1 单独一个组件,可以独立出来。 2 直接界面化的细粒度统一配置。 |
我们现在的编程方式:约定》配置》编码
Sentinel的一些流控、速率配置都可以写在代码里,但是我们本次还是大规模的学历使用配置和注解的方式,尽量少写代码。
Sentinel 是什么?
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
-
丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
-
完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
-
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
-
完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 的主要特性:
Sentinel 的开源生态:
防止服务使用中的各种问题:
- 服务雪崩
- 服务降级
- 服务熔断
- 服务限流
二、安装Sentinel控制台
Sentinel 分为两个部分:
-
核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
-
控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
Sentinel安装步骤:
①下载
下载网址
下载sentinel-dashboard-1.8.0.jar包到本地 E:\softs 目录下
②运行命令
前期条件
- java8+ 环境
- 8080端口不能被占用
命令 在E:\softs 目录下
java -jar sentinel-dashboard-1.8.0.jar
③访问sentinel管理界面
登录页面 默认用户账号密码: sentinel
http://localhost:8080
控制台:
三、初始化演示工程
1、启动Naocs8848
2、新增Module
新增Module: cloudalibaba-sentinel-service-8401微服务作为Sentinel测试
pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.zzp.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloudalibaba-sentinel-service-8401</artifactId>
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 引入自定义的API通用包 实体类 -->
<dependency>
<groupId>com.zzp.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--web actuator-->
<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>
</project>
application.yml配置文件:
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service # 微服务名称
cloud:
nacos:
discovery:
# 配置Nacos 注册中心
server-addr: 127.0.0.1:8848
sentinel:
transport:
# 配置sentinel dashboard地址
dashboard: 127.0.0.1:8080
# 默认8719端口,假如被占用会自动从8719开始依次+1扫描,直到找到未占用的端口
port: 8719
# 暴露监控端点 ‘*’ 全部打开
management:
endpoints:
web:
exposure:
include: '*'
启动类:
@SpringBootApplication
@EnableDiscoveryClient
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
控制类:
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "---------testA";
}
@GetMapping("/testB")
public String testB() {
return "---------testB";
}
}
3、启动Sentinel8080
java -jar sentinel-dashboard-1.8.0.jar
4、启动微服务8401
5、启动微服务8401后查看Sentinel控制台
这里什么都没有,需要先请求接口一下
http://localhost:8401/testA
http://localhost:8401/testB
再刷新Sentinel控制台,可以看到cloudalibaba-sentinel-service
多次点击 testA/testB
流量监控
四、流控规则
1、基本介绍
名词解释
-
资源名: 唯一名称,默认是请求路径
-
针对来源: Sentinel可以针对调用者进行限流,填写微服务的名称,默认是default(不区分来源)
-
阈值类型/单机阈值:可随时更改,无需重启系统:
- QPS:每秒的请求数量,当调用该api的QPS达到阈值的时候,进行限流。
- 线程数: 当调用该api的线程数达到阈值的时候,进行限流。
-
是否集群模式:不需要集群
-
流控模式:
- 直接:api达到限流条件时,直接限流
- 关联:当关联的资源达到限流条件时,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
-
流控效果:
- 快速失败:直接失败,抛出异常
- Warm Up: 根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值
- 排队等待:匀速排队,让请求以匀速通过,阈值类型必须设置为QPS,否则无效
2、流控模式
①直接(默认)
QPS :每秒的请求数量,当调用该api的QPS达到阈值的时候,进行限流
对 /testA 接口添加默认流控设置:
对/testA设置每秒只能请求一次 超过请求次数,请求失败
请求 :http://localhost:8401/testA
表示1秒钟内查询1次就OK,若超过次数1,就直接-快速失败,报默认错误
Blocked by Sentinel (flow limiting)
线程数: 当调用该api的线程数达到阈值的时候,进行限流
为了演示线程数效果,修改testA接口,休眠0.8秒
@GetMapping("/testA")
public String testA() {
try
{
Thread.sleep(800);
} catch (InterruptedException e)
{
e.printStackTrace();
}
return "---------testA";
}
然后,重启8401微服务
在sentinel修改流控规则
测试
②关联
关联:当关联的资源达到限流条件时,就限流自己
比如说:当与A关联的资源B达到阀值后,就限流A自己
就是 B惹事了,A挂了
把先前的/testA 接口回复原样(去掉休眠),然后重启
配置效果
当关联资源/testB的QBS阀值超过1 时,就限流/testA的Rest访问地址,当关联资源到阀值后限制配置好的资源名
使用postman模拟并发密集访问 /testB
第一步:先把地址保存自己创建的文件夹中
第二步:点击文件夹,选择 run
第三步:点击文件夹,选择 run ,并设置
点击Run conllection
同时访问:http://localhost:8401/testA
然后过一会再访问,请求ok
③链路
链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
先查看簇点链路,/testB的资源入口是sentinel_web_servlet_context
添加流控规则/testB
配置链路入口资源为sentinel_web_servlet_context
频繁访问B接口,超出访问限制时(使用上步骤postman并发请求)
同时在浏览器请求:http://localhost:8401/testB
3、流控效果
①快速失败
直接拒绝:直接失败,抛出异常
直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
上面有演示,这里就不操作了
直接报出: Blocked by Sentinel (flow limiting)
源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.alibaba.csp.sentinel.slots.block.flow.controller;
import com.alibaba.csp.sentinel.node.Node;
import com.alibaba.csp.sentinel.node.OccupyTimeoutProperty;
import com.alibaba.csp.sentinel.slots.block.flow.PriorityWaitException;
import com.alibaba.csp.sentinel.slots.block.flow.TrafficShapingController;
import com.alibaba.csp.sentinel.util.TimeUtil;
public class DefaultController implements TrafficShapingController {
private static final int DEFAULT_AVG_USED_TOKENS = 0;
private double count;
private int grade;
public DefaultController(double count, int grade) {
this.count = count;
this.grade = grade;
}
public boolean canPass(Node node, int acquireCount) {
return this.canPass(node, acquireCount, false);
}
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
int curCount = this.avgUsedTokens(node);
if ((double)(curCount + acquireCount) > this.count) {
if (prioritized && this.grade == 1) {
long currentTime = TimeUtil.currentTimeMillis();
long waitInMs = node.tryOccupyNext(currentTime, acquireCount, this.count);
if (waitInMs < (long)OccupyTimeoutProperty.getOccupyTimeout()) {
node.addWaitingRequest(currentTime + waitInMs, acquireCount);
node.addOccupiedPass(acquireCount);
this.sleep(waitInMs);
throw new PriorityWaitException(waitInMs);
}
}
return false;
} else {
return true;
}
}
private int avgUsedTokens(Node node) {
if (node == null) {
return 0;
} else {
return this.grade == 0 ? node.curThreadNum() : (int)node.passQps();
}
}
private void sleep(long timeMillis) {
try {
Thread.sleep(timeMillis);
} catch (InterruptedException var4) {
}
}
}
②Warm Up(预热)
Warm Up: 根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。
公式:阀值除以coldFactor(默认值为3),经过预热时长后才会达到阀值
Warm Up
(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
默认coldFactor为3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阀值
比如:
/testA 我们希望最终效果是每秒钟承受10个QPS,但是这里是慢慢预热起来,一开始 冷加载因子是3,那么一运行起来是 10/3=3,所有一开始你的单机阀值是3,但是这里设置预热时长是5秒。5秒钟后,单机阀值从3过度到10
源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.alibaba.csp.sentinel.slots.block.flow.controller;
import com.alibaba.csp.sentinel.node.Node;
import com.alibaba.csp.sentinel.slots.block.flow.TrafficShapingController;
import com.alibaba.csp.sentinel.util.TimeUtil;
import java.util.concurrent.atomic.AtomicLong;
public class WarmUpController implements TrafficShapingController {
protected double count;
private int coldFactor;
protected int warningToken = 0;
private int maxToken;
protected double slope;
protected AtomicLong storedTokens = new AtomicLong(0L);
protected AtomicLong lastFilledTime = new AtomicLong(0L);
public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {
this.construct(count, warmUpPeriodInSec, coldFactor);
}
public WarmUpController(double count, int warmUpPeriodInSec) {
this.construct(count, warmUpPeriodInSec, 3);
}
private void construct(double count, int warmUpPeriodInSec, int coldFactor) {
if (coldFactor <= 1) {
throw new IllegalArgumentException("Cold factor should be larger than 1");
} else {
this.count = count;
this.coldFactor = coldFactor;
this.warningToken = (int)((double)warmUpPeriodInSec * count) / (coldFactor - 1);
this.maxToken = this.warningToken + (int)((double)(2 * warmUpPeriodInSec) * count / (1.0D + (double)coldFactor));
this.slope = ((double)coldFactor - 1.0D) / count / (double)(this.maxToken - this.warningToken);
}
}
public boolean canPass(Node node, int acquireCount) {
return this.canPass(node, acquireCount, false);
}
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long passQps = (long)node.passQps();
long previousQps = (long)node.previousPassQps();
this.syncToken(previousQps);
long restToken = this.storedTokens.get();
if (restToken >= (long)this.warningToken) {
long aboveToken = restToken - (long)this.warningToken;
double warningQps = Math.nextUp(1.0D / ((double)aboveToken * this.slope + 1.0D / this.count));
if ((double)(passQps + (long)acquireCount) <= warningQps) {
return true;
}
} else if ((double)(passQps + (long)acquireCount) <= this.count) {
return true;
}
return false;
}
protected void syncToken(long passQps) {
long currentTime = TimeUtil.currentTimeMillis();
currentTime -= currentTime % 1000L;
long oldLastFillTime = this.lastFilledTime.get();
if (currentTime > oldLastFillTime) {
long oldValue = this.storedTokens.get();
long newValue = this.coolDownTokens(currentTime, passQps);
if (this.storedTokens.compareAndSet(oldValue, newValue)) {
long currentValue = this.storedTokens.addAndGet(0L - passQps);
if (currentValue < 0L) {
this.storedTokens.set(0L);
}
this.lastFilledTime.set(currentTime);
}
}
}
private long coolDownTokens(long currentTime, long passQps) {
long oldValue = this.storedTokens.get();
long newValue = oldValue;
if (oldValue < (long)this.warningToken) {
newValue = (long)((double)oldValue + (double)(currentTime - this.lastFilledTime.get()) * this.count / 1000.0D);
} else if (oldValue > (long)this.warningToken && passQps < (long)((int)this.count / this.coldFactor)) {
newValue = (long)((double)oldValue + (double)(currentTime - this.lastFilledTime.get()) * this.count / 1000.0D);
}
return Math.min(newValue, (long)this.maxToken);
}
}
其中一段代码:
默认 coldFactor 为3,即请求 QPS 从 (threshold / 3) 开始,经多少预热时长逐渐升至设定的 QPS 阀值。
案例;阀值为10+预热时长设置5秒
系统初始化从阀值为 10/3 约等于阀值刚开始为3;然后过了5秒后阀值才慢慢升恢复到10
Warm Up配置:
请求测试:http://localhost:8401/testB
刚开始不行,后续慢慢ok
应用场景
如:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值。
③ 排队等待
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
注意:匀速排队模式暂时不支持 QPS > 1000 的场景。
源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.alibaba.csp.sentinel.slots.block.flow.controller;
import com.alibaba.csp.sentinel.node.Node;
import com.alibaba.csp.sentinel.slots.block.flow.TrafficShapingController;
import com.alibaba.csp.sentinel.util.TimeUtil;
import java.util.concurrent.atomic.AtomicLong;
public class RateLimiterController implements TrafficShapingController {
private final int maxQueueingTimeMs;
private final double count;
private final AtomicLong latestPassedTime = new AtomicLong(-1L);
public RateLimiterController(int timeOut, double count) {
this.maxQueueingTimeMs = timeOut;
this.count = count;
}
public boolean canPass(Node node, int acquireCount) {
return this.canPass(node, acquireCount, false);
}
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
if (acquireCount <= 0) {
return true;
} else if (this.count <= 0.0D) {
return false;
} else {
long currentTime = TimeUtil.currentTimeMillis();
long costTime = Math.round(1.0D * (double)acquireCount / this.count * 1000.0D);
long expectedTime = costTime + this.latestPassedTime.get();
if (expectedTime <= currentTime) {
this.latestPassedTime.set(currentTime);
return true;
} else {
long waitTime = costTime + this.latestPassedTime.get() - TimeUtil.currentTimeMillis();
if (waitTime > (long)this.maxQueueingTimeMs) {
return false;
} else {
long oldTime = this.latestPassedTime.addAndGet(costTime);
try {
waitTime = oldTime - TimeUtil.currentTimeMillis();
if (waitTime > (long)this.maxQueueingTimeMs) {
this.latestPassedTime.addAndGet(-costTime);
return false;
} else {
if (waitTime > 0L) {
Thread.sleep(waitTime);
}
return true;
}
} catch (InterruptedException var15) {
return false;
}
}
}
}
}
}
匀速排队模式的阀值类型必须设成QPS ,否则无效。
设置含义:/testB每秒1次请求,超过的话就排队等待,等待的超时时间为20000毫秒。
代码 /testA 添加日记输出
@GetMapping("/testA")
public String testA() {
log.info(Thread.currentThread().getName() + " \t " + " ....testA");
return "---------testA";
}
重启,测试:
使用postman并发请求
点击执行
查看后台日记:
五、降级规则
1、介绍
概述
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
注意:本文档针对 Sentinel 1.8.0 及以上版本。1.8.0 版本对熔断降级特性进行了全新的改进升级,请使用最新版本以更好地利用熔断降级的能力
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)不生效。为了统计异常比例或异常数,需要通过 Tracer.trace(ex) 记录业务异常。
进一步说明
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升到),对这个资源的调用进行限制,让请求快速失败,避免影响到其他的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口之内,对改资源的调用都自动熔断(默认行为是抛出 DegradeException
)。
Sentinel的断路器是没有半开 状态的
半开的状态系统自动去检查是否请求异常,
没有异常就关闭断路器恢复使用,
有异常则继续打开断路器不可用,具体可以参考Hystrix
2、降级策略配置
①慢调用比例
慢调用比例 (SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN
状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
在微服务8401添加接口:
@GetMapping("/testC")
public String testC() {
try
{
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e)
{
e.printStackTrace();
}
log.info("....testC 慢调用比例");
return "---------testC 慢调用比例";
}
重启,访问 http://localhost:8401/testC ok
界面配置:
当请求/testC响应时长超过0.2秒,进入熔断,熔断时长3秒后恢复,最小请求数为5(默认值每秒最小值为5),比例阈值为1
使用postma压测
点击执行,
然后马*问:
停止postman请求后。等会再访问:http://localhost:8401/testC ok
②异常比例
异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
代码添加异常接口/testD:
@GetMapping("/testD")
public String testD() {
log.info("....testD 异常比例");
int a = 9/0;
return "---------testD 异常比例";
}
重启,先访问一次:http://localhost:8401/testD ok
Sentinel界面配置
异常比例达到20%(80%是正确的),熔断,3秒后恢复
最少每秒请求数为5(默认最少为5)
继续使用postman压测
点击执行,然后马上在浏览器访问:http://localhost:8401/testD
停止postman,访问:
这里报的错误是自己运行的异常,并不是异常比例的异常,因为这里不满足 异常比例配置的熔断条件
③异常数
异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
代码添加接口/testE:
@GetMapping("/testE")
public String testE() {
log.info("....testE 异常数");
int a = 9/0;
return "---------testE 异常数";
}
重启,先访问一次:http://localhost:8401/testE ok
Sentinel界面配置
当每分钟超过5个异常,熔断,过1分钟后,再3秒后恢复
最小请求数为5(默认最少为5)
访问:http://localhost:8401/testE
六、热点key限流
1、基本介绍
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
-
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
-
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式
源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.alibaba.csp.sentinel.slots.block;
public abstract class BlockException extends Exception {
public static final String BLOCK_EXCEPTION_FLAG = "SentinelBlockException";
public static RuntimeException THROW_OUT_EXCEPTION = new RuntimeException("SentinelBlockException");
public static StackTraceElement[] sentinelStackTrace = new StackTraceElement[]{new StackTraceElement(BlockException.class.getName(), "block", "BlockException", 0)};
protected AbstractRule rule;
private String ruleLimitApp;
public BlockException(String ruleLimitApp) {
this.ruleLimitApp = ruleLimitApp;
}
public BlockException(String ruleLimitApp, AbstractRule rule) {
this.ruleLimitApp = ruleLimitApp;
this.rule = rule;
}
public BlockException(String message, Throwable cause) {
super(message, cause);
}
public BlockException(String ruleLimitApp, String message) {
super(message);
this.ruleLimitApp = ruleLimitApp;
}
public BlockException(String ruleLimitApp, String message, AbstractRule rule) {
super(message);
this.ruleLimitApp = ruleLimitApp;
this.rule = rule;
}
public Throwable fillInStackTrace() {
return this;
}
public String getRuleLimitApp() {
return this.ruleLimitApp;
}
public void setRuleLimitApp(String ruleLimitApp) {
this.ruleLimitApp = ruleLimitApp;
}
public static boolean isBlockException(Throwable t) {
if (null == t) {
return false;
} else {
int counter = 0;
Throwable cause = t;
while(true) {
if (cause != null && counter++ < 50) {
if (!(cause instanceof BlockException) && !"SentinelBlockException".equals(cause.getMessage())) {
cause = cause.getCause();
continue;
}
return true;
}
return false;
}
}
}
public AbstractRule getRule() {
return this.rule;
}
static {
THROW_OUT_EXCEPTION.setStackTrace(sentinelStackTrace);
}
}
Sentinel响应兜底的方法
分为系统默认和客户自定义,两种
之前都是Sentinel系统默认的提示: Blocked by Sentinel (flow limiting)
这里可以使用@SentinelResource
注解(和以前的@HystrixCommand
注解类似)
代码添加接口/testHotKey
@GetMapping("/testHotKey")
@SentinelResource(value = "test_hotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value= "p2",required = false) String p2) {
return "---------testHotKey";
}
重启,先访问一次:http://localhost:8401/testHotKey?p1=a&p2=b
Sentinel界面添加热点规则配置:
表示1秒内资源名test_hotKey的第一个参数如果请求超过一次,就会熔断
测试:http://localhost:8401/testHotKey?p1=a&p2=b
设置自定义的响应
修改test_hotKey的@SentinelResource
添加blockHandler
,兜底方法 添加BlockException
@GetMapping("/testHotKey")
@SentinelResource(value = "test_hotKey",blockHandler = "blockHandler_hotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value= "p2",required = false) String p2) {
return "---------testHotKey";
}
public String blockHandler_hotKey(String p1, String p2,BlockException exception) {
return "---------testHotKey的blockHandler 处理。。。。。";
}
重启微服务
测试
这里熔断使用了自定义的响应参数
这里/testHotKey的p2参数是不做限制的
参数列外项
特殊情况:
我们期望第一个参数(p1)当时某个特殊值时,它的限流值和平时不一样
假设当p1的值等于0时,它的阀值可以达到200
配置:
点击 添加
按钮
测试:当p1=0 特殊待遇
这些的参数类型必须是String
其他:
假设在/testHotKey接口 添加异常看看
@GetMapping("/testHotKey")
@SentinelResource(value = "test_hotKey",blockHandler = "blockHandler_hotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value= "p2",required = false) String p2) {
int a = 10/0;
return "---------testHotKey";
}
public String blockHandler_hotKey(String p1, String p2,BlockException exception) {
return "---------testHotKey的blockHandler 处理。。。。。";
}
重启访问:
这里的blockHandler_hotKey
方法并不执行,
这里说@SentinelResource
注解是处理Sentinel控制台的违规情况,有blockHandler方法配置的兜底处理,
RunTimeException
int a = 10/0,这个是java运行报出的运行异常RunTimeException
,@SentinelResource
不管
总结@SentinelResource
主管配置出错,运行错误改走异常走异常
不管这里的 @SentinelResource
注解有fallback
可以处理
七、系统规则
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
背景
在开始之前,我们先了解一下系统保护的目的:
-
保证系统不被拖垮
-
在系统稳定的前提下,保持系统的吞吐量
长期以来,系统保护的思路是根据硬指标,即系统的负载 (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
估算得出。设定参考值一般是 CPUcores * 2.5
。 -
CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
-
平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
-
并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
-
入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
这里使用其中一个,全局入口 QPS 配置测试
对cloudalibaba-sentinel-service整个微服务接口限制请求量,一秒一次
测试:
八、@SentinelResource
1、按资源名称限制+后续处理
在微服务8401新增RateLimitController类
@RestController
@Slf4j
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "by_resource",blockHandler = "handlerException")
public CommonResult byResource() {
return new CommonResult(200,"按资源名称限流测试OK",new Payment(200L,"serial-001"));
}
public CommonResult handlerException(BlockException exception) {
return new CommonResult(500,exception.getClass().getCanonicalName() + "\t 服务不可用");
}
}
重启,先访问一次:http://localhost:8401/byResource OK
在Sentinel控制台界面配置
查看
配置
表示1秒钟内查询次数为1,就跑到我们自定义的handlerException方法处理,限流
测试:
代码handlerException(BlockException exception)
的 exception
对应com.alibaba.csp.sentinel.slots.block.flow.FlowException
处理
额外问题
此时关闭服务8401,在查看Sentinel控制台界面,流控规则消失了…
这说明Sentinel目前为止的流控配置都是临时性的
2、按Url地址限制+后续处理
通过访问URL来限流,会返回Sentinel自带默认的限流处理信息
在RateLimitController添加/rateLimit/byUrl 接口
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "by_url")
public CommonResult byUrl() {
return new CommonResult(200,"按url流测试OK",new Payment(200L,"serial-002"));
}
重启,先访问一次:http://localhost:8401/rateLimit/byUrl OK
Sentinel控制台界面配置
测试:
返回Sentinel自带默认的限流处理信息:Blocked by Sentinel (flow limiting)
3、上面兜底方案面临的问题
1、系统默认的,没有体现我们自己的业务要求。
2、依照现有条件,我们自定义的处理方法又和业务代码耦合在一起,不直观
3、每个业务方法都添加一个兜底的,那代码膨胀加剧
4、全局统一的处理方法没有体现
4、客户自定义限流处理逻辑
创建CustomerBlockHandler类用于自定义限流逻辑处理
package com.zzp.springcloud.myhandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.zzp.springcloud.entities.CommonResult;
/**
* @author zzp
* @create
*/
public class CustomerBlockHandler {
//这里方法必须是 static
public static CommonResult handlerException(BlockException exception){
return new CommonResult(500,"按客户自定义,异常 global handlerException----1");
}
public static CommonResult handlerException2(BlockException exception){
return new CommonResult(500,"按客户自定义,异常 global handlerException-----2");
}
}
在RateLimitController添加/rateLimit/customerBlockHandler 接口 在@SentinelResource
注解添加 blockHandlerClass
属性(自定义处理类)和 blockHandler
(指定处理方法)
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customer_blockHandler",
blockHandlerClass = CustomerBlockHandler.class,
blockHandler="handlerException2")
public CommonResult customerBlockHandler() {
return new CommonResult(200,"按客户自定义流测试OK",new Payment(200L,"serial-003"));
}
重启服务8401,先访问一次:http://localhost:8401/rateLimit/customerBlockHandler OK
Sentinel控制台界面配置
测试:
进一步说明
5、更多注解属性说明
官网
官网介绍可以用代码的形式设置配置,这些就不多介绍了
@SentinelResource
注解
注意:注解方式埋点不支持 private 方法
@SentinelResource
用于定义资源,并提供可选的异常处理和 fallback
配置项, @SentinelResource
注解包含以下属性:
-
value
:资源名称,必需项(不能为空) -
entryType:entry
类型,可选项(默认为 EntryType.OUT) -
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函数名称,可选项,通常用于通用的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
直接抛出。
Sentinel注意有三个核心Api:
-
SphU
定义资源 -
Tracer
定义统计 -
ContextUtil
定义了上下文
九、服务熔断
sentinel整合ribbon+openFeign+fallback
1、Ribbon系列
启动nacos和sentinel
新建Module:cloudalibaba-provide-payment-9003/9004作为微服务提供者
pom文件:
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自定义的API通用包 实体类 -->
<dependency>
<groupId>com.zzp.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--web actuator-->
<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: 9003/9004
spring:
application:
name: nacos-payment-provider # 微服务名称
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置Nacos地址
# 暴露监控端点 ‘*’ 全部打开
management:
endpoints:
web:
exposure:
include: '*'
启动类:
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003/9004 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class, args);
}
}
控制层:
@RestController
@Slf4j
public class PaymentController {
@Value("${server.port}")
private String serverPort;
// 这里为了偷懒 就不连接数据库了
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L,new Payment(1L,"zzp001"));
hashMap.put(2L,new Payment(2L,"zzp002"));
hashMap.put(3L,new Payment(3L,"zzp003"));
}
@GetMapping("/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 – OK
新建Module:cloudalibaba-consumer-nacos-order-84作为微服务消费者
pom文件:
<dependencies>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自定义的API通用包 实体类 -->
<dependency>
<groupId>com.zzp.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--web actuator-->
<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-cosumer # 微服务名称
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置Nacos地址
sentinel:
transport:
# 配置sentinel dashboard地址
dashboard: 127.0.0.1:8080
# 默认8719端口,假如被占用会自动从8719开始依次+1扫描,直到找到未占用的端口
port: 8719
# 消费者将要去访问的微服务名称(注册成功进的nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
启动类:
@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain84 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
业务控制类:
@RestController
@Slf4j
public class CircleBreakerController {
@Resource
private RestTemplate restTemplate;
private String serverUrl = "http://nacos-payment-provider";
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback")//没有配置
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
CommonResult commonResult = restTemplate.getForObject(serverUrl + "/paymentSQL/" + id, CommonResult.class,id);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常...");
}else if(commonResult.getData() == null){
throw new NullPointerException("NullPointerException,该ID没有对应的记录,空指针异常...");
}
return commonResult;
}
}
启动。测试:http://localhost:84/consumer/fallback/1
查看sentinel控制台界面:
只配置fallback
修改84微服务的CircleBreakerController类,添加Sentinel兜底方法
@GetMapping("/consumer/fallback/{id}")
// @SentinelResource(value = "fallback")//没有配置
@SentinelResource(value = "fallback",fallback = "handlerFallback")//fallback只负责业务异常
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
CommonResult commonResult = restTemplate.getForObject(serverUrl + "/paymentSQL/" + id, CommonResult.class,id);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常...");
}else if(commonResult.getData() == null){
throw new NullPointerException("NullPointerException,该ID没有对应的记录,空指针异常...");
}
return commonResult;
}
//本例是fallback
public CommonResult<Payment> handlerFallback(@PathVariable("id") Long id,Throwable e){
Payment payment = new Payment(id,null);
return new CommonResult<>(500,"兜底异常handlerFallback,exception内容:"+e.getMessage(),payment);
}
重启,测试:
这些说明@SentinelResource
的属性fallback
可以管理运行异常
只配置blockHandler
修改CircleBreakerController类
@GetMapping("/consumer/fallback/{id}")
// @SentinelResource(value = "fallback")//没有配置
// @SentinelResource(value = "fallback",fallback = "handlerFallback")//fallback只负责业务异常
@SentinelResource(value = "fallback",blockHandler = "blockHandler")//blockHandler只负责sentinel控制台配置违规
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
CommonResult commonResult = restTemplate.getForObject(serverUrl + "/paymentSQL/" + id, CommonResult.class,id);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常...");
}else if(commonResult.getData() == null){
throw new NullPointerException("NullPointerException,该ID没有对应的记录,空指针异常...");
}
return commonResult;
}
//本例是fallback
// public CommonResult<Payment> handlerFallback(@PathVariable("id") Long id,Throwable e){
// Payment payment = new Payment(id,null);
// return new CommonResult<>(500,"兜底异常handlerFallback,exception内容:"+e.getMessage(),payment);
// }
//本例是blockHandler
public CommonResult<Payment> blockHandler(@PathVariable("id") Long id, BlockException e){
Payment payment = new Payment(id,null);
return new CommonResult<>(500,"blockHandler-sentinel限流,无此流水号.blockException:"+e.getMessage(),payment);
}
重启:先访问一次:http://localhost:84/consumer/fallback/1 OK
在sentinel控制台界面添加配置:
测试:
fallback
和blockHandler
都配置
修改CircleBreakerController类:
@GetMapping("/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")
public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
CommonResult commonResult = restTemplate.getForObject(serverUrl + "/paymentSQL/" + id, CommonResult.class,id);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常...");
}else if(commonResult.getData() == null){
throw new NullPointerException("NullPointerException,该ID没有对应的记录,空指针异常...");
}
return commonResult;
}
//本例是fallback
public CommonResult<Payment> handlerFallback(@PathVariable("id") Long id,Throwable e){
Payment payment = new Payment(id,null);
return new CommonResult<>(500,"兜底异常handlerFallback,exception内容:"+e.getMessage(),payment);
}
//本例是blockHandler
public CommonResult<Payment> blockHandler(@PathVariable("id") Long id, BlockException e){
Payment payment = new Payment(id,null);
return new CommonResult<>(500,"blockHandler-sentinel限流,无此流水号.blockException:"+e.getMessage(),payment);
}
重启,先访问一次:http://localhost:84/consumer/fallback/1 OK
在sentinel控制台界面删除以前的添加新的配置:
测试:
此时 blockHandler
配置是有效的
若 blockHandler
和 fallback
都进行了配置,则被限流降级而抛出 BlockException
时只会进入 blockHandler
处理逻辑。
忽略属性
修改,重启,访问:
2、Feign系列
修改84微服务:
84消费者调用提供者9003
Feign组件一般消费者
84微服务
pom文件引入依赖:
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
application.yml配置文件添加配置:
# 激活Sentinel对feign的支持
feign:
sentinel:
enabled: true
启动类添加 @EnableFeignClients
注解
添加feign调用类:
PaymentFeign
package com.zzp.springcloud.feign;
import com.zzp.springcloud.entities.CommonResult;
import com.zzp.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @author zzp
* @create
*/
@FeignClient(value = "nacos-payment-provider",fallback = PaymentFeignFallback.class)
public interface PaymentFeign {
@GetMapping("/paymentSQL/{id}")
CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
PaymentFeignFallback
package com.zzp.springcloud.feign;
import com.zzp.springcloud.entities.CommonResult;
import com.zzp.springcloud.entities.Payment;
import org.springframework.stereotype.Component;
/**
* @author zzp
* @create
*/
@Component
public class PaymentFeignFallback implements PaymentFeign{
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(500,"服务降级返回,---PaymentFeignFallback.paymentSQL",new Payment(id,"errorserial"));
}
}
CircleBreakerController控制类添加接口:
@Resource
private PaymentFeign paymentFeign;
@GetMapping("/consumer/paymentSQL/{id}")
CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
return paymentFeign.paymentSQL(id);
}
启动84和9003微服务
访问:http://localhost:84/consumer/paymentSQL/1
测试84调用9003,此时故意关闭9003微服务通过者,看84消费者会不会自动降级
3、熔断框架比较
Sentinel | Hystrix | Resilience4j | |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比例、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
流量整形 | 支持预热模式、均速器模式、预热排队模式 | 不支持 | 简单的 Rate Limiter 模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
十、规则持久化
一旦我们重启应用,Sentinel控制台的规则将消失,生产环境需要将配置规则进行持久化
将限流规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上的sentinel上的流控规则持续有效
启动8401微服务:
访问:http://localhost:8401/rateLimit/byUrl
在sentinel控制台界面添加流控规则
测试:
把8401重启一下,
访问:http://localhost:8401/rateLimit/byUrl
在sentinel控制台界面刷新一下
步骤
修改cloudalibaba-sentinel-service-8401微服务
1、引入依赖
<!--SpringCloud ailibaba sentinel-datasource-nacos -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
2、yml添加配置
spring:
application:
name: cloudalibaba-sentinel-service # 微服务名称
cloud:
nacos:
discovery:
# 配置Nacos 注册中心
server-addr: 127.0.0.1:8848
sentinel:
transport:
# 配置sentinel dashboard地址
dashboard: 127.0.0.1:8080
# 默认8719端口,假如被占用会自动从8719开始依次+1扫描,直到找到未占用的端口
port: 8719
datasource:
ds1:
nacos:
server-addr: 127.0.0.1:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
3、添加Nacos业务规则配置
[
{
"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表示排队等待;clusterMode
: 是否集群。
4、启动微服务,刷新sentinel控制台
启动8401后,先访问:http://localhost:8401/rateLimit/byUrl
然后刷新sentinel控制台发现业务规则有了
5、快速访问测试接口
测试sentinel的限流规则,OK
6、停止服务在看sentinel控制台
7、重启服务再看sentinel控制台
这里需要 多次调用:http://localhost:8401/rateLimit/byUrl
然后刷新sentinel控制台,配置重新出现了,说明持久化验证通过