SpringCloud Alibaba Sentinel服务熔断与限流
简介
github:[https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5](https://github.com/alibaba/Sentinel/wiki/如何使用)
官网:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_sentinel
一句话解释,跟Hystrix一样的理念
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的轻量级流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护等多个维度来帮助您保障微服务的稳定性。
流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
熔断降级
除了流量控制以外,及时对调用链路中的不稳定因素进行熔断也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,可能会导致请求发生堆积,进而导致级联错误。
系统负载保护
Sentinel 同时提供系统维度的自适应保护能力。防止雪崩,是系统防护中重要的一环。当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。
针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
安装
下载:https://github.com/alibaba/Sentinel/releases
下载的是sentinel-dashboard-1.7.1.jar
#默认8080端口,8080不要被占用
#直接运行即可,需要jdk环境
java -jar sentinel-dashboard-1.7.1.jar
#linux系统
nohup java -jar sentinel-dashboard-1.7.1.jar &
ctrl+c
cat nohup.out
#访问8080端口
#账号sentinel,密码sentinel
演示工程
依赖配置
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- sentinel-datasource-nacos 后续持久化用 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!-- sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
yml配置
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
# Nacos服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
# sentinel dashboard 地址
dashboard: 192.168.169.130:8080
# 默认为8719,如果被占用会自动+1,直到找到为止
# 簇点链路,可以把某些微服务归类到某个链路,统一处理
port: 8719
# 流控规则持久化到nacos
datasource:
dsl:
nacos:
server-addr: localhost:8848
data-id: ${spring.application.name}
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: "*"
启动类使用的是nacos注册中心@EnableDiscoveryClient
启动,访问Rest请求,可以在sentinel管理页面看到确实有监控cloudalibaba-sentinel-service微服务:
如果不访问该微服务的rest请求或者长时间没有访问该微服务,sentinel管理页面会去掉对该微服务接口的监控。要是页面找不到就访问下资源接口。
流控规则
流量控制
就是限流,限制资源接口的访问数
官网:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6
- 资源名:唯一名称,默认请求路径,就是restcontroller上的路径
- 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
- 阈值类型/单机阈值
- QPS(每秒钟的请求数量):当调用该api的QPS达到阈值的时候,进行限流
- 线程数:当调用该api的线程数达到阈值的时候,进行限流
- 2者区别就是QPS会让请求全部过来访问,线程数最多只能过来对应线程数数量的请求数,来多了也没用
- 是否集群:不需要集群
- 流控模式:
- 直接:api达到限流条件,直接限流
- 关联:当关联的资源达到阈值时,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)(api级别的针对来源)
- 流控效果:
- 快速失败:直接失败,抛异常
- Warm Up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
流控模式
@GetMapping("/testA")
public String testA(){
try {
//0.8秒
TimeUnit.MILLISECONDS.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "testA-----";
}
@GetMapping("/testB")
public String testB(){
log.info(Thread.currentThread().getName() + "...testB ");
return "testB -----";
}
直接(默认)
系统默认直接快速失败,超过阈值直接限流
阈值类型:QPS或线程数
如下设置路径为/testA的流控:
单机阈值是每秒1个访问/每秒一个线程数
快速点击访问http://localhost:8401/testA
手速超过每秒1个访问/每秒一个线程数,就会限制访问。
关联
当关联的资源达到阈值时,就限流自己
当与A关联的资源B达到阈值后,就限流自己
举例子:支付接口遭到大量访问,且很多未处理,那么我们应该限制下订单的接口访问,不然支付接口访问会堆积更多,支付接口会挂。
当关联资源/testB的QPS阈值超过1时,就限流/testA的Rest访问地址,当关联资源到达阈值后限制配置好的的资源名。
postman模拟并发密集访问testB,jmeter也行。
这样就可以大量访问/testB,在网页访问/testA,将无法访问,被限流
链路
多个请求调用了同一个微服务
簇点链路端口默认为8719,如果被占用会自动+1,直到找到为止
簇点链路,可以把某些微服务归类到某个链路端口,统一处理
NodeSelectorSlot
中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root
的虚拟节点,调用链的入口都是这个虚节点的子节点。
一棵典型的调用树如下图所示:
machine-root
/ \
/ \
Entrance1 Entrance2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)
上图中来自入口 Entrance1
和 Entrance2
的请求都调用到了资源 NodeA
,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 FlowRule.strategy
为 RuleConstant.CHAIN
,同时设置 FlowRule.ref_identity
为 Entrance1
来表示只有从入口 Entrance1
的调用才会记录到 NodeA
的限流统计当中,而不关心经 Entrance2
到来的调用。
调用链的入口(上下文)是通过 API 方法 ContextUtil.enter(contextName)
定义的,其中 contextName 即对应调用链路入口名称。
流控效果
直接(默认)
快速失败(默认的流控处理)
直接失败,抛出异常
Blocked by Sentinel(flow limiting)页面
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
预热
公式:阈值除以coldFactor(默认值为3),经过预热时长后才会达到阈值
默认coldFactor为3,即请求QPS从threshold/3开始,经预热时长逐渐升至设定的QPS阈值
限流冷启动:https://github.com/alibaba/Sentinel/wiki/
系统初始化的阈值为10/3约等于3,即阈值刚开始为3;然后过了5秒后阈值才慢慢升高恢复到10。
多次点击http://localhost:8401/testB
一开始点太快会限流Blocked by Sentinel(flow limiting),5秒后10阈值,每秒10个内都可以访问。
应用场景:秒杀系统在开启的瞬间,会有大量流量上来,很有可能把系统打死,预热方式就是为了保护系统,可慢慢把流量放进来,慢慢的把阈值增长到设置的阈值。
排队等待
匀速排队,阈值必须设置为QPS
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
让请求均匀的速度通过,阈值必须设置为QPS,否则无效
如下,/testA每秒1次请求,超过的话排队等待,等待的超时时间为20000毫秒。
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考 流量控制 - 匀速器模式,具体的例子可以参见 PaceFlowDemo。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
postman模拟并发密集访问testA
controller打印日志
@GetMapping("/testA")
public String testA(){
log.info(Thread.currentThread().getName() + "...testA ");
return "testA -----";
}
每秒一个:
降级规则
服务降级,服务超时或异常给出友好提示或兜底方案,实际开发需要自定义返回,这里先测试。
官网:https://github.com/alibaba/Sentinel/wiki/熔断降级
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认抛出DegradeException)
Sentinel的断路器是没有半开状态的,半开的状态系统自动去检测是否请求异常, 没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用.具体可以参考Hystrix。
-
RT(平均响应时间,秒级)
平均响应时间:超出阈值 且 在时间窗口内通过的请求>=5,有两个条件同时满足后触发降级
窗口期过后关闭断路器
RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)
-
异常比例(秒级)
QPS>=5且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
-
异常数(分钟级)
异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
降级策略实战
RT
平均响应时间 (DEGRADE_GRADE_RT
):当 1s 内持续进入 N 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count
,以 ms 为单位),那么在接下的时间窗口(DegradeRule
中的 timeWindow
,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException
)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx
来配置。
对controller睡眠1秒测试TimeUnit.SECONDS.sleep(1);
,对资源选择降级设置:(条件必须每秒请求超过5个,且超过阈值)
使用jmeter压力测试
执行,一秒10个请求>5,请求均超过阈值200ms,未来的时间窗口被降级,jmeter一直执行,一直降级,关闭jmeter,即可恢复。
异常比例
异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO
):当资源的每秒请求量 >= N(可配置),并且每秒异常总数占通过量的比值超过阈值(DegradeRule
中的 count
)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule
中的 timeWindow
,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0]
,代表 0% - 100%。
在对应controller加上异常int age = 10 /0 ;
测试,访问,如下:
条件和设置:
jmeter高并发测试,再次网页访问,不是上次的报错页面了,而是降级:
异常数
异常数是按分钟统计的
异常数 (DEGRADE_GRADE_EXCEPTION_COUNT
):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow
小于 60s,则结束熔断状态后仍可能再进入熔断状态。
在对应controller加上异常int age = 10 /0 ;
测试,访问,如下:
接着设置异常数降级规则:
然后网页访问请求,前5次都是报错的页面,第6次后都是降级页面,降级页面会持续70秒,70秒过后恢复。
热点规则
也是限流措施,热点key,对某个访问量高的参数值进行限流。
官网:https://github.com/alibaba/Sentinel/wiki/热点参数限流
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
兜底方案:
分为系统默认和客户自定义,两种
之前的case,限流出问题后,都是sentinel系统默认的提示:Blocked by Sentinel(flow limiting)
我们也可以自定义,类似hystrix的fallback降级兜底方法,结论是从@HystrixCommand—>>>@SentinelResource
源码:com.alibaba.csp.sentinel.slots.block.BlockException
默认是出错就抛异常BlockException处理。
普通设置
测试代码@RestController:
@SentinelResource(value = "testHotKey", blockHandler = "dealTestHotKey")
设置热点key的异常方法,就是热点key被触发限流了就跳到dealTestHotKey方法。通过BlockException异常来处理。
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "dealTestHotKey") //value自定义,唯一就行,尽量保持跟路径一致
public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
@RequestParam(value = "p2", required = false) String p2){
//int age = 10 /0;
return "testHotKey -----";
}
//BlockException blockException必须加,兜底方法
public String dealTestHotKey(String p1, String p2, BlockException blockException){
return "dealTestHotKey---------";
}
sentinel上设置热点key:
testHotKey是注解上的名字,保持一致。
索引0开始,对应资源接口上的参数,也就是p1。
只要参数中没有p1带参,无论怎么访问都是没问题的。
如果带参p1,只要符合,不超过阈值也可以正常响应,如果1s内QPS访问次数超过阈值1时,则报错。当然这是自定义的报错页面,因为我们加了blockHandler = "dealTestHotKey"
方法
去掉blockHandler = "dealTestHotKey"
方法,则是如下页面
不是Blocked by Sentinel(flow limiting),热点key只处理Sentinel页面上的规则问题,如果代码加入int age = 10/0;
等模拟异常代码,错误信息会直接在页面显示,不处理其他异常的降级,只处理这个BlockException异常,所以热点key必须加上兜底方法。
@SentinelResource
处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;
int age = 10/0;
这个是java运行报出的运行时异常RunTimeException.@SentinelResource
不管
@SentinelResource
主管配置出错,运行出错自己走异常。
参数额外项
上述案例演示了第一个参数p1,当QPS超过1秒1次点击后马上被限流
我们期望p1参数当它是某个特殊值时,它的限流和平时不一样
比如:特例:假如当p1的值等于5时,它的阈值可以达到200
如下设置:
参数类型对应rest接口的p1参数类型
当p1=5时,连续快速测试访问都没触发降级,当p1不等于5的时候,阈值变为平常的1。当p1等于5的时候,阈值变为200。
系统规则
说白了就是这个系统规则会对所有rest接口生效。全局
官网:https://github.com/alibaba/Sentinel/wiki/系统自适应限流
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的
maxQps * minRt
估算得出。设定参考值一般是CPU cores * 2.5
。 - CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。0%-100%。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
比如选择入口QPS的阈值为1,则所有的请求都是每秒只能请求一次,不然就降级。
@SentinelResource
按资源名称+后续处理
测试自带的blockHandler异常方法
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")//名字可自定义,不唯一就行
public CommonResult byResource(){
return new CommonResult(200, "按资源名称限流测试OK", new Payment(2020L, IdUtil.simpleUUID()));//hutool的工具包,生成UUID不带-
}
public CommonResult handleException(BlockException blockException){
// 打印哪个异常方法在限流处理
return new CommonResult<>(444, blockException.getClass().getCanonicalName()+"\t服务不可用" );
}
}
用@SentinelResource上的名字byResource设置流控规则,才会调到自定义方法,如果超过sentinel控制台设置的阈值,跳到自定义的方法:
按照Url地址限流+后续处理
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl(){
return new CommonResult(200, "by url限流测试OK", new Payment(2020L, IdUtil.simpleUUID()));
}
通过访问的URL限流,会返回Sentinel自带默认的限流处理信息
通过路径url设置流控规则,超过阈值:
上面兜底方案面临的问题
- 系统默认的,没有体现我们自己的业务要求
- 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观
- 每个业务方法都添加一个兜底的,那代码膨胀
- 全局统一的处理方法没有体现
客户自定义限流处理逻辑
实际开发常用
这是争对sentinel控制规则违规的兜底,程序异常不行
自定义限流处理类CustomerBlockHandler,参数必须得加上BlockException
package com.wzq.springcloud.myhandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.wzq.springcloud.entities.CommonResult;
/**
* 自定义全局异常方法,方法可被不同的接口使用
* @author wzq
* @version 1.0
* @create 2020/03/06
*/
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception) {
return new CommonResult(444, "客户自定义,global handlerException---1");
}
public static CommonResult handlerException2(BlockException exception) {
return new CommonResult(444, "客户自定义,global handlerException---2");
}
}
contoller调用
//CustomerBlockHandler
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2")
public CommonResult customerBlockHandler(){
return new CommonResult(200, "客户自定义 限流测试OK", new Payment(2020L, IdUtil.simpleUUID()));
}
blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2")
指定自定义限流类中的某个方法来处理。方法可多次被不用controller调用,解耦。
超过流控阈值:
注意点
注意:注解方式埋点不支持 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
直接抛出(若方法本身未定义 throws BlockException 则会被 JVM 包装一层 UndeclaredThrowableException
)。
Sentinel主要有三个核心Api:SphU定义资源、Tracer定义统计、ContextUtil定义了上下文
服务熔断
提供者9003/9004,设置2个一样的微服务9003/9004
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
server:
port: 9003/9004
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: "*"
启动类:@EnableDiscoveryClient
controller接口:
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
public static Map<Long , Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L, new Payment(1L, IdUtil.simpleUUID()));
hashMap.put(2L, new Payment(2L, IdUtil.simpleUUID()));
hashMap.put(3L, new Payment(3L, IdUtil.simpleUUID()));
}
@GetMapping("/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id{
Payment payment = hashMap.get(id);
return new CommonResult<>(200, "from mysql,serverPort:" + serverPort, payment);
}
}
消费者84
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- sentinel-datasource-nacos 后续持久化用 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: 192.168.169.130:8080
port: 8719
service-url:
nacos-user-service: http://nacos-payment-provider
启动类:@EnableDiscoveryClient
Ribbon系列
负载均衡
消费者84添加rest负载均衡:
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
通过服务名调用方法后实现轮询。
无配置
controller调用提供者接口:
@RestController
@Slf4j
public class CircleBreakerController {
private static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback") //没有配置
public CommonResult<Payment> fallback(@PathVariable("id") Long id){
CommonResult<Payment> commonResult = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常");
}else if(commonResult.getData() == null){
throw new NullPointerException("NullPointerException,该ID没有记录,空指针异常");
}
return commonResult;
}
@SentinelResource(value = "fallback") //没有配置
降级都没配置,默认访问,采用ribbon轮询负载均衡:
id = 4,测试程序异常,error页面:
id = 5,测试程序异常,error页面:
只配置程序异常fallback处理
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",fallback = "handlerFallback") //配置了fallback的,fallback只负责业务异常
public CommonResult<Payment> fallback(@PathVariable("id") Long id){
CommonResult<Payment> commonResult = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常");
}else if(commonResult.getData() == null){
throw new NullPointerException("NullPointerException,该ID没有记录,空指针异常");
}
return commonResult;
}
// 本例是fallback
public CommonResult handlerFallback(Long id, Throwable e){
Payment payment = new Payment(id, null);
return new CommonResult(444, "兜底异常handler,exception内容"+e.getMessage(), payment);
}
@SentinelResource(value = "fallback",fallback = "handlerFallback") //配置了fallback的,fallback只负责业务异常
fallback只处理程序异常的兜底,出错则跳到handlerFallback方法,测试程序异常,不再是error页面:
只配置sentinel控制台违规异常blockHandler处理
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",blockHandler = "blockHandler") 配置了blockHandler,只负责sentinel控制台配置违规
public CommonResult<Payment> fallback(@PathVariable("id") Long id){
CommonResult<Payment> commonResult = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常");
}else if(commonResult.getData() == null){
throw new NullPointerException("NullPointerException,该ID没有记录,空指针异常");
}
return commonResult;
}
public CommonResult blockHandler(Long id, BlockException exception){
Payment payment = new Payment(id, null);
return new CommonResult<>(445, "blockHandler-sentinel 限流,无此流水号:blockException" + exception.getMessage(), payment);
}
@SentinelResource(value = "fallback",blockHandler = "blockHandler") 配置了blockHandler,只负责sentinel控制台配置违规
blockHandler只处理sentinel控制台中配置的限流规则违规异常,先配置测试限流,名字是fallback那个:
前2次访问还是error页面,之后访问就触发了blockHandler兜底方法:
fallback和blockHandler都配置
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",fallback = "handlerFallback", blockHandler = "blockHandler")// 配置了blockHandler和fallback
public CommonResult<Payment> fallback(@PathVariable("id") Long id){
CommonResult<Payment> commonResult = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class);
if(id == 4){
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常");
}else if(commonResult.getData() == null){
throw new NullPointerException("NullPointerException,该ID没有记录,空指针异常");
}
return commonResult;
}
// 本例是fallback
public CommonResult handlerFallback(Long id, Throwable e){
Payment payment = new Payment(id, null);
return new CommonResult(444, "兜底异常handler,exception内容"+e.getMessage(), payment);
}
// blockHandler
public CommonResult blockHandler(Long id, BlockException exception){
Payment payment = new Payment(id, null);
return new CommonResult<>(445, "blockHandler-sentinel 限流,无此流水号:blockException" + exception.getMessage(), payment);
}
@SentinelResource(value = "fallback",fallback = "handlerFallback", blockHandler = "blockHandler")// 配置了blockHandler和fallback
配置了blockHandler和fallback
设置流控规则:
1秒一个正常访问,但是超过阈值,调用blockHandler方法:
程序异常测试,调用fallback方法:
但是访问次数超过阈值,一样还会报blockHandler方法:
若blockHandler和fallback都进行了配置,则被限流而抛出BlockException时只会进入blockHandler处理逻辑。
忽略属性
@SentinelResource(value = "fallback",fallback = "handlerFallback", blockHandler = "blockHandler",exceptionsToIgnore = {IllegalArgumentException.class}) // 配置了blockHandler和fallback
exceptionsToIgnore={异常1,异常2,…}
这样可以忽略掉某些程序异常,就是这些忽略的异常报错了,不会跳到fallback兜底方法,正常error页面。
Feign系列
新建提供者的feign模块,只需一个模块
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
直接写接口,无需配置和启动类:
接口跟服务提供者的controller一样
/**
* @author wzq
* @version 1.0
* @date 2020/03/07
*/
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallback.class)
public interface PaymentFeign {
@GetMapping("/paymentSQL/{id}")
CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
Fallback熔断实现,对应该接口的方法:
/**
* @author wzq
* @version 1.0
* @date 2020/03/07
*/
@Component
public class PaymentFallback implements PaymentFeign {
// 熔断
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(444, "fallback");
}
}
服务消费者:
引入需要feign接口的模块依赖
启动类加@EnableFeignClients
yml配置加:
#激活sentinel对feign的支持
feign:
sentinel:
enabled: true
contoller调用:
@Resource
private PaymentFeign paymentFeign;
// 直接调用feign
@GetMapping("/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id{
return paymentFeign.paymentSQL(id);
}
这样接口报错(程序错误和控制台违规)就会调用PaymentFallback的降级熔断方法
测试84调用9003,此时故意关闭9003微服务提供者,看84消费侧自动降级,不会被耗死
框架比较:
Sentinel | Hystrix | resilience4j | |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展性 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于QPS,支持基于调用关系的规范 | 有限的支持 | Rate Limiter |
规则持久化
不配每次重启微服务,之前在sentinel控制台上设置的规则就会不见。
一旦我们重启应用,sentinel规则消失,生产环境需要将配置规则进行持久化
将限流规则持久进Nacos保存,只要刷新微服务某个rest地址,sentinel控制台的流控规则就能看得到,只要Nacos里面的配置不删除,针对8401上的流控规则持续有效。
<!-- sentinel-datasource-nacos 后续持久化用 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
# Nacos服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
# sentinel dashboard 地址
dashboard: 192.168.169.130:8080
# 默认为8719,如果被占用会自动+1,直到找到为止
port: 8719
# 流控规则持久化到nacos
datasource:
dsl:
nacos:
server-addr: 192.168.169.130:8848
data-id: ${spring.application.name}
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: "*"
添加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:是否集群
重启后,访问微服务任一接口就出现原来的配置,持久化,可以说是初始化,之后可以改,一直存在nacos。