一、Sentinel简介
1、Sentinel 是什么
官网: https://github.com/alibaba/Sentinel/
中文wiki:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
我们之前讲解过 Hystrix ,它也实现了降级与熔断,但是它有如下缺点:
- 需要我们程序员自己手动搭建监控平台
- 没有一套 web 界面可以给我们进行更加细粒度话的配置:流控、速率控制、服务熔断、服务降级。。。。。。
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性:
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
- 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
2、Sentinel 能干嘛
二、安装 Sentinel 控制台
Sentinel 由两部分组成:后台、前台8080。
Sentinel 分为两个部分:
- 和辛苦(Java 客户端)不依赖任何框架/库,能够运行于所有 java 运行时环境,同时对 Dubbo/Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 SpringBoot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
1、安装步骤
下载
下载地址:https://github.com/alibaba/Sentinel/releases ,目前最新版本为 1.8.2
运行
前提:Java8 环境OK、8080端口不能被占用
输入以下命令:
java -jar sentinel-dashboard-1.8.2.jar
出现如下界面说明启动成功
访问
默认 Sentinel 用户名和密码: sentinel 、sentinel
在浏览器输入 http://localhost:8080/
三、初始化演示工程
启动 Nacos 8848。
新建 Module: cloudalibaba-sentinel-service8401
pom
<dependencies>
<!-- alibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- alibaba sentinel-datasource-nacos,后续持久化用到 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!-- alibaba 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>
<!-- 引入自己的 cloud-api-commons 模块-->
<dependency>
<groupId>com.cloud.study</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot 整合 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>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
yaml
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos 服务注册中心
sentinel:
transport:
dashboard: localhost:8080 #sentinel dashboard 地址
port: 8719 #默认 8719端口,假如被占用会自动从 8719开始依次+1扫描,直至找未被占用的端口
#暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
主启动
@EnableDiscoveryClient
@SpringBootApplication
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
业务测试类
@RestController
public class FlowLimitController {
@GetMapping(value = "/testA")
public String testA(){
return "-------testA";
}
@GetMapping(value = "/testB")
public String testB(){
return "-------testB";
}
}
启动 8401
启动 Sentinel 8080
此时 微服务 8401 已经入驻进 Nacos
进入 Sentinel Dashboard 空空如也,这是因为 Sentinel 采用的懒加载,只有访问页面的时候才会出现。分别访问 http://localhost:8401/testA、http://localhost:8401/testB
四、流控制规则
1、基本介绍
资源名:唯一名称,默认请求路径。
针对来源: Sentinel 可以针对调用者进行限流,填写微服务名,默认 default(不区分来源)。
阈值类型/单机阈值:
- QPS(每秒请求数量):当调用该 api 的 QPS 达到阈值的时候,进行限流。
- 线程数:当调用该 api 的线程数量达到阈值的时候,进行限流。
是否集群:不需要集群。
流控模式:
- 直接:api 达到限流条件时,直接限流。
- 关联:当关联的资源达到阈值时,就限流自己。
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api 级别的针对来源】
流控效果:
- 快速失败:直接失败,抛异常。
- warm up:根据 codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值。
- 排队等待:匀速排队,让请求以均匀的速度通过,阈值类型必须设置为 QPS,否则无效。
2、流控-QPS直接失败
上图表示一秒钟内查询1次OK(http://localhost:8401/testA),若超过一次就直接快速失败,报默认错误。
思考:现在限流的出错页面是系统自带了,我们能不能使用自己自定义的出错页面??
3、流控-线程数直接失败
修改之前的流控配置,将阈值类型改成:并发线程数。
上图表示当处理线程数是1时OK,当处理线程数超过1时,报默认错误。
此时,快速的刷新 http://localhost:8401/testA 页面,并不会报错。因为此时1个线程可以处理我们的请求。
我们修改下 controller ,增加延时1秒内只能处理一个请求
@GetMapping(value = "/testA")
public String testA() throws InterruptedException {
TimeUnit.SECONDS.sleep(1);
return "-------testA";
}
此时打开两个 http://localhost:8401/testA 页面,并快速的刷新,就会出现报错。
4、流控-关联
当关联的资源达到阈值时,就限流自己。
比如我们的例子中,当与testA关联的testB达到阈值后,就限流A自己。
设置效果:当关联资源 testB 的qps 阈值超过1时,就限流 testA的访问。
测试
使用 postMan 新建一个请求,测试后能正常访问
点击小图片出现run按钮界面
点击 run按钮,配置循环次数和时间间隔
此时点击 【run测试】按钮 ,立刻访问 testA,发现A 被限流。
等过了一会 postMan 跑完之后,testA又可以正常访问。
由此可以总结出:当testB达到阈值后,testA被限制了。
5、流控-预热
公式:阈值除以 coldFactor(默认值为3),经过预热时长后才会达到阈值。
Warm Up方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过“冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
coldFactor 默认值为3,即请求QPS从 【阈值/3】 开始,经过预热时长设定置 QPS 阈值。
例如下图,就表示 testA 刚刚开始时,阈值为 3(10/3),阈值慢慢增加,等到5秒后,阈值为10。
我们按照上图进行设置,一直快速刷新 http://localhost:8401/testA 页面。刚开始的时候会出现限流的报错页面,后来慢慢的页面出现的次数少了,等5秒后,就不会再出现报错页面了。
应用场景:如秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阈值。
6、流控-排队等待
匀速排队,让请求以均匀的速度通过,阈值类型必须设置成 QPS,否则无效。
设置含义: /testA 每秒1次请求,超过的话就排队等待,等待的超时时间为20000毫秒。
五、降级规则
1.基本介绍
熔断降级 · alibaba/Sentinel Wiki · GitHub
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或者异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其他资源而导致级联错误。
当资源被迫降级后,在接下来的降级时间窗口期内,对该资源的调用都自动熔断(默认行为是抛出DegradeException)。
注意,Sentinel 的断路器是没有半开状态的。半开的状态由系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器。具体可参考 Hystrix。
Sentinel 提供以下几种熔断策略:
- 慢调用比例(SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长内请求数目大于设置的最小请求数,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF_OPEN),若接下来的一个请求小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用则会再次熔断。
- 异常比例(Error_RATIO):当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF_OPEN),若接下来的请求成功完成(没有错误)则结束熔断,否则会再次熔断。异常比例的阈值范围是【0.0 ~ 1.0】,代表 0% ~ 100%。
- 异常数(ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(
BlockException
)不生效。为了统计异常比例或异常数,需要通过Tracer.trace(ex)
记录业务异常。
2、降级策略实战 - 慢调用比例
在controller 中加入 testD
@GetMapping(value = "/testD")
public String testD(){
//暂停一秒
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "-------testD";
}
配置降级策略:当1秒内有超过5次的请求,且RT超过200毫秒的请求占比超过50%,则会熔断3秒钟。
Jmeter 压力测试,1秒内有10个线程进行 testD 请求
结论:
当启动 Jemeter 的时候,浏览器访问 访问 test 会出现如下提示。
3、降级策略实战 - 异常比例
修改 testD 方法
@GetMapping(value = "/testD")
public String testD(){
log.info("testD 测试异常比例");
int age = 10/0;
return "-------testD";
}
配置降级策略:当1秒内有超过5次的请求,且异常的比例超过 30%,则会熔断3秒钟。
Jmeter 压力测试,1秒内有10个线程进行 testD 请求,按照【3、降级策略实战 - 异常比例】进行配置。
当启动 Jemeter 的时候,浏览器访问 访问 test 会出现如下提示。
4、降级策略实战 - 异常数
配置降级策略:当1秒内有超过5次的请求,且异常数量超过2个,则会熔断3秒钟。
Jmeter 压力测试,1秒内有10个线程进行 testD 请求,按照【3、降级策略实战 - 异常比例】进行配置。
当启动 Jemeter 的时候,浏览器访问 访问 test 会出现如下提示。
六、热点 key 限流
热点参数限流 · alibaba/Sentinel Wiki · GitHub
何为热点?热点即经常访问的数据。很多时候我们系统统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品ID 为参数,统计一段时间内最常购买的商品ID并进行限制。
- 用户ID 为参数,针对一段时间内频繁访问的用户ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
兜底方法分为系统默认和用户自定义两种。之前我们测试的时候,都是用 sentinel 系统默认的提示:Blocked by Sentinel(flow limiting)。我们可以使用 @SentinelResource 注解来实现。
1.基本配置测试
下面来进行配置,首先修改 controller
@GetMapping(value = "/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";
}
/**
* 参数与 testHotKey 一致,并增加 BlockException
*/
public String deal_testHotKey(String p1, String p2, BlockException exception){
return "deal_testHotKey 繁忙";
}
sentinel 配置:testHotKey 资源,在1秒内超过1个一个请求(带p1参数),则会进行限流。
注意:资源名要是 @SentinelResource 的值,参数索引是从0开始,比如方法
testHotKey(@RequestParam(value = "p1",required = false) String p1,@RequestParam(value = "p2",required = false) String p2)
第0个参数是 p1,第1个参数是p2。
测试:
- 浏览器快速刷新 http://localhost:8401/testHotKey?p1=a&p2=b,多刷新几次就会出现如下错误。
- 浏览器快速刷新不带p1参数的链接,比如 http://localhost:8401/testHotKey?p2=b,都能正常访问。
2、额外参数配置
上个案例演示了第一个参数 p1,当 QPS 超过1秒1次点击后马上被限流。我们期望 p1 参数,当它是某个特殊值的时候,它的限流和平时不一样。加入 p1 的值为 5 时,它的阀值可以达到200。
在 Sentinel 控制台进行配置
当快速刷新 http://localhost:8401/testHotKey?p1=1 时,会出现报错页面。而当快速刷新 http://localhost:8401/testHotKey?p1=5 时,页面正常。
注意:参数类型必须是基本数据类型和String。
另外,当出现 运行时异常时,@SentinelResource 的 blockHandler 的兜底方法不会进入,会直接显示错误页面。
七、系统规则
热点参数限流 · alibaba/Sentinel Wiki · GitHub
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、cpu十一率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能在最大的吞吐量的同时保证系统整体的稳定性。
系统规则支持以下模式:
- Load 自适应(仅对 Unix/linux 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps*minRt 估算得出。设定参考值一般是 CPU usage * 2.5 。
- CPU usage(1.5.0+版本):当系统的 CPU 使用率超过阈值即触发系统保护(取值范围 0.0 - 1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的QPS达到阈值即触发系统保护。
比如我们配置如下规则,入口 QPS 阈值为 1
此时快速系统中的任何链接(例如 testA、testB)时,都会触发限流。
八、@SentinelResource
1、按资源名称限流 + 后续处理
1.1 修改 cloudalibaba-sentinel-service8401 模块
在 cloudalibaba-sentinel-service8401 模块中新增一个 controller
@RestController
@Slf4j
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public R byResource() {
return new R(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
}
public R handleException(BlockException exception){
return new R(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
}
}
1.2 Sentinel 控制台配置
按照 @SentinelResource 中的资源名称配置我们的限流规则
1.3 测试
一秒点击一下(http://localhost:8401/byResource)OK。
超过上述,疯狂点击,返回了自己定义的限流处理信息,限流发生。
1.4 额外问题
关闭服务 8401 后,过会后刷新 Sentinel Dashboard,发现我们配置的流控规则消失了。
2、按照 url 地址限流 + 后续处理
2.1 修改 cloudalibaba-sentinel-service8401 模块
在 cloudalibaba-sentinel-service8401 模块RateLimitController 增加 byUrl 方法
@GetMapping("/byUrl")
@SentinelResource(value = "byUrl")
public R byUrl() {
return new R(200, "按url限流测试OK", new Payment(2021L, "serial002"));
}
2.2 Sentinel 控制台配置
我们在资源名中配置的是 url (/byUrl)
2.3 测试
一秒点击一下(http://localhost:8401/byUrl)OK。
超过上述,疯狂点击,返回了Sentinel自带的限流处理结果。
3、上面兜底方案面临的问题
系统默认的兜底方案,没有体现我们自己的业务要求。
依照现有条件,我们自定义的处理方案又和业务代码耦合在一起,不直观。
每个业务方法都添加一个兜底方法,代码会膨胀。
全局统一的处理方法没有体现。
4、客户自定义限流处理逻辑
4.1 创建 CustomerBlockHandler 类用于自定义限流处理逻辑
/**
* 全局统一的兜底处理类
*/
public class CustomerBlockHandler {
// 要求:
// 1 当前方法的返回值和参数要跟原方法一致
// 2 参数列表与原方法匹配,并在最后添加一个BlockException 类型参数
// 3 需要用static修饰
public static R handleException1(BlockException exception){
return new R(444, "用户自定义,全局 handleException ---- 1");
}
// 要求:
// 1 当前方法的返回值和参数要跟原方法一致
// 2 参数列表与原方法匹配,并在最后添加一个BlockException 类型参数
// 3 需要用static修饰
public static R handleException2(BlockException exception){
return new R(444, "用户自定义,全局 handleException ---- 2");
}
}
4.2 RateLimitController 修改
@RestController
@Slf4j
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException1")
public R byResource() {
return new R(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
}
@GetMapping("/byUrl")
@SentinelResource(value = "byUrl", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException2")
public R byUrl() {
return new R(200, "按url限流测试OK", new Payment(2021L, "serial002"));
}
}
4.3 Sentinel 控制台配置
byUrl使用的是按 url 配置,byResource 是按照资源名配置,如下:
4.4 测试
疯狂点击 http://localhost:8401/byResource,返回了我们自定义的结果。
疯狂点击 http://localhost:8401/byUrl,返回了Sentinel自带的限流处理结果,这说明自定义处理方法只针对资源名称的配置才会生效。
我们修改 byUrl 的 Sentinel 控制台配置,按照资源名称配置
疯狂点击 http://localhost:8401/byUrl,出现了我们自定义的处理页面。
5、更多注解属性说明
https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81
Sentinel 主要由三个核心API:
- SphU 定义资源
- Tracer 定义统计
- ContextUtil 定义了上下文
@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.8.0 版本开始, defaultFallback 支持在类级别进行配置。1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException
)进行处理,不能针对业务异常进行处理。
特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException
时只会进入 blockHandler
处理逻辑。若未配置 blockHandler
、fallback
和 defaultFallback
,则被限流降级时会将 BlockException
直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException
)。
九、服务熔断功能
sentinel 整合 Ribbon + openFeign + fallback
2、Ribbon 系列
启动 nacos 和 sentinel
提供者 9003/9004
9003和9004工程内容几乎一样,只是配置文件端口不一样
新建 module :cloudalibaba-provider-payment9003
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>cloud-study</artifactId>
<groupId>com.cloud.study</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloudalibaba-provider-payment9003</artifactId>
<dependencies>
<!-- alibaba-nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己的 cloud-api-commons 模块-->
<dependency>
<groupId>com.cloud.study</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</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>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
pom
server:
port: 9003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848
#暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
主启动
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class, args);
}
}
业务类
@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, "1213312qe123123123123111"));
hashMap.put(2L, new Payment(2L, "1213312qe123123123123112"));
hashMap.put(3L, new Payment(3L, "1213312qe123123123123113"));
}
@GetMapping("/paymentSQL/{id}")
public R<Payment> paymentSQL(@PathVariable("id") Long id){
Payment payment = hashMap.get(id);
return new R<>(200,"from mysql, serverPort:" + serverPort, payment);
}
}
消费者84
新建 module:cloudalibaba-consumer-order84
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>cloud-study</artifactId>
<groupId>com.cloud.study</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloudalibaba-consumer-order84</artifactId>
<dependencies>
<!-- alibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- alibaba sentinel-datasource-nacos,后续持久化用到 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!-- alibaba 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>
<!-- 引入自己的 cloud-api-commons 模块-->
<dependency>
<groupId>com.cloud.study</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot 整合 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>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
yaml
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos 服务注册中心
sentinel:
transport:
dashboard: localhost:8080 #sentinel dashboard 地址
port: 8719 #默认 8719端口,假如被占用会自动从 8719开始依次+1扫描,直至找未被占用的端口
service-url:
nacos-user-service: http://nacos-payment-provider
#暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
主启动
@SpringBootApplication
@EnableDiscoveryClient
public class OrderMain84 {
public static void main(String[] args) {
SpringApplication.run(OrderMain84.class, args);
}
}
配置类
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
controller
@RestController
@Slf4j
public class CircleBreakerController {
@Value("${service-url.nacos-user-service}")
private String SERVICE_URL;
@Resource
private RestTemplate restTemplate;
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback")
public R<Payment> fallback(@PathVariable("id") Long id){
if (id == 4) {
throw new IllegalArgumentException("非法参数异常......");
}
R<Payment> r = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, R.class, id);
if (r.getData() == null) {
throw new NullPointerException("NullPointException,该 ID 没有对应记录,空指针异常");
}
return r;
}
}
测试:
- 访问 http://localhost:84/consumer/fallback/1 正常,刷新几次发现ribbon负载均衡是生效的
- 访问 http://localhost:84/consumer/fallback/4 出现报错页面,不友好
消费者84 - @SentinelResource 只配置 fallback
fallback 只负责运行时异常
修改 controller 代码
@RestController
@Slf4j
public class CircleBreakerController {
@Value("${service-url.nacos-user-service}")
private String SERVICE_URL;
@Resource
private RestTemplate restTemplate;
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback")
public R<Payment> fallback(@PathVariable("id") Long id){
if (id == 4) {
throw new IllegalArgumentException("非法参数异常......");
}
R<Payment> r = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, R.class, id);
if (r.getData() == null) {
throw new NullPointerException("NullPointException,该 ID 没有对应记录,空指针异常");
}
return r;
}
public R handlerFallback(@PathVariable("id") Long id, Throwable e) {
return new R(444,"兜底异常 handlerFallback,exception 内容 " + e.getMessage(), new Payment(id, "null"));
}
}
测试:
- 访问 http://localhost:84/consumer/fallback/4 异常信息是我们自定义的
消费者84 - @SentinelResource 只配置 blockHandler
blockHandler 只负责 sentinel 控制台配置违规
修改 controller 代码
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", blockHandler = "blockHandler")
public R<Payment> fallback(@PathVariable("id") Long id){
if (id == 4) {
throw new IllegalArgumentException("非法参数异常......");
}
R<Payment> r = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, R.class, id);
if (r.getData() == null) {
throw new NullPointerException("NullPointException,该 ID 没有对应记录,空指针异常");
}
return r;
}
public R blockHandler(@PathVariable("id") Long id, BlockException e) {
return new R(444,"blockHandler-sentinel 限流,无此流水:BlockException " + e.getMessage(), new Payment(id, "null"));
}
在sentinel 控制台给 fallback 配置流控规则
测试(验证了 @SentinelResource的blockHandler 只负责 sentinel 控制台配置违规):
- 访问一次 http://localhost:84/consumer/fallback/4 出现报错页面
- 快速刷新几次 http://localhost:84/consumer/fallback/4 出现我们自定义的报错
消费者84 - @SentinelResource 配置 fallback、blockHandler
修改 controller
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")
public R<Payment> fallback(@PathVariable("id") Long id){
if (id == 4) {
throw new IllegalArgumentException("非法参数异常......");
}
R<Payment> r = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, R.class, id);
if (r.getData() == null) {
throw new NullPointerException("NullPointException,该 ID 没有对应记录,空指针异常");
}
return r;
}
//fallback 兜底函数
public R handlerFallback(@PathVariable("id") Long id, Throwable e) {
return new R(444,"兜底异常 handlerFallback,exception 内容 " + e.getMessage(), new Payment(id, "null"));
}
// sentinel 配置违规兜底
public R blockHandler(@PathVariable("id") Long id, BlockException e) {
return new R(444,"blockHandler-sentinel 限流,无此流水:BlockException " + e.getMessage(), new Payment(id, "null"));
}
在sentinel 控制台给 fallback 配置流控规则
测试:
- 访问一次 http://localhost:84/consumer/fallback/4 出现fallback兜底函数信息
消费者84 - @SentinelResource 增加配置 exceptionsToIgnore
修改controller,在 @SentinelResource 注解增加 exceptionsToIgnore 配置。它说明当出现 IllegalArgumentException 这个异常的时候,fallback 兜底方法不生效。
@RestController
@Slf4j
public class CircleBreakerController {
@Value("${service-url.nacos-user-service}")
private String SERVICE_URL;
@Resource
private RestTemplate restTemplate;
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback",
blockHandler = "blockHandler",
exceptionsToIgnore = {IllegalArgumentException.class})
public R<Payment> fallback(@PathVariable("id") Long id){
if (id == 4) {
throw new IllegalArgumentException("非法参数异常......");
}
R<Payment> r = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, R.class, id);
if (r.getData() == null) {
throw new NullPointerException("NullPointException,该 ID 没有对应记录,空指针异常");
}
return r;
}
//fallback 兜底函数
public R handlerFallback(@PathVariable("id") Long id, Throwable e) {
return new R(444,"兜底异常 handlerFallback,exception 内容 " + e.getMessage(), new Payment(id, "null"));
}
// sentinel 配置违规兜底
public R blockHandler(@PathVariable("id") Long id, BlockException e) {
return new R(444,"blockHandler-sentinel 限流,无此流水:BlockException " + e.getMessage(), new Payment(id, "null"));
}
}
测试:
- 访问一次 http://localhost:84/consumer/fallback/4 (此时会出现 IllegalArgumentException 异常)出现报错页面
- 而访问 http://localhost:84/consumer/fallback/5 出现我们自定义的报错
3、Feign 系列
修改 cloudalibaba-consumer-order84 模块。
注意,项目启动可能会失败,是因为 SpringCloud 与 SpringCloudAlibaba 的版本不对应导致的,我修改了父Pom的版本如下,就可以正常启动了
pom 文件新增 openFeign 依赖
<!-- openFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
yaml 增加 openFeign 的配置
#激活sentinel对Feign的支持
feign:
sentinel:
enabled: true
主启动类增加 @EnableFeignClients 注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderMain84 {
public static void main(String[] args) {
SpringApplication.run(OrderMain84.class, args);
}
}
增加带 @FeignClient 的接口
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping("/paymentSQL/{id}")
public R<Payment> paymentSQL(@PathVariable("id") Long id);
}
增加降级处理类
/**
* 兜底方法
*/
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public R<Payment> paymentSQL(Long id) {
return new R(444,"服务降级返回");
}
}
controller 增加如下内容
@Resource
private PaymentService paymentService;
@GetMapping("/consumer/paymentSQL/{id}")
public R<Payment> paymentSQL(@PathVariable("id") Long id){
return paymentService.paymentSQL(id);
}
测试:
- 访问 http://localhost:84/consumer/paymentSQL/1 正常
- 关闭 payment9003/9004 服务,再访问 http://localhost:84/consumer/paymentSQL/1 ,会出现降级异常
4、熔断框架比较
十、规则持久化
1、是什么
一旦我们重启应用,sentinel控制台的规则将消失,生成环境需要将配置规则进行持久化
2、怎么玩
将限流规则持久化仅 Nacos 保存,只需要刷新 8401 某个 rest 地址, sentinel 控制台的流控规则就能看到,只要 Nacos 里面的配置不删除,针对 8401 上 sentinel 上的流控规则持续有效。
3、步骤
修改 cloudalibaba-sentinel-service8401 。
POM 中需要有 sentinel-datasource-nacos 的依赖
<!-- alibaba sentinel-datasource-nacos,后续持久化用到 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
yaml 配置中 sentinel 要添加 nacos 和 持久化配置
spring:
cloud:
sentinel:
# nacos 持久化 sentinel 控制台配置
datasource:
ds1:
nacos:
server-addr: localhost:8848 #nacos 注册中心
dataID: cloudalibaba-sentinel-service #本项目的 spring.application.name
groupId: DEFAULT_GROUP #默认分组
data-type: json
rule-type: flow
添加 Nacos 业务规则配置
[
{
"resource":"/byUrl",
"limit":"default",
"grade":1,
"count":1,
"strage":0,
"controlBehavior":0,
"clusterMode":false
}
]
resource:来源名称
limitApp:来源应用
grade:阈值类型,0 线程数,1 QPS
count:阈值
strategy:流控模式,0 直接,1 关联,2链路
controlBehavior:流控效果, 0 直接失败, 1 Warm Up,2排队等待
clusterMode:是否集群
这种做法比较麻烦,后续看还有没有更加好的办法。