Sentinel
官方文档:https://sentinelguard.io/zh-cn/docs/introduction.html
介绍&配置Wiki:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
Wiki文档:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
文中demo:https://duangouyu.coding.net/p/demo/d/SCA/git/tree/sentinel
Sentinel 基本概念
资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
Sentinel 功能和设计理念
流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:
流量控制有以下几个角度:
- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
- 运行指标,例如 QPS、线程池、系统负载等;
- 控制的效果,例如直接限流、冷启动、排队等。
Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
熔断降级
什么是熔断降级
除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。这个问题和 Hystrix 里面描述的问题是一样的。
Sentinel 和 Hystrix 的原则是一致的: 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。
熔断降级设计理念
在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。
与 Hystrix 的区别
Hystrix 通过线程池的方式,来对依赖(在我们的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本,还需要预先给各个资源做线程池大小的分配。
Sentinel 对这个问题采取了两种手段:
- 通过并发线程数进行限制
和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。
- 通过响应时间对资源进行降级
除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。
系统负载保护
Sentinel 同时提供系统维度的自适应保护能力。防止雪崩,是系统防护中重要的一环。当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。
针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
接入Sentinel 控制台
下载控制台
地址:https://github.com/alibaba/Sentinel/releases
使用手册:https://sentinelguard.io/zh-cn/docs/dashboard.html
下载完成后直接使用 java -jar 启动即可
Sentinel控制台中的服务信息需要产生调用后才会显示
客户端接入
在 Nacos&Dubbo协同的demo基础上添加依赖和配置
地址:https://duangouyu.coding.net/p/demo/d/SCA/git/tree/NacosDubboDemo
添加依赖&整合dubbo
springcloudalibaba 的 starter 不用添加接入控制台的依赖 starter 中已经依赖了
但是sentinel适配dubbo的依赖仍需要添加,因为它在stater中被声明为了可选的依赖
<!-- sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- sentinel 适配 dubbo -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo-adapter</artifactId>
</dependency>
添加配置
spring:
cloud:
sentinel:
transport:
# 控制台地址
dashboard: 127.0.0.1:8080
# 通过此端口接收来自控制台的数据 如果端口被占用会依次+1
port: 8720
# 禁止收敛URL入口处的context 不关闭链路流控无法生效
web-context-unify: false
# 关闭懒加载
eager: true
查看服务信息
访问接口在 Sentinel控制台中查看服务信息
服务提供者簇点链路
服务消消费者簇点链路
Sentinel 流控规则
Wiki:https://github.com/alibaba/Sentinel/wiki/%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6
规则介绍
资源名:默认请求路径
针对来源:Sentinel可以针对调⽤者进⾏限流,填写微服务名称,默认default(不 区分来源)
阈值类型/单机阈值 :
QPS:(每秒钟请求数量)当调⽤该资源的QPS达到阈值时进⾏限流
并发线程数:当调⽤该资源的线程数达到阈值的时候进⾏限流(线程处理请求的时候, 如果说业务逻辑执⾏时间很⻓,流量洪峰来临时,会耗费很多线程资源,这些线程 资源会堆积,最终可能造成服务不可⽤,进⼀步上游服务不可⽤,最终可能服务雪崩)
是否集群:是否集群限流
流控模式:
直接:资源调⽤达到限流条件时,直接限流
关联:关联的资源调⽤达到阈值时候限流⾃⼰
链路:只对指定链路上的流量进行限流
流控效果:
快速失败:直接失败,抛出异常
Warm Up:根据冷加载因⼦(默认3)的值,从阈值/冷加载因⼦( qps的1/3 ),经过预热时⻓, 才达到设置的QPS阈值
阈值类型必须设置为QPS
,否则⽆效 排队等待:匀速排队,让请求匀速通过,超时抛出异常,
阈值类型必须设置为QPS
,否则⽆效
阈值类型
QPS
QPS:Queries Per Second意思是“每秒查询率”,是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。
例如:将阈值设置为10那么一秒内只允许通过十个请求,下面使用 ApacheJmeter 进行测试 1个线程 15个请求 间隔1s
可以看到十五个请求中只有十个通过了
流控规则
流控规则
并发线程数
将controller 添加线程休眠 1s 进行测试
例如:将阈值设置为1,则同时只允许一个线程进行并发处理,下面 使用 ApacheJmeter 进行测试 指定五个线程并发,每个线程只发送一次请求
,可以看到只有一个请求通过了
流控规则
测试结果
流控模式
直接
上面的测试的阈值类型均是直接对自己进行限流,直接抛出异常进行流控
关联
关联的资源调⽤达到阈值时候限流⾃⼰
例如:/consumer/test
调用的qps 到达10时 /consumer/hello
进行限流,应如下图所示
流控规则
jmeter 测试 /consumer/test 资源请求15次,/consumer/hello 进行了限流
链路
对调用链路进行限流 只需要指定入口链路即可
首先对上面使用到的两个资源进行改造,让他们调用同一个provider-service中的方法进行调用 只对一个进行限流
这里是在consumer-service中进行了限流
入口名
限流规则
测试结果
流控效果
触发流控后后的效果
快速失败
上面测试过的阈值类型,流控模式均是快速失败,直接返回流控提示
Warm Up ( 预热/冷启动 )
经过指定的时间后,系统的qps会达到设置的阈值
例如:设置预热时长为5s,qps为15,5s内的通过的qps为 15/3 = 5 ,其余请求会被流控,5s后通过qps会达到超过5,
使用 Jmeter 持续请求60s,线程延迟100ms,查看效果
流控规则
控制台结果信息
jmeter
排队等待
每秒内只处理指定阈值的请求,其余请求排队等候,超过超时时间后被流控
例如:每秒内只处理1个请求,超出的请求等待2s后被流控,jmeter 5个线程同时请求
jmeter
流控规则
每秒只处理一个请求 匀速处理
Sentinel 熔断规则
Sentinel 1.8 及以上版本,对熔断降级特性进行了全新的改进升级,加入了类似于 Hystrix 熔断中的半开尝试策略
Wiki:https://github.com/alibaba/Sentinel/wiki/%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7
熔断策略
Sentinel 提供以下几种熔断策略:
- 慢调用比例 (
SLOW_REQUEST_RATIO
):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。 - 异常比例 (
ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是[0.0, 1.0]
,代表 0% - 100%。 - 异常数 (
ERROR_COUNT
):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
引用自官方Wiki
熔断策略说明
资源名:需要进行熔断保护的资源名
熔断策略:
慢调用比例:选择以慢调用比例作为阈值,
慢调用数/总请求数=慢调用比例
最大RT:设置最大的响应时间阈值,大于此阈值统计为慢调用 ,单位ms,注意Sentinel默认统计的RT上限是4900ms,超出此阈值的都会算作4900ms,若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt=xxx来配置
统计时长:指定统计满调用比例的时间窗口 单位ms
比例阈值:取值 [ 0.0 ~ 1.0] 之间的浮点数,代表 0% - 100%,当慢调用在指定的统计时间段内比例达到此比例时,进行熔断,
熔断时长:当发生熔断后,熔断的持续时间,单位 s,
最小请求数:在
统计时长
内,当大于此阈值时的请求才会统计慢调用,否则不会触发熔断规则异常比例:选择异常比例作为阈值,
异常请求数/总请求数=异常比例数
统计时长:指定统计满调用比例的时间窗口 单位ms
比例阈值:取值 [ 0.0 ~ 1.0] 之间的浮点数,代表 0% - 100%,当请求异常在指定的统计时间段内比例达到此比例时,进行熔断
熔断时长:当发生熔断后,熔断的持续时间,单位 s,
最小请求数:在
统计时长
内,当大于此阈值时的请求才会统计异常比例,否则不会触发熔断规则异常数:选择异常数量作为阈值,
异常请求数量
统计时长:指定统计满调用比例的时间窗口 单位ms
异常数:当请求异常在指定的统计时间段内达到此异常数时进行熔断
熔断时长:当发生熔断后,熔断的持续时间,单位 s,
最小请求数:在
统计时长
内,当大于此阈值时的请求才会统计异常数,否则不会触发熔断规则
熔断状态说明
状态 | 说明 |
---|---|
OPEN | 打开状态,熔断所有请求 |
HALF_OPEN | 探测恢复状态,当熔断时长过后,进入探测恢复状态,如果接下来的一个请求顺利通过(慢调用策略:请求响应时间小于RT,异常比例&异常数:请求没有发生异常),则结束熔断,否则会再次熔断 |
CLOSED | 熔断器关闭,请求正常通过 |
慢调用比例
当一定时间内慢调用达到一定比例后,就会熔断
例如:当 /consumer/hello 中的name参数没有时,线程睡眠1s,熔断最大RT 900ms、比例阈值0.5、熔断时长5s、最小请求数5、统计时长1000ms(1s),先使用 jmeter 进行请求,5个线程无限循环,持续1s,请求时不加 name 参数,以触发熔断,熔断后查看效果,当熔断过后,不加 name 参数测试探针回复状态,再次熔断过后,加 name 参数正常请求。
熔断规则
当 jmeter 测试触发熔断后 和 探测恢复状态触发熔断后
探测恢复状态的请求会正常返回,但返回后会继续触发熔断,若加上 name 参数则会进入熔断关闭状态
异常比例
与慢调用比例类似,只不过这个是统计单位时间内触发异常的次数,只是统计指标不同
熔断规则
测试结果是与异常比例一样的
异常数
在单位时间内触发异常的次数超过阈值,熔断
熔断规则
测试结果是与异常比例也是一样的
Sentinel 热点规则
Wiki:https://github.com/alibaba/Sentinel/wiki/%E7%83%AD%E7%82%B9%E5%8F%82%E6%95%B0%E9%99%90%E6%B5%81
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
热点参数规则
热点参数规则(ParamFlowRule
)类似于流量控制规则(FlowRule
):
属性 | 说明 | 默认值 |
---|---|---|
resource | 资源名,必填 | |
count | 限流阈值,必填 | |
grade | 限流模式 | QPS 模式 |
durationInSec | 统计窗口时间长度(单位为秒),1.6.0 版本开始支持 | 1s |
controlBehavior | 流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持 | 快速失败 |
maxQueueingTimeMs | 最大排队等待时长(仅在匀速排队模式生效),1.6.0 版本开始支持 | 0ms |
paramIdx | 热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的参数索引位置 | |
paramFlowItemList | 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型 | |
clusterMode | 是否是集群参数流控规则 | false |
clusterConfig | 集群流控相关配置 |
使用方法
添加注解
在方法上添加注解
@SentinelResource
例如:这里同时制定了资源名,方便查看,但是不要与已经有的名字重复,否则会重复统计
@SentinelResource("consumer-hello") @GetMapping("/hello") public String hello(String name) throws Exception { // if (ObjectUtils.isEmpty(name)){ // // 模拟 RT //// Thread.sleep(1000); // // 模拟异常 // throw new RuntimeException(); // } log.info("处理时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SS"))); return this.providerService.sayHello(name); }
配置热点规则
在簇点链路中找到热点资源
配置热点规则
测试
使用postman测试
name=123 可以轻易触发热点规则( qps = 1 ),而 name=mengshuo则不容易( qps = 100 )
Sentinel 系统规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(
EntryType.IN
),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。系统规则支持以下的模式:
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的
maxQps * minRt
估算得出。设定参考值一般是CPU cores * 2.5
。- CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
Sentinel 授权规则
Wiki:https://github.com/alibaba/Sentinel/wiki/%E9%BB%91%E7%99%BD%E5%90%8D%E5%8D%95%E6%8E%A7%E5%88%B6
其实就是黑白名单配置
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制(黑白名单控制)的功能。来源访问控制根据资源的请求来源(
origin
)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。调用方信息通过
ContextUtil.enter(resourceName, origin)
方法中的origin
参数传入。规则配置
来源访问控制规则(
AuthorityRule
)非常简单,主要有以下配置项:
resource
:资源名,即限流规则的作用对象。limitApp
:对应的黑名单/白名单,不同 origin 用,
分隔,如appA,appB
。strategy
:限制模式,AUTHORITY_WHITE
为白名单模式,AUTHORITY_BLACK
为黑名单模式,默认为白名单模式。
Sentinel 集群流控
Wiki:https://github.com/alibaba/Sentinel/wiki/%E9%9B%86%E7%BE%A4%E6%B5%81%E6%8E%A7
Sentinel开源版集群流控需要自己做改造,阿里云的Sentinle可以直接使用
挖坑,
Sentinel 兜底方法
sentinel 默认被限流、熔断等操作时的处理对于rest服务不太友好
使用@SentinelResource
Wiki:https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81
blockHandler:指定sentinel 触发流控等效果后的处理方法,
此方法必须在最后添加BlockException 类型的形参
blockHandlerClass:当 blockHandler 与正常方法不在同一类中时指定类,此时 blockHandler 方法也应该为
静态
的fallback :回退、降级方法名称,例如调用时出现异常,这个异常不是 sentinel 限流等措施的异常
fallbackClass:降级类,当降级方法与正常方法不在同一个类中时使用,此时的降级方法应为
静态
的
-
在同类中添加方法
@SentinelResource(value = "consumer-hello", fallback = "helloBlockHandler",blockHandler = "helloBlockHandler") @GetMapping("/hello") public RestResponse<?> hello(String name) throws Exception { if (ObjectUtils.isEmpty(name)) { // 模拟 RT // Thread.sleep(1000); // 模拟异常 throw new RuntimeException(); } log.info("处理时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SS"))); return RestResponse.success().data(this.providerService.sayHello(name)); } @GetMapping("/test") public String test(String name) { return this.providerService.sayHello(name); } public RestResponse<?> helloBlockHandler(String name, BlockException blockException){ return RestResponse.failed(); }
测试:
-
在其他类中添加方法
- @SentinelResource 配置
@SentinelResource( value = "consumer-hello", fallbackClass = ConsumerControllerFallback.class, fallback = "helloBlockHandler", blockHandlerClass = ConsumerControllerFallback.class, blockHandler = "helloBlockHandler" ) @GetMapping("/hello") public RestResponse<?> hello(String name) throws Exception { if (ObjectUtils.isEmpty(name)) { // 模拟 RT // Thread.sleep(1000); // 模拟异常 throw new RuntimeException(); } log.info("处理时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SS"))); return RestResponse.success().data(this.providerService.sayHello(name)); }
- 其他类添加方法
public class ConsumerControllerFallback { public static RestResponse<?> helloBlockHandler(String name, BlockException blockException){ return RestResponse.failed(); } }
测试:
统一处理
实现BlockExceptionHandler 类
实现 BlockExceptionHandler 后可以统一处理BlockException的异常 这个异常是 Sentinel 中的流控等措施的异常父类,其子类有:
- FlowException:流控规则异常。
- DegradeException:熔断规则异常。
- ParamFlowException:热点参数规则异常。
- SystemBlockException:系统规则异常。
- AuthorityException:授权规则异常。
实现类,当前实现类并不能处理 使用了 @SentinelResource 注解后的资源抛出的限流等异常信息,可以使用注解指定,或者SpringWeb中的全局异常处理器
package top.mengshuo.common.exception.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import top.mengshuo.common.exception.entity.RestResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Sentinel 异常处理
*
* @author mengshuo
* @since 2021-11-23
*/
@Component
public class SentinelBlockExceptionHandle implements BlockExceptionHandler {
/**
* Handle the request when blocked.
*
* @param request Servlet request
* @param response Servlet response
* @param e the block exception
* @throws Exception users may throw out the BlockException or other error occurs
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = null;
if (e instanceof FlowException) {
//限流异常
msg = "接口已被限流";
} else if (e instanceof DegradeException) {
//熔断异常
msg = "接口已被熔断,请稍后再试";
} else if (e instanceof ParamFlowException) {
//热点参数限流
msg = "热点参数限流";
} else if (e instanceof SystemBlockException) {
//系统规则异常
msg = "系统规则(负载/....不满足要求)";
} else if (e instanceof AuthorityException) {
//授权规则异常
msg = "授权规则不通过";
}
response.setStatus(500);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
//ObjectMapper是内置Jackson的序列化工具类,这用于将对象转为JSON字符串
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getWriter(), RestResponse.failed().message(msg));
}
}
定义全局异常处理
这个是SpringWeb提供的能力,项目中也是经常使用,只能处理使用了 @Sentinel 注解的资源限流等信息,
package top.mengshuo.common.exception.spring;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import top.mengshuo.common.exception.entity.RestResponse;
/**
* @author mengshuo
* @since 2021-11-23
*/
@RestControllerAdvice
public class SystemExceptionHandle {
@ExceptionHandler(BlockException.class)
public RestResponse<?> sentinelException(Exception e) {
String msg = null;
if (e instanceof FlowException) {
//限流异常
msg = "接口已被限流";
} else if (e instanceof DegradeException) {
//熔断异常
msg = "接口已被熔断,请稍后再试";
} else if (e instanceof ParamFlowException) {
//热点参数限流
msg = "热点参数限流";
} else if (e instanceof SystemBlockException) {
//系统规则异常
msg = "系统规则(负载/....不满足要求)";
} else if (e instanceof AuthorityException) {
//授权规则异常
msg = "授权规则不通过";
}
return RestResponse.failed().message(msg);
}
}
可以将以上两个异常处理提取到公共模块中,但是需要导入下面的依赖
<dependencies>
<!-- sentinel 适配 spring mvc 依赖 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-webmvc-adapter</artifactId>
</dependency>
<!-- 热点参数依赖 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
</dependency>
<!-- tomcat依赖 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
<!-- jackson 序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- spring context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<!-- spring web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>
统一响应对象&状态枚举类
统一响应对象
package top.mengshuo.common.exception.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import top.mengshuo.common.exception.enums.HttpStatus;
/**
* @author mengshuo
* @since 2021-11-23
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
public class RestResponse<T> {
private Integer code;
private String message;
private T data;
public RestResponse(Integer code) {
this.code = code;
}
public RestResponse(HttpStatus httpStatus) {
this.code = httpStatus.getCode();
this.message = httpStatus.getMessage();
}
public static <T> RestResponse<T> success() {
return new RestResponse<>(HttpStatus.SUCCESS);
}
public static <T> RestResponse<T> failed() {
return new RestResponse<>(HttpStatus.FAILED);
}
public RestResponse<T> message(String message) {
this.message = message;
return this;
}
public RestResponse<T> data(T data) {
this.data = data;
return this;
}
}
状态枚举
package top.mengshuo.common.exception.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author mengshuo
* @since 2021-11-23
*/
@Getter
@AllArgsConstructor
public enum HttpStatus {
/**
* 成功
*/
SUCCESS(2000, "请求成功"),
/**
* 失败
*/
FAILED(5000, "请求失败"),
;
private Integer code;
private String message;
}
测试结果
Sentinel 规则数据持久化
sentinel 的规则数据默认放在内存中,服务重启后规则消失
规则管理及推送
一般来说,规则的推送有下面三种模式:
推送模式 | 说明 | 优点 | 缺点 |
---|---|---|---|
原始模式 | API 将规则推送至客户端并直接更新到内存中,扩展写数据源(WritableDataSource ) | 简单,无任何依赖 | 不保证一致性;规则保存在内存中,重启即消失。严重不建议用于生产环境 |
Pull 模式 | 扩展写数据源(WritableDataSource ), 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件 等 | 简单,无任何依赖;规则持久化 | 不保证一致性;实时性不保证,拉取过于频繁也可能会有性能问题。 |
Push 模式 | 扩展读数据源(ReadableDataSource ),规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。生产环境下一般采用 push 模式的数据源。 | 规则持久化;一致性;快速 | 引入第三方依赖 |
原始模式
如果不做任何修改,Dashboard 的推送规则方式是通过 API 将规则推送至客户端并直接更新到内存中:
这种做法的好处是简单,无依赖;坏处是应用重启规则就会消失,仅用于简单测试,不能用于生产环境。
Pull模式
pull 模式的数据源(如本地文件、RDBMS 等)一般是可写入的。使用时需要在客户端注册数据源:将对应的读数据源注册至对应的 RuleManager,将写数据源注册至 transport 的 WritableDataSourceRegistry
中。以本地文件数据源为例:
public class FileDataSourceInit implements InitFunc {
@Override
public void init() throws Exception {
String flowRulePath = "xxx";
ReadableDataSource<String, List<FlowRule>> ds = new FileRefreshableDataSource<>(
flowRulePath, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {})
);
// 将可读数据源注册至 FlowRuleManager.
FlowRuleManager.register2Property(ds.getProperty());
WritableDataSource<List<FlowRule>> wds = new FileWritableDataSource<>(flowRulePath, this::encodeJson);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
WritableDataSourceRegistry.registerFlowDataSource(wds);
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
本地文件数据源会定时轮询文件的变更,读取规则。这样我们既可以在应用本地直接修改文件来更新规则,也可以通过 Sentinel 控制台推送规则。以本地文件数据源为例,推送过程如下图所示:
首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。使用 pull 模式的数据源时一般不需要对 Sentinel 控制台进行改造。
这种实现方法好处是简单,不引入新的依赖,坏处是无法保证监控数据的一致性。
Push模式
生产环境下一般更常用的是 push 模式的数据源。对于 push 模式的数据源,如远程配置中心(ZooKeeper, Nacos, Apollo等等),推送的操作不应由 Sentinel 客户端进行,而应该经控制台统一进行管理,直接进行推送,数据源仅负责获取配置中心推送的配置并更新到本地。因此推送规则正确做法应该是 配置中心控制台/Sentinel 控制台 → 配置中心 → Sentinel 数据源 → Sentinel,而不是经 Sentinel 数据源推送至配置中心。这样的流程就非常清晰了:
我们提供了 ZooKeeper, Apollo, Nacos 等的动态数据源实现。以 ZooKeeper 为例子,如果要使用第三方的配置中心作为配置管理,您需要做下面的几件事情:
- 实现一个公共的 ZooKeeper 客户端用于推送规则,在 Sentinel 控制台配置项中需要指定 ZooKeeper 的地址,启动时即创建 ZooKeeper Client。
- 我们需要针对每个应用(appName),每种规则设置不同的 path(可随时修改);或者约定大于配置(如 path 的模式统一为
/sentinel_rules/{appName}/{ruleType}
,e.g.sentinel_rules/appA/flowRule
)。 - 规则配置页需要进行相应的改造,直接针对应用维度进行规则配置;修改同个应用多个资源的规则时可以批量进行推送,也可以分别推送。Sentinel 控制台将规则缓存在内存中(如
InMemFlowRuleStore
),可以对其进行改造使其支持应用维度的规则缓存(key 为 appName),每次添加/修改/删除规则都先更新内存中的规则缓存,然后需要推送的时候从规则缓存中获取全量规则,然后通过上面实现的 Client 将规则推送到 ZooKeeper 即可。 - 应用客户端需要注册对应的读数据源以监听变更,可以参考 相关文档。
从 Sentinel 1.4.0 开始,Sentinel 控制台提供 DynamicRulePublisher
和 DynamicRuleProvider
接口用于实现应用维度的规则推送和拉取,并提供了相关的示例。Sentinel 提供应用维度规则推送的示例页面(/v2/flow
),用户改造控制台对接配置中心后可直接通过 v2 页面推送规则至配置中心。改造详情可参考 应用维度规则推送示例。
部署多个控制台实例时,通常需要将规则存至 DB 中,规则变更后同步向配置中心推送规则。
基于Nacos的规则持久化
直接在nacos中创建配置文件
最终的配置都是放在nacos配置中心,可以直接编辑配置文件达到动态编辑规则的目的,
具体规则参数可以参考:
com.alibaba.cloud.sentinel.datasource.RuleType
枚举中的规则类的属性,即com.alibaba.csp.sentinel.slots.block.Rule
的实现类中的属性
添加配置
默认 Nacos 适配的 dataId 和 groupId 约定如下:
- groupId:
SENTINEL_GROUP
- 流控规则 dataId:
{appName}-flow-rules
,比如应用名为 appA,则 dataId 为appA-flow-rules
用户可以在
NacosConfigUtil
修改对应的 groupId 和 dataId postfix。用户可以在NacosConfig
配置对应的Converter
,默认已提供FlowRuleEntity
的 decoder 和 encoder。
配置项说明:
spring.cloud.sentinel.datasource.xx.yy.**
说明:
xx 用来区分不同的规则,名字随意起
yy代表数据存储位置 例如nacos
**代表配置项,
不同的配置持久化配置不同,具体持久化配置参考:
com.alibaba.cloud.sentinel.datasource.config.AbstractDataSourceProperties
及其子类的属性
spring:
cloud:
nacos:
discovery:
# 注册中心地址
server-addr: 192.168.59.102:8848
config:
# 配置中心地址
server-addr: 192.168.59.102:8848
# nacos config dataId 的后缀,也是配置内容的文件扩展名
file-extension: yaml
sentinel:
transport:
dashboard: 127.0.0.1:8080
# 通过此端口接收来自控制台的数据 如果端口被占用会依次+1
port: 8720
# 禁止收敛URL入口处的context 不关闭链路流控无法生效
web-context-unify: false
# 关闭懒加载
eager: true
# sentinel 规则持久化
datasource:
# 限流
flow:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
# 规则类型,取值见:com.alibaba.cloud.sentinel.datasource.RuleType
rule_type: flow
# 降级
degrade:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
data-id: ${spring.application.name}-degrade-rules
group-id: SENTINEL_GROUP
# 规则类型,取值见:com.alibaba.cloud.sentinel.datasource.RuleType
rule-type: degrade
# 热点参数
param-flow:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
data-id: ${spring.application.name}-param-flow-rules
group-id: SENTINEL_GROUP
# 规则类型,取值见:com.alibaba.cloud.sentinel.datasource.RuleType
rule-type: param-flow
# 系统规则
system:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
data-id: ${spring.application.name}-system-rules
group-id: SENTINEL_GROUP
# 规则类型,取值见:com.alibaba.cloud.sentinel.datasource.RuleType
rule-type: system
# 授权规则 黑白名单
authority:
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
data-id: ${spring.application.name}-authority-rules
group-id: SENTINEL_GROUP
# 规则类型,取值见:com.alibaba.cloud.sentinel.datasource.RuleType
rule-type: authority
可以将相同的配置添加到nacos配置中心
通过控制台对数据进行双向同步
Wiki:https://github.com/alibaba/Sentinel/wiki/Sentinel-控制台(集群流控管理)#规则配置。
-
复制
src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos
目录到src/main/java/com/alibaba/csp/sentinel/dashboard/rule
下 -
将
src/main/java/com/alibaba/csp/sentinel/dashboard/controller/v2/FlowControllerV2.java
中注入的bean替换为 刚才复制的类@Autowired @Qualifier("flowRuleDefaultProvider") private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider; @Autowired @Qualifier("flowRuleDefaultPublisher") private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
替换为
@Autowired @Qualifier("flowRuleNacosProvider") private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider; @Autowired @Qualifier("flowRuleNacosPublisher") private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
-
修改NacosConfig类配置
全限定名:com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfig#nacosConfigService
@Configuration public class NacosConfig { @Bean public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() { return JSON::toJSONString; } @Bean public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() { return s -> JSON.parseArray(s, FlowRuleEntity.class); } @Bean public ConfigService nacosConfigService() throws Exception { return ConfigFactory.createConfigService("localhost"); } }
改为
也可以把nacos地址硬编码进去
@Configuration public class NacosConfig { @Value("${nacos.server-addr}") private String serverAddress; @Bean public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() { return JSON::toJSONString; } @Bean public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() { return s -> JSON.parseArray(s, FlowRuleEntity.class); } @Bean public ConfigService nacosConfigService() throws Exception { return ConfigFactory.createConfigService(serverAddress); } }
添加配置
nacos.server-addr=192.168.59.102:8848
-
修改 pom.xml
<!-- for Nacos rule publisher sample --> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> <!--<scope>test</scope>--> </dependency>
-
修改前端
<!--<li ui-sref-active="active" ng-if="entry.appType==0">--> <!--<a ui-sref="dashboard.flow({app: entry.app})">--> <!--<i class="glyphicon glyphicon-filter"></i> 流控规则 V1</a>--> <!--</li>-->
改为
<li ui-sref-active="active" ng-if="entry.appType==0"> <a ui-sref="dashboard.flow({app: entry.app})"> <i class="glyphicon glyphicon-filter"></i> 流控规则Nacos</a> </li>
注意:这仅仅只是持久化了流控规则,若是想用其他则需要进行大量改造,当然也可以用阿里云 Sentinel 企业版