Sentinel 实现熔断与限流 1、Sentinel 介绍 官方 Github
官方文档
1-1、是什么 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
丰富的应用场景: Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控: Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
广泛的开源生态: Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
完善的 SPI 扩展点: Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 的主要特性:
—句话解释,之前我们讲解过的Hystrix 。
2-2、Hystrix 与 Sentinel 比较 Hystrix:
Sentinel:
单独一个组件,可以独立出来。
直接界面化的细粒度统一配置。
约定 > 配置 > 编码
都可以写在代码里面,但是我们本次还是大规模的学习使用配置和注解的方式,尽量少写代码
2、Sentinel 下载安装运行 官方文档
服务使用中的各种问题:
Sentinel 分为两个部分:
2-1、安装步骤 下载 https://github.com/alibaba/Sentinel/releases
运行命令 前提:java8 环境 OK + 8080 端口不能被占用
运行命令:
1 java -jar sentinel-dashboard-1.7 .1 .jar
由于我的 8080 端口被占用,我换成了 8888 端口
1 java -jar sentinel-dashboard-1.7 .1 .jar --server.port=8888
访问 sentinel 管理界面 http://localhost:8080,登录账号密码均为 sentinel
3、初始化演示工程 启动 Nacos8848 成功
3-1、新建 cloudalibaba-sentinel-service8401 pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 <?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 > springCloud2023</artifactId > <groupId > com.jcvv.springcloud</groupId > <version > 1.0-SNAPSHOT</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > cloudalibaba-sentinel-service8401</artifactId > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-datasource-nacos</artifactId > </dependency > <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-transport-simple-http</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-sentinel</artifactId > </dependency > <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 > 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 > <dependency > <groupId > com.jcvv.springcloud</groupId > <artifactId > cloud-api-common</artifactId > <version > ${project.version}</version > </dependency > </dependencies > </project >
application.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 server: port: 8401 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: 119.3 .211 .104 :8089 sentinel: transport: dashboard: localhost:8080 port: 8719 client-ip: localhost eager: true management: endpoints: web: exposure: include: '*'
主启动类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.jcvv.springcloud;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication @EnableDiscoveryClient public class MainApp8401 { public static void main (String[] args) { SpringApplication.run(MainApp8401.class,args); } }
controller 层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.jcvv.springcloud.controller;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestController @Slf4j public class FlowLimitController { @GetMapping("/testA") public String testA () { return "------testA" ; } @GetMapping("/testB") public String testB () { log.info(Thread.currentThread().getName() + "\t" + "...testB" ); return "------testB" ; } }
测试 空空如也,啥都没有
Sentinel 采用的懒加载说明:
执行一次访问即可:http://localhost:8401/testA、http://localhost:8401/testB
结论:sentinel8080 正在监控微服务 8401
遇到问题
注意:云服务器 的要开放 8719 端口,且项目也必须在服务器上 。
百度一下,网上关于解决错误简直是胡言乱语。扯淡成分太多了,妈了个巴子的,害的我研究了一天
原因:要使用 sentinel,必须双方都能连通,而服务器网络 ping 不通本机网络
参考文章:https://huaweicloud.csdn.net/633113d9d3efff3090b51790.html
遇到 timeout 之类的错误
解决方式:添加client-ip
即 sentinel.transport.client-ip:192.168.43.116
其中这里的地址为项目启动设备的 ip 地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 spring: application: name: cloudalibaba-sentinel-service cloud: nacos: discovery: server-addr: 119.3 .211 .104 :8089 sentinel: transport: dashboard: 192.168 .43 .116 :8080 port: 8719 client-ip: 192.168 .43 .116 eager: true
4、流控规则 4-1、基本介绍
解释说明
QPS 和线程数的区别
QPS 类似于银行的保安:所有的请求到 Sentinel 后,他会根据阈值放行,超过报错
线程数类似于银行的窗口:所有的请求会被放进来,但如果阈值设置为 1,那么其他的请求就会报错也就是,银行里只有一个窗口,一个人在办理业务中,其他人跑过来则会告诉你“现在不行,没到你”
重要属性
Field
说明
默认值
resource
资源名,资源名是限流规则的作用对象
count
限流阈值
grade
限流阈值类型,QPS 模式(1)或并发线程数模式(0)
QPS 模式
limitApp
流控针对的调用来源
default,代表不区分调用来源
strategy
调用关系限流策略:直接、链路、关联
根据资源本身(直接)
controlBehavior
流控效果(直接拒绝/WarmUp/匀速+排队等待),不支持按调用关系限流
直接拒绝
clusterMode
是否集群限流
否
4-2、流控模式 直接(默认)
直接->快速失败:系统默认
配置及说明:表示 1 秒钟内查询 1 次就是 OK,若超过次数 1,就直接-快速失败,报默认错误
测试
访问 testB 成功
思考
直接调用默认报错信息,技术方面 OK,但是否应该有我们自己的后续处理?类似有个 fallback 的兜底方法?
关联 当关联的资源达到阈值时,就限流自己 当与 A 关联的资源 B 达到阀值后,就限流 A 自己设置 testA
当关联资源/testB 的 qps 阀值超过 1 时,就限流/testA 的 Rest 访问地址,当关联资源到阈值后限制配置好的资源名。
测试
访问 testB 成功
postman 里新建多线程集合组:
Run:
20 个线程,每隔 0.3s 访问一次
运行后发现 testA 挂了,点击访问:http://localhost:8401/testA
链路 只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API 级别的针对来源】
4-3、流控效果 快速失败(默认的流控处理) 直接失败,抛出异常:Blocked by Sentinel (flow limiting)
预热 Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP )方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。详细文档可以参考 流量控制 - Warm Up 文档 ,具体的例子可参见WarmUpFlowDemo
通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
WarmUp 配置 默认 coldFactor 为 3,即请求 QPS 从(threshold / 3) 开始,经多少预热时长才逐渐升至设定的 QPS 阈值
系统初始化的阀值为 10 / 3 约等于 3,即阀值刚开始为 3;然后过了 5 秒后阀值才慢慢升高恢复到 10。效果为:开始访问 localhost:/testA 时每秒请求别超过 10/3 个才能正常访问,5 秒后可以接受的请求可以达到每秒 10 次。
应用场景
秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值
排队等待 匀速排队,让请求以均匀的速度通过,阀值类型必须设成 QPS,否则无效
设置含义:/testA 每秒 1 次请求,超过的话就排队等待,等待的超时时间为 20000 毫秒
说明:匀速排队,阈值必须设置为 QPS
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER )方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考流量控制 - 匀速器模式 ,具体的例子可以参见 PaceFlowDemo 。
该方式的作用如下图所示:
5、降级规则 5-1、熔断降级概述 除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用 进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
5-2、降级策略介绍
RT(平均响应时间,秒级)
异常比列(秒级)
QPS >= 5 且异常比例(秒级统计)超过阈值时 ,触发降级;时间窗口结束后,关闭降级
异常数(分钟级)
异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
进一步说明:
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)
Sentinel 的断路器是没有半开状态的。(Sentinei 1.8.0 已有半开状态)
半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用;有异常则继续打开断路器不可用。具体可以参考 Hystrix
5-3、RT 介绍
平均响应时间(DEGRADE_GRADE_RT ):当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值( count ,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的timeWindow ,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出DegradeException )。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900ms,若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt=xxx 来配置。
注意 :Sentinel 1.7.0 才有平均响应时间 (DEGRADE_GRADE_RT
),Sentinel 1.8.0 的没有这项,取而代之的是慢调用比例 (SLOW_REQUEST_RATIO
)。
慢调用比例 (SLOW_REQUEST_RATIO ):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs )内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。熔断降级 · 熔断降级 · alibaba/Sentinel Wiki · GitHub
接下来讲解 Sentinel 1.7.0 的。
实操 在 controller 中加入 textD
1 2 3 4 5 6 7 8 9 10 @GetMapping("/testD") public String testD () { try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } log.info("testD 测试RT" ); return "------testD" ; }
配置:
jmeter 压测:永远一秒钟打进来 10 个线程(大于 5 个了)调用 testD
压测时访问 /testD
停止压测后:`
结论:
按照上述配置,永远一秒钟打进来 10 个线程(大于 5 个了)调用 testD,我们希望 200 毫秒处理完本次任务,如果超过 200 毫秒还没处理完,在未来 1 秒钟的时间窗口内,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了,后续停止 jmeter,没有这么大的访问量了,断路器关闭(保险丝恢复),微服务恢复正常。
5-4、异常比例 介绍
异常比例(DEGRADE_GRADE_EXCEPTION_RATIO ):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值( DegradeRule 中的 count )之后,资源进入降级状态,即在接下的时间窗口( DegradeRule 中的 timeWindow ,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是[0.0, 1.0] ,代表 0% -100%。
注意 ,与 Sentinel 1.8.0 相比,有些不同(Sentinel 1.8.0 才有的半开状态),Sentinel 1.8.0 的如下:
异常比例 (ERROR_RATIO ):当单位统计时长(statIntervalMs )内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0] ,代表 0% - 100%。熔断降级 · alibaba/Sentinel Wiki · GitHub
接下来讲解 Sentinel 1.7.0 的。
实操 在 controller 中加入 textE
1 2 3 4 5 6 @GetMapping("/testE") public String testE () { log.info("testE 测试异常数" ); int a = 10 /0 ; return "------testE" ; }
配置
单独一次访问:http://localhost:8401/testE
jmeter 压测后:
结论
按照上述配置,单独访问一次,必然来一次报错一次(int age = 10/0),调一次错一次;开启 jmeter 后,直接高并发发送请求,多次调用达到我们的配置条件了。
断路器开启(保险丝跳闸),微服务不可用了,不再报错 error 而是服务降级了
5-5、异常数 介绍
异常数( DEGRADE_GRADF_EXCEPTION_COUNT ):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别 的,若timeWindow 小于 60s,则结束熔断状态后可能再进入熔断状态。
说明:假设时间窗口期为 10 秒,设置的异常数不能超过 5,统计阶段第 1 秒的时候有 5 个异常,服务熔断后进入时间窗口期,时间窗口期结束,但是一分钟还没结束,那么此时的异常数还是 5 个,又进入熔断状态。
注意,与 Sentinel 1.8.0 相比,有些不同(Sentinel 1.8.0 才有的半开状态),Sentinel 1.8.0 的如下:
异常数 (ERROR_COUNT ):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
接下来讲解 Sentinel 1.7.0 的。
异常数是按照分钟统计的,时间窗口一定要大于等于 60 秒 。
实战 配置:
测试:
一到五次访问会报错,第六次访问后会服务降级,直到时间窗口期结束,服务才开始报错
6、热点 key 限流 6-1、基本介绍 何为热点:热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的 Top N 数据,并对其访问进行限流或者其它操作:
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。热点参数限流 · alibaba/Sentinel Wiki · GitHub
承上启下复习
兜底方法,分为 系统默认 和 客户自定义 两种。
之前的案例,限流出问题后,都是用 sentinel 系统默认的提示: Blocked by Sentinel (flow limiting)
我们能不能自定?类似 hystrix,某个方法出问题了,就找对应的兜底降级方法?
结论 - 从 HystrixCommand 到@SentinelResource
6-2、实操 在 controller 中加入:
1 2 3 4 5 6 7 8 9 @GetMapping("/testHotKey") @SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey") public String testHotKey (@RequestParam(value = "p1",required = false) String p1, @RequestParam(value = "p2",required = false) String p2) { return "------testHotKty" ; } public String deal_testHotKey (String p1, String p2, BlockException exception) { return "------testHotKty,o(T~~T)o" ; }
说明:
@SentinelResource(value = “testHotKey”)异常打到了前台用户界面看到,不友好
@SentinelResource(value = “testHotKey”,blockHandler = “deal_testHotKey”)方法 testHotKey 里面第一个参数只要 QPS 超过每秒 1 次,马上降级处理,异常用了我们自己定义的兜底方法。
**当新增热点规则时,资源名有斜杠,找的是 GetMapping,没有则找的是 SentinelResource**
配置:
上面的抓图表示第一个参数有值的话,1 秒的 QPS 为 1,超过就限流,限流后调用 deal_testHotKey 支持方法。
测试:
6-3、参数例外项 上述案例演示了第一个参数 p1,当 QPS 超过 1 秒 1 次点击后马上被限流
特例情况:我们期望 p1 参数当它是某个特殊值时,它的限流值和平时不一样
特例:假如当 p1 的值等于 5 时,它的阈值可以达到 200
热点参数的注意点,参数必须是基本类型或者 String
测试:
说明:
@SentinelResource :处理的是 Sentine 控制台配置的违规情况,有 blockHandler 方法配置的兜底处理
@RuntimeException:int age=10/0,这个是 java 运行时报出的运行时异异常 RunTimeException,@SentineResource 不管
7、系统规则 7-1、各项配置参数说明
7-2、@SentinelResource 配置 按资源名称限流+后续处理 pom 文件增加
1 2 3 4 5 <dependency > <groupId > com.atguigu.springcloud</groupId > <artifactId > cloud-api-commons</artifactId > <version > ${project.version}</version > </dependency >
新增 controller
1 2 3 4 5 6 7 8 9 10 11 12 @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 服务不可用" ); } }
配置流控规则
簇点链路:byResource 是资源
表示 1 秒钟内查询次数大于 1,就跑到我们自定义的兜底方法
测试
额外问题
此时关闭服务 8401 看看:Sentinel 控制台,流控规则消失了?????临时/持久?
按照 Url 地址限流+后续处理 通过访问的 URL 来限流,会返回 Sentinel 自带默认的限流处理信息
RateLimitController 新增
1 2 3 4 @GetMapping("/rateLimit/byUrl") public CommonResult byUrl () { return new CommonResult (200 ,"按url限流测试" ,new Payment (2020L ,"serial002" )); }
簇点链路:byUrl 是资源
测试
上面兜底方案面临的问题
客户自定义限流处理逻辑 创建 CustomerBlockHandler 类用于自定义限流处理逻辑
1 2 3 4 5 6 7 8 public class CustomerBlockHandler { public static CommonResult handlerException1 (BlockException exception) { return new CommonResult (4444 ,"按用户自定义,global handlerException.......1" ); } public static CommonResult handlerException2 (BlockException exception) { return new CommonResult (4444 ,"按用户自定义,global handlerException.......2" ); } }
controller 新增
找 CustomerBlockHandler 类里的 handleException2 方法进行兜底处理
1 2 3 4 5 6 7 @GetMapping("/rateLimit/customerBlockHandler") @SentinelResource(value = "customerBlockHandler", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2") public CommonResult customerBlockHandler () { return new CommonResult (200 ,"按用户自定义" ,new Payment (2020L ,"serial003" )); }
测试:
对资源名称流控
对 URL 地址流控
在设置了 URL 地址流控的前提下,设置按资源名称流控无效
URL 地址流控优先级大于资源名称流控
更多注解属性说明 @SentinelResource 注解最主要的两个用法:限流控制和熔断降级的具体使用案例介绍完了。另外,该注解还有一些其他更精细化的配置,比如忽略某些异常的配置、默认降级函数等等,具体可见如下说明:
value:资源名称,必需项(不能为空)
entryType:entry 类型,可选项(默认为 EntryType.OUT)
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 逻辑中,而是会原样抛出。
7-3、服务熔断功能 sentinel 整合 ribbon+openFeign+fallback
Ribbon 系列——提供者 9003/9004 启动 nacos 和 sentinel,新建 cloudalibaba-provider-payment9003/9004 两个一样的做法
pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 <?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 > springCloud2023</artifactId > <groupId > com.jcvv.springcloud</groupId > <version > 1.0-SNAPSHOT</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > cloudalibaba-provider-payment9003</artifactId > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-nacos-discovery</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 > 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 > <dependency > <groupId > com.jcvv.springcloud</groupId > <artifactId > cloud-api-common</artifactId > <version > ${project.version}</version > </dependency > </dependencies > </project >
applition.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 server: port: 9003 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: 119.3 .211 .104 :8089 management: endpoints: web: exposure: include: '*'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 server: port: 9004 spring: application: name: nacos-payment-provider cloud: nacos: discovery: server-addr: 119.3 .211 .104 :8089 management: endpoints: web: exposure: include: '*'
主启动类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.jcvv.springcloud;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication @EnableDiscoveryClient public class PaymentMain9003 { public static void main (String[] args) { SpringApplication.run(PaymentMain9003.class, args); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.jcvv.springcloud;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication @EnableDiscoveryClient public class PaymentMain9004 { public static void main (String[] args) { SpringApplication.run(PaymentMain9004.class, args); } }
controller 层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @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 , "xcxcxcxcxcxcxcxcxcxcxcxcxc11111111" )); hashMap.put(2L , new Payment (2L , "xcxcxcxcggggggggg2222222222222222" )); hashMap.put(3L , new Payment (3L , "xcxcxcxccxxcxcfafdgdgdsgdsgds33333" )); } @GetMapping("/payment/get/{id}") public CommonResult paymentSql (@PathVariable("id") Long id) { Payment payment = hashMap.get(id); CommonResult result = new CommonResult (200 , "from mysql, server port : " + serverPort + " ,查询成功" , payment); return result; } }
Ribbon 系列——消费者 84 新建 cloudalibaba-consumer-nacos-order84
pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 <?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 > springCloud2023</artifactId > <groupId > com.jcvv.springcloud</groupId > <version > 1.0-SNAPSHOT</version > </parent > <modelVersion > 4.0.0</modelVersion > <artifactId > cloudalibaba-consumer-nacos-order84</artifactId > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-datasource-nacos</artifactId > </dependency > <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-transport-simple-http</artifactId > </dependency > <dependency > <groupId > com.alibaba.cloud</groupId > <artifactId > spring-cloud-starter-alibaba-sentinel</artifactId > </dependency > <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 > 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 > <dependency > <groupId > com.jcvv.springcloud</groupId > <artifactId > cloud-api-common</artifactId > <version > ${project.version}</version > </dependency > </dependencies > </project >
application.yml 如果 8080 端口被占用,sentinel 的端口改成了 8888
1 2 3 java -jar sentinel.jar --server.port=8888 sentinel.transport.dashboard: 192.168 .1 .159 :8888
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 server: port: 84 spring: cloud: nacos: discovery: server-addr: 119.3 .211 .104 :8089 sentinel: transport: dashboard: 192.168 .1 .159 :8080 port: 8719 client-ip: 192.168 .1 .159 eager: true application: name: nacos-order-consumer
主启动类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.jcvv.springcloud;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication @EnableDiscoveryClient public class OrderMain84 { public static void main (String[] args) { SpringApplication.run(OrderMain84.class, args); } }
配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.jcvv.springcloud.config;import org.springframework.cloud.client.loadbalancer.LoadBalanced;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.client.RestTemplate;@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate () { return new RestTemplate (); } }
controller 层 fallback 管运行异常、blockHandler 管配置违规
① 无配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.jcvv.springcloud.controller;import com.alibaba.csp.sentinel.annotation.SentinelResource;import com.jcvv.springcloud.entities.CommonResult;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;import javax.annotation.Resource;@RestController @Slf4j public class CircleBreakerController { private static final String PAYMENT_URL="http://nacos-payment-provider" ; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback") public CommonResult fallback (@PathVariable("id") Long id) { if (id == 4 ){ throw new IllegalArgumentException ("IllegalArgumentException...非法参数异常" ); }else if (id > 4 ){ throw new NullPointerException ("NullPointerException...空指针异常" ); } return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class); } }
测试:
② 只配置 fallback
③ 只配置 blockHandler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.jcvv.springcloud.controller;import com.alibaba.csp.sentinel.annotation.SentinelResource;import com.alibaba.csp.sentinel.slots.block.BlockException;import com.jcvv.springcloud.entities.CommonResult;import com.jcvv.springcloud.entities.Payment;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;import javax.annotation.Resource;@RestController @Slf4j public class CircleBreakerController { private static final String PAYMENT_URL = "http://nacos-payment-provider" ; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback", blockHandler = "blockHandler") public CommonResult fallback (@PathVariable("id") Long id) { if (id == 4 ) { throw new IllegalArgumentException ("IllegalArgumentException...非法参数异常" ); } else if (id > 4 ) { throw new NullPointerException ("NullPointerException...空指针异常" ); } return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class); } public CommonResult handleFallback (@PathVariable("id") Long id, Throwable e) { Payment payment = new Payment (id, null ); return new CommonResult (444 , "---兜底异常handlerFallback,exception内容:" + e.getMessage(), payment); } public CommonResult blockHandler (@PathVariable("id") Long id, BlockException e) { Payment payment = new Payment (id, "null" ); return new CommonResult (445 , "blockHandler-sentinel限流,blockException:" + e.getMessage(), payment); } }
本例 sentinel 需配置:
簇点链路:
服务降级:
异常超过 2 次后,断路器打开
测试
第一次和第二次访问还是报异常:
第三次访问
时间窗口期 10s 过了之后:又恢复异常
④fallback 和 blockHandler 都配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.jcvv.springcloud.controller;import com.alibaba.csp.sentinel.annotation.SentinelResource;import com.alibaba.csp.sentinel.slots.block.BlockException;import com.jcvv.springcloud.entities.CommonResult;import com.jcvv.springcloud.entities.Payment;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;import javax.annotation.Resource;@RestController @Slf4j public class CircleBreakerController { private static final String PAYMENT_URL="http://nacos-payment-provider" ; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback",blockHandler = "blockHandler",fallback = "handleFallback") public CommonResult fallback (@PathVariable("id") Long id) { if (id == 4 ){ throw new IllegalArgumentException ("IllegalArgumentException...非法参数异常" ); }else if (id > 4 ){ throw new NullPointerException ("NullPointerException...空指针异常" ); } return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class); } public CommonResult handleFallback (@PathVariable("id") Long id, Throwable e) { Payment payment = new Payment (id, null ); return new CommonResult (444 , "---兜底异常handlerFallback,exception内容:" +e.getMessage(),payment); } public CommonResult blockHandler (@PathVariable("id") Long id, BlockException e) { Payment payment = new Payment (id, "null" ); return new CommonResult (445 , "blockHandler-sentinel限流,blockException:" +e.getMessage(),payment); } }
本例 sentinel 需配置:
簇点链路:
服务降级:
异常超过 2 次后,断路器打开
测试
第一次和第二次访问:
第三次访问
结论:
若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑
忽略异常 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.jcvv.springcloud.controller;import com.alibaba.csp.sentinel.annotation.SentinelResource;import com.alibaba.csp.sentinel.slots.block.BlockException;import com.jcvv.springcloud.entities.CommonResult;import com.jcvv.springcloud.entities.Payment;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;import javax.annotation.Resource;@RestController @Slf4j public class CircleBreakerController { private static final String PAYMENT_URL = "http://nacos-payment-provider" ; @Resource private RestTemplate restTemplate; @GetMapping("/consumer/fallback/{id}") @SentinelResource(value = "fallback", fallback = "handleFallback", blockHandler = "blockHandler", exceptionsToIgnore = {IllegalArgumentException.class}) public CommonResult fallback (@PathVariable("id") Long id) { if (id == 4 ) { throw new IllegalArgumentException ("IllegalArgumentException...非法参数异常" ); } else if (id > 4 ) { throw new NullPointerException ("NullPointerException...空指针异常" ); } return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class); } public CommonResult handleFallback (@PathVariable("id") Long id, Throwable e) { Payment payment = new Payment (id, null ); return new CommonResult (444 , "---兜底异常handlerFallback,exception内容:" + e.getMessage(), payment); } public CommonResult blockHandler (@PathVariable("id") Long id, BlockException e) { Payment payment = new Payment (id, "null" ); return new CommonResult (445 , "blockHandler-sentinel限流,blockException:" + e.getMessage(), payment); } }
测试:
7-4、全局降级 上面是单个进行 fallback 和 blockhandler 的测试,下面是整合 openfeign 实现把降级方法解耦。和 Hystrix 几乎一摸一样!
添加 open-feign 依赖:
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
yml 追加如下配置
1 2 3 4 feign: sentinel: enabled: true
主启动类添加注解 : @EnableFeignClients 激活 open-feign
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.jcvv.springcloud;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class OrderMain84 { public static void main (String[] args) { SpringApplication.run(OrderMain84.class, args); } }
service:
1 2 3 4 5 6 @FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class) public interface PaymentService { @GetMapping(value = "/payment/get/{id}") public CommonResult paymentSql (@PathVariable("id") Long id) ; }
1 2 3 4 5 6 7 @Component public class PaymentFallbackService implements PaymentService { @Override public CommonResult paymentSql (Long id) { return new CommonResult (44444 ,"服务降级返回,---PaymentFallbackService" ); } }
controllert 添加:
1 2 3 4 5 6 7 @Resource private PaymentService paymentService;@GetMapping(value = "/consumer/paymentSQL/{id}") public CommonResult paymentSQL (@PathVariable Long id) { return paymentService.paymentSql(id); }
测试:
为了模拟异常,在远程调用方添加延时(open-feign 默认延时大于 1 秒才会回调),如下。
测试时发现如果只给一个微服务加延时,另一个不加,会一直调用没有延时的微服务,所以要么两个都加延时或者停掉一个
1 2 3 4 5 6 7 @GetMapping("/payment/get/{id}") public CommonResult paymentSql (@PathVariable("id") Long id) throws InterruptedException { Payment payment = hashMap.get(id); CommonResult result = new CommonResult (200 , "from mysql, server port : " + serverPort + " ,查询成功" , payment); TimeUnit.SECONDS.sleep(6 ); return result; }
7-5、持久化 拿 cloudalibaba-sentinel-service8401 项目操作举例子
目前的 sentinel 当重启以后,数据都会丢失,和 nacos 类似原理。需要持久化。它可以被持久化到 nacos 的数据库中。
pom 文件
1 2 3 4 <dependency > <groupId > com.alibaba.csp</groupId > <artifactId > sentinel-datasource-nacos</artifactId > </dependency >
application.yml
1 2 3 4 5 6 7 8 9 10 11 spring: cloud: sentinel: datasource: ds1: nacos: server-addr: localhost:8848 dataId: ${spring.application.name} group: DEFAULT_GROUP data-type: json rule-type: flow
去 nacos 上创建一个 dataid ,名字和 yml 配置的一致,json 格式,内容如下:
1 2 3 4 5 6 7 8 9 10 11 [ { "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 表示排队等待;
cIusterM0de 是否集群
启动应用,发现存在 关于资源 “/rateLimit/byUrl” 的流控规则.
总结: 就是在 sentinel 启动的时候,去 nacos 上读取相关规则配置信息,实际上它规则的持久化,就是第三步,粘贴到 nacos 上保存下来,就算以后在 sentinel 上面修改了,重启应用以后也是无效。
相关链接:Seata解决分布式事务问题