Sentinel:分布式系统的流量防卫兵;随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
官网下载地址:https://github.com/alibaba/Sentinel/releases
前提:java8 环境 + 8080端口不能被占用【默认启动端口为8080】
运行命令
java -jar sentinel-dashboard-1.8.0.jar
http://localhost:8080 登录账号与密码均为:sentinel
导入 pom 依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
创建 application.yml
server:
port: 9002
spring:
application:
name: seata-order-service
cloud:
nacos:
discovery:
server-addr: 192.168.0.184:8848
namespace: 6ab3cf71-816c-4857-aa23-fdafd7ba1662
sentinel:
transport:
# 配置 Sentinel dashboard 地址
dashboard: localhost:8080
# 默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
主启动
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class OrderMain9002 {
public static void main(String[] args) {
SpringApplication.run(OrderMain9002.class, args);
}
}
业务类
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA(){
return "--------testA";
}
@GetMapping("/testB")
public String testB(){
return "---------testB";
}
}
启动 Sentinal8080,启动Nacos,启动 演示项目,登录 nacos 控制台即可看到 演示项目已经成功启动:
登录 sentinal 控制台,发现还是空空如也,这是因为 sentinal 采用的是懒加载机制,需要我们访问 演示项目后才会监控服务
多次访问 http://localhost:9002/testA 与 http://localhost:9002/testB 即可看到 sentinal 管理控制台已经显示实时监控,其中 seata-order-service 是服务名,testA 与 testB 是访问的 演示项目 监控,绿色表示成功的访问
官网:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6
流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
流量控制主要有两种统计类型,一种是统计并发线程数,另外一种则是统计 QPS。类型由
FlowRule
的grade
字段来定义。其中,0 代表根据并发数量来限流,1 代表根据 QPS 来进行流量控制。其中线程数、QPS 值,都是由StatisticSlot
实时统计获取的。
一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:
resource
:资源名,即限流规则的作用对象count
: 限流阈值grade
: 限流阈值类型(QPS 或并发线程数)limitApp
: 流控针对的调用来源,若为 default
则不区分调用来源strategy
: 调用关系限流策略controlBehavior
: 流量控制效果(直接拒绝、Warm Up、匀速排队)
- 资源名:唯一名称,默认为请求路径
- 针对来源:Sentinel 可以针对调用者进行限流,填写微服务名,默认 default(不区分)
- 阈值类型/单机阈值:
- QPS:(每秒钟的请求数量):当调用该 api 的 QPS 达到阈值的时候,进行限流
- 线程数:当调用该 api 的线程数达到阈值的时候,进行限流
- 是否集群:【当前演示项目不集群】
- 流控规则:
- 直接:api 达到限流条件时,直接限流
- 关联:当关联的资源达到阈值时,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,进行限流)【api 级别针对来源】
- 流控效果:
- 快速失败:直接失败,抛异常
- Warm Up:根据 coldFactor(冷加载因子,默认3)的值,从阈值/coldFactor,经过预热时长,才达到设置的 QPS 阈值
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为 QPS,否则无效
新增流控规则
该规则的意思是:seata-order-service 服务下的 /testA 请求 每秒钟只能有两次,超过这个次数就会触发限流。
测试:访问 http://localhost:9002/testA 可以发现,在一秒钟内,一次或两次访问都不会有问题,一旦超过两次,就会触发限流,如下图:
扩展思考:
直接调用默认报错信息,技术方面 OK 但是是否应该有我们自己的后续处理?类似于 fallback 的兜底方法,毕竟直接给用户返回一个这样的异常页面是很不友好的。自定义限流处理逻辑,这些将在后面介绍。
修改流控规则
阈值类型修改为线程数后,发现无论我们怎么点击刷新 http://localhost:9002/testA 都不会进入限流,这是因为 QPS 与 线程数的 处理方式是不一样的,如下图:
如图所示:QPS 的处理是直接拦截请求,当一秒钟以内的请求超过阈值时,就会今天限流,而线程数的处理是,直接将请求放行不做拦截,但是只提供一个线程处理请求,当一个线程处理不过来的时候,就会限流;我们上面疯狂刷新页面都没有进入限流,是因为线程处理请求的所需实际很短,远远小于我们刷新的操作的时间,我们可以通过增加请求处理所需的时间,来测试限流效果,改动如下
【由于没有配置持久化,所有修改启动后需要重新配置流控规则,这个一定要记住,之前的流控规则由于没做持久化都没了,持久化后续会介绍。】
:
@GetMapping("/testA")
public String testA(){
try {
TimeUnit.MILLISECONDS.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "--------testA";
}
增加一个延时模拟请求的处理时间,由于延时是0.8 秒,所有我们一秒一个请求时可以处理的,我们可以开两个页面一起请求,这样一个线程就处理不过来了,就会出现如下的限流效果:
当关联的资源达到阈值时,就限流自己;可以理解为:别人惹事,自己背锅。
当与 A 关联的资源 B 达到阈值后,就限流 A 自己;实际应用中,比如:支付接口达到阈值后,就限流下订单的接口。
当关联的资源 /testB 的 QPS 阈值超过 1 时,就限流 /testA 的 Rest 访问地址,
当关联资源到阈值后限制配置好的资源名
我们用 postman 模拟并发密集访问 testB,启动 20 个线程,每隔 0.3 秒访问 testB 一次
这时 访问 http://localhost:9002/testA ,结果如下图:在 postman 运行的期间,访问 testA 都会触发限流,postman 请求结束后,会恢复正常访问。
Warm Up(
RuleConstant.CONTROL_BEHAVIOR_WARM_UP
)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。详细文档可以参考 流量控制 - Warm Up 文档,具体的例子可以参见 WarmUpFlowDemo。公式:阈值除以 coldFactor(默认值为3),经过预热时长后才会达到阈值。默认coldFactor为3,即请求从 threshold / 3开始,经预热时长逐渐升至设定的 QPS 阈值。
通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
新建流控规则
上图:阈值为 10 + 预热时长为 5 秒。系统初始化的阈值为 10/3 约等于3,即初始时一秒钟能接受 3 个请求;然后通过 5 秒后,阈值才能慢慢身高恢复到 10,这时能够一秒处理 10个请求。
疯狂刷新请求 http://localhost:9002/testB 发现开始时还会出现 限流的情况,慢慢的越来越少;5秒后基本上不会出现限流的情况了【手速超过一秒10次的不算】;当一段时间不请求后,在去重新请求,这时候又是从 一秒3次开始算起,5秒后达到10次;
总结:这种规则主要是为了处理突然出现的大流量请求;比如秒杀系统开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是为了保护系统,可以慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
匀速排队(
RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考 流量控制 - 匀速器模式,具体的例子可以参见 PaceFlowDemo。即:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为 QPS,否则无效
该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
新建流控规则
上图:/testB 每秒1次请求,超过的话就排队等待,等待的超时时间为 20000 ms 即 20 s;
为了方便看到处理过程,我们需要修改 testB,
记住重新添加流控规则
@GetMapping("/testB")
public String testB(){
log.info(Thread.currentThread().getName()+"\t"+"......testB");
return "---------testB";
}
我们可以通过 postman 来模拟请求,
通过控制台打印的日志,我们可以看到无论我们一次性发请多少个请求,每秒都只会按照我们设置的一秒处理一个,超过设置的超时时间还没被处理的请求会直接被限流。
官网:https://github.com/alibaba/Sentinel/wiki/%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
最大RT
的请求统计为 慢调用
最小请求数
,并且慢调用
比例超过 比例阈值
,则接下来的时间内请求会自动被熔断。熔断时长
后熔断器会进入探测恢复状态,若接下来的一个请求响应时间小于 最大RT
则结束熔断,大于设置的 最大RT
则会再次被熔断。最小请求数
,并且异常的比例大于 比例阈值
,则接下来的 熔断时长
内请求会自动被熔断。熔断时长
后熔断器会进入探测恢复状态,若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。最小请求数
,并且 异常数
超过阈值之后会自动进行熔断。熔断时长
后熔断器会进入探测恢复状态,若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。注意异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(
BlockException
)不生效。
熔断降级规则(DegradeRule)包含下面几个重要的属性:
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,即规则的作用对象 | |
grade | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 |
count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | |
timeWindow | 熔断时长,单位为 s | |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 |
statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) |
- 请求的响应时间大于
最大RT
的请求统计为慢调用
- 当单位统计时长(默认1秒)内请求数目大于设置的
最小请求数
,并且慢调用
比例超过比例阈值
,则接下来的时间内请求会自动被熔断。- 经过
熔断时长
后熔断器会进入探测恢复状态,若接下来的一个请求响应时间小于最大RT
则结束熔断,大于设置的最大RT
则会再次被熔断。
修改 testA
@GetMapping("/testA/{id}")
public String testA(@PathVariable("id") Integer id){
if(id <= 0){
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("......testA");
return "--------testA";
}
新增降级规则
该规则表示:若请求时间超过500毫秒则为
慢调用
,若一秒内1请求超过 5 次,且出现慢调用
的比例超过 50%,则发生熔断时长为 5s 的熔断。即:一秒内请求超过 5 次,且 50% 的请求超过 500 毫秒,则触发熔断并熔断 5 秒。
测试
为了方便测试,我们可以使用
jmeter
来做压力测试;首先建立一个线程组,线程组每秒启动三个线程发请求,每个线程中包含三个请求,其中 testA-F 表示会超时的请求,即:http://localhost:9002/testA/-1;testA-S与 testA-S2 表示正常访问的请求,即:http://localhost:9002/testA/1;
jmeter
改为中文版的方法:找到 jmeter下的bin目录,打开jmeter.properties 文件,找到language
去掉前面的 # 号,将其改为 language=zh_CN
启动测试:这时一秒内总共有 9 个请求,其中3个超时,6个成功,这时我们可以通过浏览器访问:http://localhost:9002/testA/1 发现可以正常访问;这是因为
慢调用
的比例仅为 1/3 远远没达到 50%。这时我们删除掉 testA-S2 再次启动,浏览器请求:http://localhost:9002/testA/1 可以发现已经发生了服务熔断,这时我们关闭
jmeter
,再请求,大概过了几秒后,会发现服务又恢复了。
还有一种情况是 请求全部超时,但是一秒内请求数小于
最小请求数
,这时是不会触发熔断的,感兴趣的可以自行修改参数试下,这里就不做介绍了。
- 单位统计时长内请求数目大于设置的
最小请求数
,并且异常的比例大于比例阈值
,则接下来的熔断时长
内请求会自动被熔断。- 经过
熔断时长
后熔断器会进入探测恢复状态,若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
修改 testB
@GetMapping("/testB/{id}")
public String testB(@PathVariable("id") Integer id){
if(id <= 0){
int i = 5 / 0;
}
log.info(Thread.currentThread().getName()+"\t"+"......testB");
return "---------testB";
}
新增降级规则
该规则表示:在 1 秒内,请求超过 5 次,且异常比例大于 50% 时,触发熔断,熔断时长为5 秒。
测试
同样使用
jmeter
进行测试,配置如下:与上面的同理
启动后,浏览器调用 http://localhost:9002/testB/1 ,可以发现能够正常调用,不会触发降级;同理可以删除
testB-S2
,之后再重试 ,发现出现了服务降级,关闭jmeter
几秒后又会恢复正常调用。
- 当单位统计时长内的请求数目大于设置的
最小请求数
,并且异常数
超过阈值之后会自动进行熔断。- 经过
熔断时长
后熔断器会进入探测恢复状态,若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
新建降级规则
该规则表示:在 1 秒内,请求超过 4 次,且其中异常数超过 2 次时,触发熔断,熔断时长为5 秒。
测试
直接在浏览器输入:http://localhost:9002/testB/-11 刷新测试,可以看到:当 一秒内 请求超过 4个 且异常数大于 2个 时,进入熔断,这时候调用 http://localhost:9002/testB/11 也将返回熔断的提示,继续调用,大概在 5秒 左右会恢复正常;当然,如果在熔断期间调用 http://localhost:9002/testB/-11,则会继续进入熔断状态。
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
/**
* 热点限流
* @param p1
* @param p2
* @return
*/
@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 "--------testHotKey";
}
public String deal_testHotKey(String p1, String p2, BlockException exception){
return "---------deal_testHotKey,/(ㄒoㄒ)/~~";
}
兜底方法分为 系统默认 和客户自定义,两种,之前的案例中,限流或熔断后,都是用的 sentinel 系统默认的提示:Blocked by Sentinel (flow limiting);
现在我们可以通过 @SentinelResource 注解 自定义兜底的处理方法,@SentinelResource 与 @HystrixCommand 类似;
blockHandler
是用来指定 当违背了 sentinel 控制台设置的规则【限流熔断、热点。。。等】后的 处理方法;
新增热点规则
该规则表示:当访问资源名为 testHotKey 的资源时,参数中包含 索引为0的参数【即:p1】,且在一秒内 访问了 2次以上时,触发限流;也就是说在访问 http://localhost:9002/testHotKey?p1=5&p2=b 或 http://localhost:9002/testHotKey?p1=5 包含
p1
参数的请求时,一秒超过两个就会进入限流;而 http://localhost:9002/testHotKey 与 http://localhost:9002/testHotKey?p2=b 这种不包含p1
参数的请求则不受影响;当进入限流时,会调用blockHandler
指定的方法【即:deal_testHotKey】,返回响应结果,如图:
注意:
- 【testHotKey 表示 @SentinelResource
value
值为testHotKey
的资源,而 /testHotKey 表示,请求路径为/testHotKey
的资源,两者注意不要混淆。】blockHandler
用来指定 违背规则时的 处理方法,当不指定时【即:@SentinelResource(value = “testHotKey”)】,会直接返回异常页面,如下:
这对于用户来说,是很不友好的,所以尽量自己指定
blockHandler
来处理响应;
特别注意:blockHandler 只管sentinel控制台配置规则的违规情况,业务逻辑产生的异常不归它管,至于业务逻辑异常的处理方法,后续会介绍。
上述案例演示了第一个参数 p1 ,当 QPS 超过 1 秒钟 2 次点击后马上被限流,这是只普通情况下;
特例情况:我们期望 p1 参数当它是某个特殊值时,它的限流值和平时不一样;如:当 p1 的值等于 5 时,他的阈值可以达到 200;
配置:修改 testHotKey 热点规则
由于 p1 参数时 String 类型,这里选择 String ,当参数值为 5 时,限流阈值为 200,选择好后点击添加,结果如下图:接下来点击保存即可 【参数例外项可以添加多个】。
这时 分别调用 http://localhost:9002/testHotKey?p1=2 和 http://localhost:9002/testHotKey?p1=5 疯狂刷新,即可看到 当一秒内请求次数达到 3次时,
p1=2
的请求会进入限流,而p1=5
的请求无论怎么刷新都不会进入限流。
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(
EntryType.IN
),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式:
maxQps * minRt
估算得出。设定参考值一般是 CPU cores * 2.5
。该规则表示:该该机器上的入口流量的 QPS 达到阈值,即 1秒超过 1次访问时 触发限流;无论是那个接口url 的请求都一样;
在开发中我们基本上不会使用到 系统规则,万一要使用到也要万分小心,这种规则是适用于单机全局的,稍不注意就会造成难以挽回的严重后果。
官网:https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81
注意:
注解方式埋点不支持 private 方法
。
@SentinelResource
用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource
注解包含以下属性:
value
:资源名称,必需项(不能为空)entryType
:entry 类型,可选项(默认为 EntryType.OUT
)blockHandler
/ blockHandlerClass
: blockHandler
对应处理 BlockException
的函数名称,可选项。blockHandler 函数访问范围需要是 public
。blockHandler 函数签名和位置要求:
BlockException
blockHandlerClass
为对应的类的 Class
对象,注意对应的函数必需为 static 函数,否则无法解析。fallback
/ fallbackClass
:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore
里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
Throwable
类型的参数用于接收对应的异常。fallbackClass
为对应的类的 Class
对象,注意对应的函数必需为 static 函数,否则无法解析。defaultFallback
(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore
里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
Throwable
类型的参数用于接收对应的异常。fallbackClass
为对应的类的 Class
对象,注意对应的函数必需为 static 函数,否则无法解析。exceptionsToIgnore
(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。注:1.6.0 之前的版本 fallback 函数只针对降级异常(
DegradeException
)进行处理,不能针对业务异常进行处理。
特别注意:
BlockException
时只会进入 blockHandler
处理逻辑。blockHandler
、fallback
和 defaultFallback
,则被限流降级时会将 BlockException
直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException
)。新增业务处理
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult byResource() {
return new CommonResult(200, "按资源名称限流测试", new Payment(2020L, "serial0001"));
}
public CommonResult handleException(BlockException exception){
return new CommonResult(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
}
测试:访问:http://localhost:9002/byResource ,访问正常如图:
新增流控规则–按资源名称
这时,一秒钟点击2下,OK;疯狂点击超过1秒2次,则返回自己定义的限流处理信息,限流发生;
通过访问的 URL 来限流,会返回 Sentinel 自带默认的限流处理信息
删除掉上面的
byResource
按资源名称
的限流规则,新增按url地址
的限流规则
这时,当限流发生时,会返回 Sentinel 自带默认的限流处理信息,如下图:
总结:如果按
资源名称
限流,可以设置自定义的限流处理信息,由blockHandler
指定;而如果按URL路径
限流,则会返回 Sentinel 自带默认的限流处理信息;
以上的 @SentinelResource 的
blockHandler
属性虽然实现了自定义限流处理逻辑,但是我们自定义的处理方法又和业务代码耦合在一块,不直观;每个业务方法都要添加一个兜底的,会导致代码膨胀;而且全局统一的处理方法没有体现。为了解决这些问题:我们可以将 兜底处理方法抽离出来,以下将介绍具体操作。
抽取兜底方法
public class CustomerBlockHandler {
public static CommonResult handlerExceptionOne(BlockException exception){
return new CommonResult(4444, "按用户自定义,global handlerException----------One");
}
public static CommonResult handlerExceptionTwo(BlockException exception){
return new CommonResult(5555, "按用户自定义,global handlerException----------Two");
}
}
方法必须是 static 的,返回值类型必须与原方法返回值一致,并且最后加一个额外的参数,类型为
BlockException
新增业务处理
/**
* 自定义限流处理逻辑
* @return
*/
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerExceptionOne")
public CommonResult customerBlockHandler(){
return new CommonResult(200, "按用户自定义", new Payment(2020L, "serial0035"));
}
blockHandlerClass:表示兜底方法所在的类,blockHandler:表示具体的兜底方法;修改完成后,启动服务并访问:http://localhost:9002/rateLimit/customerBlockHandler 结果正常返回
新增限流规则
添加限流规则后,可以看到一旦我们的请求频率超过 1秒2次 就会进入我们自定义的限流;如:这次自定义的的 CustomerBlockHandler 类的 handlerExceptionOne 方法
新增业务逻辑
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L, new Payment(1L, "link-bd6fb5d9fc03459e885dfc2f4f74c239"));
hashMap.put(2L, new Payment(2L, "link-a588ec6a216f43baa573feb64e0d0dc4"));
hashMap.put(3L, new Payment(3L, "link-2f9342d7eb8940269bd9e2260cff227d"));
}
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback")
public CommonResult<Payment> fallback(@PathVariable("id") Long id){
Payment payment = hashMap.get(id);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常。。。。。");
}else if(null == payment){
throw new NullPointerException("NullPointerException, 该ID没有对应记录,空指针异常..........");
}
CommonResult<Payment> result = new CommonResult<>(200, "success", payment);
return result;
}
如上:这里我们只设置了资源名称,没有新增其它配置;为了方便快速的演示,我们直接用 模拟了三条数据,并且模拟了两 java 异常;这时访问 前三条数据能正常返回数据,从第四条数据开始将抛出异常,并且由于没有配置后续处理方法,会直接抛出异常页面,如下:http://localhost:9002/consumer/fallback/5
fallback 管运行时异常;用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了
exceptionsToIgnore
里面排除掉的异常类型)进行处理
新增 fallback 封装类 CustomFallBack,方法必须为 static 的
public class CustomFallBack {
public static CommonResult handleerFallback(@PathVariable Long id, Throwable e){
Payment payment = new Payment(id, "null");
return new CommonResult(444, "兜底异常handlerFallback,exception内容 "+e.getMessage(), payment);
}
}
修改业务类,fallbackClass
指定兜底类,fallback
指定兜底方法
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallbackClass = CustomFallBack.class, fallback = "handleerFallback")
public CommonResult<Payment> fallback(@PathVariable("id") Long id){
Payment payment = hashMap.get(id);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常。。。。。");
}else if(null == payment){
throw new NullPointerException("NullPointerException, 该ID没有对应记录,空指针异常..........");
}
CommonResult<Payment> result = new CommonResult<>(200, "success", payment);
return result;
}
重启服务,调用 http://localhost:9002/consumer/fallback/1 发现正常的服务调用会正常返回;调用 http://localhost:9002/consumer/fallback/4 或 http://localhost:9002/consumer/fallback/5 可以发现以前的异常页面已经消失不见了,替换成了我们自己定义的异常处理回调;如下图:
blockHandler
管配置违规;也就是说blockHandler
只负责处理在 Sentinel 控制台 配置的规则的违规时的处理;
新增 blockHandler 封装类 CustomerBlockHandler,方法必须为 static 的
public class CustomerBlockHandler {
public static CommonResult handlerExceptionOne(BlockException exception){
return new CommonResult(4444, "按用户自定义违规处理,global handlerException----------One");
}
public static CommonResult handlerExceptionTwo(Long id, BlockException exception){
return new CommonResult(5555, "按用户自定义违规处理,global handlerException----------Two");
}
}
修改 fallback 方法
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerExceptionTwo")
public CommonResult<Payment> fallback(@PathVariable("id") Long id){
Payment payment = hashMap.get(id);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常。。。。。");
}else if(null == payment){
throw new NullPointerException("NullPointerException, 该ID没有对应记录,空指针异常..........");
}
CommonResult<Payment> result = new CommonResult<>(200, "success", payment);
return result;
}
新增降级规则
该规则表示:当1秒内,请求超过3个且异常数达到2个时,服务发生熔断且熔断时间为5秒;
这时候请求 http://localhost:9002/consumer/fallback/3 能正常访问,当请求 http://localhost:9002/consumer/fallback/4 时,由于没有配置 fallback 来处理运行时异常,所以直接显示异常页面,如下:
当一秒内请求达到3次且异常数达到阈值时,触发降级并执行我们自定义的兜底方法;如下图:
修改 fallback 方法,重启服务
@GetMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",
blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerExceptionTwo",
fallbackClass = CustomFallBack.class, fallback = "handleerFallback")
public CommonResult<Payment> fallback(@PathVariable("id") Long id){
Payment payment = hashMap.get(id);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常。。。。。");
}else if(null == payment){
throw new NullPointerException("NullPointerException, 该ID没有对应记录,空指针异常..........");
}
CommonResult<Payment> result = new CommonResult<>(200, "success", payment);
return result;
}
配置降级规则
这时可以看到这样一种情况:一秒一次调用 http://localhost:9002/consumer/fallback/4 产生异常,会回调 fallback 的兜底方法返回响应结果;当一秒超过三次调用 http://localhost:9002/consumer/fallback/4 ,这时违背了控制台配置的降级规则,触发了 blockHandler 的回调方法,调用 blockHandler 的兜底方法返回响应结果,如下图:
总结:若 blockHandler 和 fallback 都进行了配置,则违背控制台配置规则而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若没有违背控制台规则,并且在服务调用的过程中产生了运行时异常时,则会调用 fallback 处理逻辑。
用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
修改业务方法
添加 exceptionsToIgnore;这时 IllegalArgumentException 异常将不再有兜底方法,产生该异常时,将响应默认的异常页面,如下:
现在我们面临一个问题:一旦我们重启应用,Sentinel 规则将会消失,生产环境需要将配置规则进行持久化。Nacos 是阿里中间件团队开源的服务发现和动态配置中心。Sentinel 针对 Nacos 作了适配,底层可以采用 Nacos 作为规则配置数据源。使用时只需添加以下依赖:【如果已经添加了的不必重复添加】
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
将配置规则持久化进 Nacos 保存,只要刷新 9002【演示项目】某个 rest 地址,sentinel 控制台就能看到规则,只要 Nacos 里面的配置不删除,针对 9002 上 sentinel 上的配置规则持续有效。
修改 application.yml,添加 sentinel 持久化数据源配置
Nacos 中添加流控配置文件,其中
service-addr
: Nacos 服务的地址,dataId
:配置文件的 Data ID,groupId
:配置文件的分组,namespace
:Nacos 命名空间的ID,data-type
:配置文件类型
配置内容默认是数组格式,如果要配置多条,可以用 “,” 号隔开各个 “{}”;
以上规则表示:资源名为 fallback 的资源,在 1秒内 访问两次以上时,会触发限流 快速失败 并调用 blockHandler 的回调方法。
流控配置文件各配置项详解:
- resource:资源名称;
- limitApp:来源应用;
- grade:阈值类型,0表示线程数、1表示QPS;
- count:单机阈值;
- strategy:流控模式,0表示直接,1表示关联,2表示链路;
- controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待
- clusterMode:是否集群。
这时,启动应用程序,浏览器访问:http://localhost:9002/consumer/fallback/1 【由于 Sentinel 是懒加载的,所以必须访问一次。】,然后打开 Sentilen 控制台,点击 流控规则 ,即可在流控规则列表看到我们在 Nacos 中配置的流控规则,并且其配置自动生效。
更多请参考官网:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
使用 Sentinel 与 Ribbon 和 OpenFeign 整合,实现声明式微服务调用、负载均衡和服务的降级熔断等;
为了测试 负载均衡 和 声明式微服务调用 我们需要新建两个 服务提供者模块 ribbon-feign-provider9005 和 ribbon-feign-provider9006,两者提供的服务相同;
导入依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
创建application.yml
server:
port: 9006
spring:
application:
name: ribbon-feign-service
cloud:
nacos:
discovery:
server-addr: 192.168.0.184:8848
namespace: 6ab3cf71-816c-4857-aa23-fdafd7ba1662
management:
endpoints:
web:
exposure:
include: '*'
启动类
@EnableDiscoveryClient
@SpringBootApplication
public class RibboFeignMain9005 {
public static void main(String[] args) {
SpringApplication.run(RibboFeignMain9005.class, args);
}
}
业务类【为了方便测试,直接模拟了从 mysql 数据库查询的数据】
@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, "link-bd6fb5d9fc03459e885dfc2f4f74c239"));
hashMap.put(2L, new Payment(2L, "link-a588ec6a216f43baa573feb64e0d0dc4"));
hashMap.put(3L, new Payment(3L, "link-2f9342d7eb8940269bd9e2260cff227d"));
}
@GetMapping("/paymentSQL/{id}")
public CommonResult<Payment> paymentSql(@PathVariable("id") Long id){
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult(200,"form myql serverPort: "+serverPort, payment);
return result;
}
}
RibboFeignMain9006 除了端口号,其它与上述一致,创建好后,分别启动这两个服务,如图:
测试 9005 与 9006 访问是否正常,如下图:
pom 由于之前已经导入了所需的依赖包,所以不需要进行修改;
修改 application.yml,增加 openFeign 的配置
server:
port: 9002
spring:
application:
name: seata-order-service
cloud:
nacos:
discovery:
server-addr: 192.168.0.184:8848
namespace: 6ab3cf71-816c-4857-aa23-fdafd7ba1662
sentinel:
transport:
# 配置 Sentinel dashboard 地址
dashboard: localhost:8080
# 默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
datasource:
dsl:
nacos:
server-addr: 192.168.0.184:8848
dataId: seata-order-service
groupId: DEV_GROUP
namespace: 6ab3cf71-816c-4857-aa23-fdafd7ba1662
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
#设置feign客户端超时时间(openfeign默认支持ribbon)
ribbon:
#指的是建立连接所用时间,适用于网络正常情况下,两端连接所用的时间
ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000
logging:
level:
#feign日志以什么级别监控哪个包路径
com.akieay.alibaba.order.feign.*: debug
新增 openFeign 的日志配置类
/**
* @author akieay
* 配置feign的日志级别
*/
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
增加声明式的远程调用业务类,其中
ribbon-feign-service
表示调用的远程服务的 名称,其中的方法则与远程调用的方法一致【包括请求参数,方法名,返回值,请求方式与URL】,总之要除了没有方法体,其它要完全一致;
@FeignClient(value = "ribbon-feign-service")
public interface RibbonFeignService {
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSql(@PathVariable("id") Long id);
}
新增业务类
@RestController
@Slf4j
public class RibbonFeignController {
@Autowired
private RibbonFeignService ribbonFeignService;
@GetMapping("/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
return ribbonFeignService.paymentSql(id);
}
}
启动类
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class OrderMain9002 {
public static void main(String[] args) {
SpringApplication.run(OrderMain9002.class, args);
}
}
测试:直接调用:http://localhost:9002/consumer/paymentSQL/1 多次调用可发现,openFeign 默认采用
轮询
的负载均衡策略调用远程服务,9005 与 9006 的微服务轮询调用;
由于增加了 openFeign 的日志配置,我们可以直接在控制台看到远程调用的日志,如下:
openFeign 默认使用
轮询
的负载均衡策略,我们可以使用 ribbon 修改负载均衡策略,如下面的:随机
负载均衡策略;
新增自定义负载均衡规则配置类
/**
* @author akieay
* @Date: 2020/10/22 0:47
* 自定义负载均衡策略
*/
@Configuration
public class MySelfRule {
/**
* 随机
* @return
*/
@Bean
public IRule myRule(){
return new RandomRule();
}
}
修改 RibbonFeignService,配置负载均衡策略为
随机
@FeignClient(value = "ribbon-feign-service", configuration = MySelfRule.class)
public interface RibbonFeignService {
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSql(@PathVariable("id") Long id);
}
启动服务,浏览器调用:http://localhost:9002/consumer/paymentSQL/1 ;这时候可以看到,微服务的调用完全是随机的,有的连续几次调用都是 9005,有的时候连续几次是 9006,有的时候是 9005 与 9006 交替调用;
其它负载均衡策略参照: