下面内容摘要自 github alibaba/Sentinel 介绍
Sentinel 是用于 维护微服务架构稳定性 的组件,稳定性 包括:
优点:
与 Hystrix对比,Hystrix 传送门在此
Hystrix | Sentinel | |
---|---|---|
提供服务 | 耦合于项目 | 独立组件独立部署 |
提供管理 | 需要 dashboard 配合搭建管理端 | 默认提供页面化细粒度统一配置 |
隔离策略 | 信号量(并发线程数限流) | 信号量 / 线程池 |
熔断降级策略 | 基于RT / 异常比例 / 异常数 | 基于异常比例 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(RxJava) |
动态规则配置 | 多种数据源 | 多种数据源 |
扩展性 | 多扩展点(Slot) | 插件的形式 |
注解 | √ | √ |
限流 | 基于 QPS,支持基于调用关系的限流(关联、链路) | 有限支持 |
sentinel 的核心视角
sentinel 具有比较独特的核心视角
资源(resource)
官网说明
资源(resource) 是 Sentinel 中的核心概念之一。最常用的资源是我们代码中的 Java 方法。 当然,您也可以更灵活的定义你的资源,例如,把需要控制流量的代码用 Sentinel API SphU.entry(“HelloWorld”) 和 entry.exit() 包围起来即可。
resource 的本体是服务中的接口,微服务架构中所有需要被监控和控制的都是其中的接口,包括
resource 是 sentinel 看待 resource 的本体——即服务中的接口——的角度。
在 sentinel 的视角,服务中的接口是可以监控和进行隔离操作的,sentinel 的作用主要就是把这些接口,摆出十八般模样,因此称呼它们为 资源(resource)。类似厨师对待食材的视角。
resource 的声明
官网提供了 3 类、共计 5 种声明方式,
功能插槽(slot chain)
官网说明
在 Sentinel 里面,所有的资源都对应一个资源名称以及一个 Entry。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 API 显式创建
每一个 Entry 创建的时候,同时也会创建一系列功能插槽(slot chain)
官网描述中,一系列功能插槽是指如下一系列,这里按拆分成两个表格
表 2.1-1 主要用于收集信息的 slot,resource 的围观群众
名称 | 收集信息 | 作用 |
---|---|---|
NodeSelectorSlot | 资源的调用路径,以树状结构存储 | 根据调用路径来限流降级 |
ClusterBuilderSlot | 资源的统计信息以及调用者信息(RT, QPS, thread count ) | 多维度限流,降级 |
StatisticSlot | runtime 指标监控信息 |
表 2.1-2 主要用于控制的 slot,resource 的调教者
名称 | 依据 | 行为 |
---|---|---|
DegradeSlot | 统计信息 / 预设的规则 | 熔断降级 |
FlowSlot | 预设的限流规则 / 上表 slot 统计的状态 | 流量控制 |
AuthoritySlot | 黑白名单 / 调用来源信息 | 黑白名单控制 |
SystemSlot | 系统的状态 | 入口流量 |
ParamFlowSlot |
从下图看,功能插槽(slot chain) 也是按上面的区分成组
功能插槽(slot chain) 的扩展
通过 ProcessorSlot / SlotChainBuilder 可以实现功能插槽(slot chain) 的扩展
扩展插槽的位置如下图所示
规则(rule)
Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效。同时 Sentinel 也提供相关 API,供您来定制自己的规则策略。
Sentinel 支持以下几种规则,依次对应 表 2.1-2 中各个 slot
下载
可以从 github sentinel 直接下载
下载后是一个 jar 包
运行
nohup java -jar sentinel-dashboard-1.8.4.jar &
启动成功后可以通过 8080 看到登录页面
添加项目支持
依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
配置
server:
port: 8400
spring:
application:
name: nacos-sentinel-service
cloud:
nacos:
config:
server-addr: 192.168.3.10:8848
file-extension: yml
discovery:
server-addr: 192.168.3.10:8848
sentinel:
transport:
dashboard: 192.168.3.10:8080 #配置Sentinel dashboard地址
port: 8719 #若有占用,它自己+1,再被占用,再+1
启动类
@SpringBootApplication
@EnableDiscoveryClient
public class NacosSentinelApplication {
public static void main(String[] args) {
SpringApplication.run(NacosSentinelApplication.class,args);
}
}
启动并验证
依次完成如下操作
在 nacos 上检查服务注册
调用服务,sentinel 默认懒加载,不调用服务不触发监控
查看 sentinel dashboard 上的监控信息
流量控制规则的 通式 为
对套用了流量控制规则的 资源 A 而言
被控资源:A
监控项:
流控模式
直接
直接对 A 的监控项进行统计
关联
当另一个资源 B 与 A 存在竞争/依赖关系时,二者有关联。关联模式下,统计关联资源 B 的监控项,若触发条件,对 A 进行流量控制
用于 A 给资源 B 让渡流量的场景,比如 A 代表普通用户业务办理,而 B 代表 vip 用户
链路
链路可能有多个链路可以调用 A,链路模式可以只统计处于指定链路下的 A 的监控项,其余对 A 的调用不计入统计。通过链路入口指定链路
阈值
对监控项的统计达到此数值时,触发规则
触发规则意味着对 被控资源 施加 流量控制行为
流量控制行为
快速失败
新的请求就会被立即拒绝
拒绝方式为抛出FlowException,请求默认返回响应 Blocked by Sentinel (flow limiting)
监控项为 并发线程数 时,流量控制行为 默认并且只有 快速失败
预热(Warm Up)
从初始流速开始缓慢加大请求流速,经过预热时长达到最大流速。
长期不活跃的系统可能处于 “冷运行” 状态。若突发大流量,系统正常情况下会创建系统资源以适应骤增的流量。但是,资源的创建(比如在线程池中开辟新的线程)需要时间,系统可能在准备充足的资源之前就被压垮。预热可以通过缓冲流量放行速度,争取这个时间。
其中具体设定如下:
排队等待
新的请求放入漏桶(基于漏桶算法),并根据阈值严格限制单机请求的放行速度
在间歇性出现大流量的场景(某一秒有大量的请求到来,而接下来的几秒则处于空闲状态),可以使系统平稳的应对波动的流量,而不是马上丢弃一部分
单机请求的放行时间间隔为 (1000 / 阈值) ms
需要配置超时时间,在漏桶中存在超过超时时间的请求将被丢弃
熔断/降级控制规则的 通式 为
对套用了熔断/降级控制规则的 资源 A 而言
被控资源:A
统计时长
即通式中的 单位时间,作为统计窗口
最小请求数
即通式中的 请求量达到一定级别
请求未达到此数量时,不考虑熔断 / 降级
最小请求数作用于每一个统计窗口,每轮统计都需要达到最小请求数才判断是否触发规则
监控项:
对 A 的调用的 健康状态
健康状态有多种描述方式或评估方法,对应 熔断策略 ,包括
熔断策略和阈值
慢调用比例
调用时间超过 最大 RT(最大 Response Time) 的调用会被统计为慢调用
阈值为一个比例
异常比例
发生业务异常的调用会被统计为异常调用
阈值为一个比例
异常数
同上,但阈值为一个具体数字
熔断 / 降级和熔断时长
每次熔断都会使资源在一个熔断时长中不可用
度过一轮熔断时长后,进入探测恢复状态(HALF-OPEN 状态),放行一个请求
若此请求正常响应,则资源恢复,否则继续熔断一个熔断时长
按熔断策略不同,请求正常响应的判断标准也不同
热点参数限流规则的 通式 为
对套用了热点参数限流规则的 资源 A 而言
被控资源:
A
统计窗口时长
即通式中的 单位时间
带参访问
带参访问需要通过 @SentinelResource 声明资源,否则可能不生效
以下情况被认为带参访问
访问资源时,携带了指定的参数
访问资源时,携带了指定的参数,且值为指定值
参数通过在资源参数表中的索引进行指定,从 0 开始计数,1 表示 A 的第二个参数
参数可以设置例外项
每个例外项都是这个参数的一个指定值
每个例外项都具有参数类型,但类型仅支持 数字、字符 和 字符串
每个例外项都可以有自己的独立阈值
阈值
阈值是一个 Qps 值
对监控项的统计达到此数值时,触发规则
触发规则意味着对 被控资源 阻塞访问
阻塞访问
效果等同 §4.1 流量控制规则 中的快速失败
系统自适应保护规则的 通式 为
对套用了系统自适应保护规则的 系统 A 而言
被控系统:
A
入口流量
所有进入系统 A,需要被 A 处理的流量
入口流量有多种描述方式或评估方法,包括
Load(仅对 Linux/Unix-like 机器生效)
A 的负载超过阈值,且系统当前的并发线程数超过 系统容量 时才会触发
系统容量由系统的 maxQps * minRt
设定参考值一般是 CPU cores * 2.5
存疑:详见 系统容量
CPU 使用率
当系统 CPU 使用率超过阈值(取值范围 0.0-1.0)。
RT (响应时间)
当单台机器上所有入口流量的平均 RT 达到阈值,单位是毫秒。
线程数
当单台机器上所有入口流量的并发线程数达到阈值
入口 QPS
当单台机器上所有入口流量的 QPS 达到阈值
阈值
对监控项的统计达到此数值时,触发规则
触发规则意味着对 被控资源 阻塞访问
阻塞访问
效果等同 §4.1 流量控制规则 中的快速失败
系统容量(不确定,但能说通)
官网提供的计算方式:系统容量 = maxQps * minRt
系统容量是一个用于评估当前系统处理能力的值,这个值的含义大约可以理解为:
按系统的处理能力,需要长时间保持 (系统容量)根 线程同时作业,才能消化掉当前的业务访问
或者可以把这个系统容量理解成 由实际观测数据估算的系统当前常驻线程数
我们很好理解系统容量的单位是 个,一个 对应 一根线程:
令 maxQps = y个/秒(Qps是个速度,不是纯数值),minRt = x 秒
则,系统容量 = y个/秒 * x 秒 = xy个
官网上的计算方式可能是按下面的思路得来的:
在一段时长 T 里,
系统在源源不断的处理业务,业务可以理解为请求的集合,
每个请求都需要一个时长t 才能处理完,系统实际处理了 Ttotal = ∑ t 时长的业务
那么如何在一个时长 T 里,处理了明显更多时长 TTotal 的业务 —— 通过多线程
所以,用时长 T 处理总时长为 Ttotal 的业务需要系统常驻 (Ttotal / T) 根线程
问:现在有一盘包子,正常人吃得吃 60 分钟,现在只有 15 分钟,怎么吃完?
答:4人同时吃
假设上面的时长 T 就是 1 秒,则 Ttotal 也相应的成为 Tsecond (Ttotal / T = Tsecond / 1s)
Tsecond 不太好估算,于是把所有的请求都进行折算
每个请求都折算成 1-n 个特别简单的单位请求,相当于每个请求都是对单位请求的 1-n 倍加权,1 秒内的所有请求 可以折算成 1 秒内的所有单位请求的总和,记为 C
在此前提下,我们可以认为每个单位请求的处理时间一样并且很短,作为单位请求处理时间 t’
则有 Tsecond = ( ∑ Random(n) ) * t’ = Ct’
C 也不好计算,于是只能认为在某个时段,系统里处理的请求都是单位请求级别,此时请求数量最多。所以将单位时间内请求数量的最大值作为 C,因为时长是 1 秒,所以这个值实际就是一秒内请求数的最大值,即 maxQps * 1秒(qps 的单位是 个/秒)
t’ 也不好计算,于是只能认为单位请求处理时间就是系统中出现的最短的响应时间,即 minRt
于是有下表
原始 | 折算 | 对折算的估算取值 |
---|---|---|
请求数量 | 单位请求总量 | maxQps * 1s |
请求响应时间 | 单位请求处理时间 | minRt |
∑ 求和 | 乘法 | 乘法 |
Tsecond | ( ∑ Random(n) ) * t’ = Ct’ | maxQps * 1s * minRt |
Tsecond / 1s | Ct’ / 1s | maxQps * minRt |
疑问:
官网描述
Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5
从上面的计算推导看 maxQps * minRt 其实是估算了一个线程数,而官网上对阈值的推荐配置 CPU cores * 2.5 明显也是个线程数。load1 超过用户设置的值(CPU cores * 2.5 )和 当前的并发线程数超过系统容量难道不是一回事?还是说系统容量是通过压测数据计算的,而不是实时数据? 有看到这里的大佬请解惑。
value
资源名称,必需项,不能为空
entryType
entry 类型,可选项,默认为 EntryType.OUT
blockHandler
当资源发生 BlockException 时,对应处理的方法名称,可选项
blockHandler 方法
fallback
当资源发生 Exception 时,对应处理方法名称,可选项
fallback 方法
defaultFallback
当资源发生 Exception 时且没有指定 fallback 方法时,缺省 fallback 方法名,可选项
缺省 fallback 方法
exceptionsToIgnore
忽略异常
当资源抛出被此属性涵盖的异常时,
需注意
使用 @SentinelResource 声明的资源,必须至少声明 blockHandler、fallback 或 defaultFallback 之一,否则发生异常时会抛出白页,如下图
使用 SphU 声明资源
SphU 工具可以声明 Sentinel 资源,但可读性相对 @SentinelResource 较差,同时不能自动完成对上层业务异常的统计,因此不推荐
SphU 工具提供了 3 种资源声明方法,三种方法的区别不在于声明资源本身
而是处理 block 或 fallback 的位置随三种声明方式,出现格式上的变化,细节如下
基于 try-catch-finally 或 try-with-resource
// 1.5.0 版本开始可以利用 try-with-resources 特性
// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
try (Entry entry = SphU.entry("resourceName")) {
// 被保护的业务逻辑
// do something here...
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
// 在此处进行相应的处理操作
}
基于 if-try-finally-else
// 资源名可使用任意有业务语义的字符串
if (SphO.entry("自定义资源名")) {
// 务必保证finally会被执行
try {
/**
* 被保护的业务逻辑
*/
} finally {
SphO.exit();
}
} else {
// 资源访问阻止,被限流或被降级
// 进行相应的处理操作
}
基于异步调用
try {
AsyncEntry entry = SphU.asyncEntry(resourceName);
// 异步调用.
doAsync(userId, result -> {
try {
// 在此处处理异步调用的结果.
} finally {
// 在回调结束后 exit.
entry.exit();
}
});
} catch (BlockException ex) {
// Request blocked.
// Handle the exception (e.g. retry or fallback).
}
SphU 声明资源的套路
使用 Tracer 统计业务异常(非 BlockException 异常)
Tracer 用于记录用户通过 SphU 或 SphO 手动定义的资源的业务异常。
因上述资源,不能由 Sentinel 感知上层业务异常并自动记录,所以,只能手动调用如下 API
上述 API 不能在 try-with-resources 形式的 SphU.entry(xxx) 中使用,否则会统计不上
完整示例
依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
配置
server:
port: 8800
spring:
application:
name: nacos-sentinel-ribbon-sentinel-comsumer
cloud:
nacos:
discovery:
server-addr: 192.168.3.10:8848
sentinel:
transport:
dashboard: 192.168.3.10:8080 #配置Sentinel dashboard地址
port: 8719 #若有占用,它自己+1,再被占用,再+1
#使用 OpenFeign 并需要日志时添加
logging:
config: classpath:logback.cfg.dev.xml
level:
# 对服务类进行正常的接口配置,否则对应类根本不输出日志,就无所谓其中的接口日志记录到什么程度了
com.fc.sprcloudlearning.order.service.PaymentOpenfeignClient: debug
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(2))
.setReadTimeout(Duration.ofSeconds(2))
.messageConverters(new GsonHttpMessageConverter(new GsonBuilder().serializeNulls().create())).build();
}
}
//使用 OpenFeign 并需要日志时添加
@Configuration
public class OpenFeignConfig {
@Bean
Logger.Level openFeignLoggerLevel(){
return Logger.Level.FULL;
}
}
消费方法+ fallback + block 代码
//ribbon 调用
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
@SentinelResource(value = "find",fallback = "getout",blockHandler = "getoutBlock")
public CommonResult<PaymentEntity> find(@PathVariable Long id){
if(3 < id)
throw new IllegalArgumentException("查无此单");
return restTemplate.getForObject(BASE_URL+"/payment/"+id ,CommonResult.class);
}
//OpenFeign 调用
@RequestMapping(value = "/f/{id}", method = RequestMethod.GET)
@SentinelResource(value = "find",fallback = "getout",blockHandler = "getoutBlock")
public CommonResult<PaymentEntity> finds(@PathVariable Long id){
if(3 < id)
throw new IllegalArgumentException("查无此单");
return paymentOpenfeignClient.findById(id);
}
//@Component
@FeignClient(value = "nacos-sentinel-ribbon-sentinel-provider",fallback = PaymentOpenfeignClientFallback.class)
public interface PaymentOpenfeignClient {
@RequestMapping(value = "/payment/{id}", method = RequestMethod.GET)
CommonResult<PaymentEntity> findById(@PathVariable("id") Long id);
}
/* *******************************
* 以下是两种方式通用的 fallback / block
******************************* */
public CommonResult<PaymentEntity> getout(@PathVariable Long id ,Throwable e){
return new CommonResult<PaymentEntity>(500,e.getClass().getName()+" | "+e.getMessage()+" GET OUT!!!");
}
public CommonResult<PaymentEntity> getoutBlock(Long id , BlockException e){
return new CommonResult<PaymentEntity>(500,e.getClass().getName()+" | "+e.getMessage()+" Block OUT!!!");
}
blockhandler 和 fallback
通过下面操作可以实现上述要求的一部分
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
spring:
application:
name: nacos-sentinel-ribbon-sentinel-comsumer
cloud:
nacos:
discovery:
server-addr: 192.168.3.10:8848
sentinel:
transport:
dashboard: 192.168.3.10:8080 #配置Sentinel dashboard地址
port: 8719 #若有占用,它自己+1,再被占用,再+1
datasource:
ds1:
nacos:
server-addr: 192.168.3.10:8848
dataId: nacos-sentinel-ribbon-sentinel-comsumer.flow
groupId: Sentinel
data-type: json
rule-type: FLOW
ds2:
nacos:
server-addr: 192.168.3.10:8848
dataId: nacos-sentinel-ribbon-sentinel-comsumer.degrade
groupId: Sentinel
data-type: json
rule-type: DEGRADE
对于 Sentinel 整合 nacos 的完整需求,Sentinel 的官网 给出了一套基于数据源 datasource 的方案
下面的案例中选用 Push 进行实现和改造(更好的易用性),Push 模式是生产环境普遍选用的方案。
并在此基础上分离定义、注册、参数,简化生效配置
准备
需要依次完成下面的步骤,这些步骤完成一个通用模块
这个 module 完全可以单独提取出去,成为一个独立的 jar 存在,以做到多项目/模块引用
以下代码可以从 https://gitee.com/unfixed/sprcloudlearning.git 获取
增加通用 module: sentinel-nacos-auto
sentinel-nacos-auto 的依赖
<dependencies>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
dependencies>
实现读写数据源
也可以 参考官方代码
import com.alibaba.csp.sentinel.concurrent.NamedThreadFactory;
import com.alibaba.csp.sentinel.datasource.AbstractDataSource;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.log.RecordLog;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import java.util.Properties;
import java.util.concurrent.*;
public class ReadableSentinelNacosDatasource<T> extends AbstractDataSource<String, T> {
private static final int DEFAULT_TIMEOUT = 3000;
//只有一条线程的线程池,线程阻塞时丢弃老任务
private final ExecutorService pool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(1), new NamedThreadFactory("sentinel-nacos-ds-update", true),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final Listener configListener;
protected final String groupId;
protected final String dataId;
private final Properties properties;
protected ConfigService configService;
public ReadableSentinelNacosDatasource(String serverAddr, String groupId, String dataId, Converter<String, T> parser) {
this(buildProperties(serverAddr), groupId, dataId, parser);
}
//创建了一个仅针对 server.groupId.dataId 的读数据源
public ReadableSentinelNacosDatasource(final Properties properties, final String groupId, final String dataId, Converter<String, T> parser) {
super(parser);
this.configService = null;
if (StringUtil.isBlank(groupId) || StringUtil.isBlank(dataId)) {
throw new IllegalArgumentException(String.format("Bad argument: groupId=[%s], dataId=[%s]", groupId, dataId));
}
AssertUtil.notNull(properties, "Nacos properties must not be null, you could put some keys from PropertyKeyConst");
this.groupId = groupId;
this.dataId = dataId;
this.properties = properties;
//通过此监听器与 nacos 相连
this.configListener = new Listener() {
@Override
public Executor getExecutor() {
return pool;
}
@Override
public void receiveConfigInfo(String configInfo) {
RecordLog.info("[ReadableSentinelNacosDatasource] New property value received for (properties: {}) (dataId: {}, groupId: {}): {}"
, properties, dataId, groupId, configInfo);
T newValue = ReadableSentinelNacosDatasource.this.parser.convert(configInfo);
getProperty().updateValue(newValue);
}
};
this.initNacosListener();
this.loadInitialConfig();
}
private void loadInitialConfig() {
try {
T newValue = this.loadConfig();
if (newValue == null) {
RecordLog.warn("[ReadableSentinelNacosDatasource] WARN: initial config is null, you may have to check your data source");
}
this.getProperty().updateValue(newValue);
} catch (Exception e) {
RecordLog.warn("[ReadableSentinelNacosDatasource] Error when loading initial config", e);
}
}
private void initNacosListener() {
try {
this.configService = NacosFactory.createConfigService(this.properties);
this.configService.addListener(this.dataId, this.groupId, this.configListener);
} catch (Exception e) {
RecordLog.warn("[ReadableSentinelNacosDatasource] Error occurred when initializing Nacos data source", e);
}
}
@Override
public String readSource() throws Exception {
if (this.configService == null) {
throw new IllegalStateException("Nacos config service has not been initialized or error occurred");
} else {
return this.configService.getConfig(this.dataId, this.groupId, DEFAULT_TIMEOUT);
}
}
@Override
public void close() {
if (this.configService != null) {
this.configService.removeListener(this.dataId, this.groupId, this.configListener);
}
this.pool.shutdownNow();
}
private static Properties buildProperties(String serverAddr) {
Properties properties = new Properties();
properties.setProperty("serverAddr", serverAddr);
return properties;
}
}
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.datasource.WritableDataSource;
import com.fc.sprcloudlearning.sentinelnacos.datasource.ReadableSentinelNacosDatasource;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class WritableSentineNacoslDatasource<T> extends ReadableSentinelNacosDatasource implements WritableDataSource<T> {
private final Lock lock = new ReentrantLock(true);
private final Converter<T, String> configEncoder;
public WritableSentineNacoslDatasource(String serverAddr, String groupId, String dataId, Converter<T, String> configEncoder) {
super(serverAddr, groupId, dataId, configEncoder);
this.configEncoder = configEncoder;
}
@Override
public void write(T value) throws Exception {
lock.lock();
try {
String convertResult = configEncoder.convert(value);
configService.publishConfig(dataId, groupId, convertResult);
} finally {
lock.unlock();
}
}
@Override
public void close() {
super.close();
}
}
实现数据源注册
import com.alibaba.csp.sentinel.command.handler.ModifyParamFlowRulesCommandHandler;
import com.alibaba.csp.sentinel.datasource.ReadableDataSource;
import com.alibaba.csp.sentinel.datasource.WritableDataSource;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.alibaba.csp.sentinel.slots.system.SystemRule;
import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;
import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.fc.sprcloudlearning.sentinelnacos.config.SentinelNacosDatasourceConfig;
import com.fc.sprcloudlearning.sentinelnacos.datasource.ReadableSentinelNacosDatasource;
import com.fc.sprcloudlearning.sentinelnacos.datasource.WritableSentineNacoslDatasource;
import java.util.List;
public class Sentinel4NacosInitor implements InitFunc {
private final SentinelNacosDatasourceConfig config;
public Sentinel4NacosInitor(SentinelNacosDatasourceConfig config) {
this.config = config;
init();
}
@Override
public void init() {
//流控规则
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource =
new ReadableSentinelNacosDatasource<>(config.server(), config.groupId(), config.flow(),
source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {}));
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
WritableDataSource<List<FlowRule>> writableFlowRuleDataSource =
new WritableSentineNacoslDatasource<>(config.server(), config.groupId(), config.flow(), JSON::toJSONString);
WritableDataSourceRegistry.registerFlowDataSource(writableFlowRuleDataSource);
//降级规则
ReadableDataSource<String, List<DegradeRule>> degradeRuleDataSource =
new ReadableSentinelNacosDatasource<>(config.server(), config.groupId(), config.degrade(),
source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {}));
DegradeRuleManager.register2Property(degradeRuleDataSource.getProperty());
WritableDataSource<List<DegradeRule>> writableDegradeRuleDataSource =
new WritableSentineNacoslDatasource<>(config.server(), config.groupId(), config.degrade(), JSON::toJSONString);
WritableDataSourceRegistry.registerDegradeDataSource(writableDegradeRuleDataSource);
//系统规则
ReadableDataSource<String, List<SystemRule>> systemRuleDataSource =
new ReadableSentinelNacosDatasource<>(config.server(), config.groupId(), config.system(),
source -> JSON.parseObject(source, new TypeReference<List<SystemRule>>() {
}));
SystemRuleManager.register2Property(systemRuleDataSource.getProperty());
WritableDataSource<List<SystemRule>> writableSystemRuleDataSource =
new WritableSentineNacoslDatasource<>(config.server(), config.groupId(), config.system(), JSON::toJSONString);
WritableDataSourceRegistry.registerSystemDataSource(writableSystemRuleDataSource);
//授权规则
ReadableDataSource<String, List<AuthorityRule>> authorityRuleDataSource =
new ReadableSentinelNacosDatasource<>(config.server(), config.groupId(), config.authority(),
source -> JSON.parseObject(source, new TypeReference<List<AuthorityRule>>() {}));
AuthorityRuleManager.register2Property(authorityRuleDataSource.getProperty());
WritableDataSource<List<AuthorityRule>> writableAuthorityRuleDataSource =
new WritableSentineNacoslDatasource<>(config.server(), config.groupId(), config.authority(), JSON::toJSONString);
WritableDataSourceRegistry.registerAuthorityDataSource(writableAuthorityRuleDataSource);
//热点参数规则
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleDataSource =
new ReadableSentinelNacosDatasource<>(config.server(), config.groupId(), config.param(),
source -> JSON.parseObject(source, new TypeReference<List<ParamFlowRule>>() {
}));
ParamFlowRuleManager.register2Property(paramFlowRuleDataSource.getProperty());
WritableDataSource<List<ParamFlowRule>> writableParamFlowRuleDataSource =
new WritableSentineNacoslDatasource<>(config.server(), config.groupId(), config.param(), JSON::toJSONString);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(writableParamFlowRuleDataSource);
}
}
实现配置
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "config.sentinel.datasource.nacos")
public class SentinelNacosDatasourceConfig {
private String server;
private String groupId;
private String dataId;
private String flowSuffix = "flow";
private String authoritySuffix = "authority";
private String degradeSuffix = "degrade";
private String paramSuffix = "param";
private String systemSuffix = "system";
private String separator = ".";
/* *******************************
* 以下是 setter/getter
******************************* */
public String getServer() {
return server;
}
public void setServer(String server) {
this.server = server;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getDataId() {
return dataId;
}
public void setDataId(String dataId) {
this.dataId = dataId;
}
public String getFlowSuffix() {
return flowSuffix;
}
public void setFlowSuffix(String flowSuffix) {
this.flowSuffix = flowSuffix;
}
public String getAuthoritySuffix() {
return authoritySuffix;
}
public void setAuthoritySuffix(String authoritySuffix) {
this.authoritySuffix = authoritySuffix;
}
public String getDegradeSuffix() {
return degradeSuffix;
}
public void setDegradeSuffix(String degradeSuffix) {
this.degradeSuffix = degradeSuffix;
}
public String getParamSuffix() {
return paramSuffix;
}
public void setParamSuffix(String paramSuffix) {
this.paramSuffix = paramSuffix;
}
public String getSystemSuffix() {
return systemSuffix;
}
public void setSystemSuffix(String systemSuffix) {
this.systemSuffix = systemSuffix;
}
public String getSeparator() {
return separator;
}
public void setSeparator(String separator) {
this.separator = separator;
}
public String server() {
return server;
}
public String groupId() {
return groupId;
}
public String dataId() {
return dataId;
}
public String flow() {
return StringUtils.join(dataId, separator, flowSuffix);
}
public String authority() {
return StringUtils.join(dataId, separator, authoritySuffix);
}
public String degrade() {
return StringUtils.join(dataId, separator, degradeSuffix);
}
public String param() {
return StringUtils.join(dataId, separator, paramSuffix);
}
public String system() {
return StringUtils.join(dataId, separator, systemSuffix);
}
}
import com.alibaba.csp.sentinel.init.InitFunc;
import com.fc.sprcloudlearning.sentinelnacos.Sentinel4NacosInitor;
import org.springframework.context.annotation.Bean;
public class Sentinel4NacosInitorConfig {
@Bean
public InitFunc sentinel4NacosInitor(SentinelNacosDatasourceConfig config){
return new Sentinel4NacosInitor(config);
}
}
实现@Enable
import com.fc.sprcloudlearning.sentinelnacos.config.Sentinel4NacosInitorConfig;
import com.fc.sprcloudlearning.sentinelnacos.config.SentinelNacosDatasourceConfig;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({SentinelNacosDatasourceConfig.class,
Sentinel4NacosInitorConfig.class})
public @interface EnableSentinel4Nacos {
}
使用
经过上面准备,下面的使用在手感上接近,用 sentinel-nacos-auto 平替此依赖即可(配置还是要改的)
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
引入模块 sentinel-nacos-auto
<dependency>
<groupId>com.fc.sprcloudlearninggroupId>
<artifactId>sentinel-nacos-autoartifactId>
<version>${project.version}version>
dependency>
启动类注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableSentinel4Nacos //这里
public class OrderComsummerApplication {
public static void main(String[] args) {
SpringApplication.run(OrderComsummerApplication.class,args);
}
}
配置
以下为最简配置,使用下面配置后,会自动生成 5 个 dataId
每个完整 dataId = {dataId}{separator}{rule-type-suffix},如 appName.flow
{separator} 默认值为 .
可以通过 config.sentinel.datasource.nacos.separator 进行修改
{rule-type-suffix} 默认为 flow / authority / degrade / param / system
可以通过如 config.sentinel.datasource.nacos.flow-suffix 配置进行修改
config:
sentinel:
datasource:
nacos:
server: http://192.168.3.10:8848
group-id: Sentinel
data-id: nacos-sentinel-ribbon-sentinel-comsumer
效果
已有规则可以自动拉取
规则可生效
sentinel dashboard 的操作自动同步 nacos
进阶优化方案
可以在 @EnableSentinel4Nacos 注解中增加参数 server / groudId / dataId,以实现最简配置场景的无配置化
对应的,需要增加从启动类上获取 注解以及获取参数的逻辑,此逻辑可能可以通过 Environment 自动完成,此时需要额外补充填充逻辑
本文部分内容参考自
sentinel的限流操作:快速失败、Warm UP、排队等待
实战流控效果-WarmUp
生产环境下sentinel规则持久化方案
传送门:
微服务架构 | 组件目录