官方Github
官方文档
什么是Sentinel?
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如:
1.引入依赖
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-coreartifactId>
<version>1.8.0version>
dependency>
2.编写测试类
@RestController
@Slf4j
public class HelloController {
private static final String RESOURCE_NAME = "hello";
@RequestMapping("/hello")
public String hello(){
Entry entry = null;
// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串
try {
entry = SphU.entry(RESOURCE_NAME);
}catch (BlockException e1){
// 资源访问阻止,被限流或被降级 进行相应的处理操作
log.info("block异常");
}catch (Exception ex){
//如果需要配置降级规则,需要通过这种方式记录业务异常
//Tracer.traceEntry(ex,entry);
}
finally {
if(entry!=null){
entry.exit();
}
}
return "hello,sentinel";
}
//定义流控规则
@PostConstruct
private static void init(){
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource(RESOURCE_NAME);
// 设置流控规则 QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 设置受保护的资源阈值
rule.setCount(1);
rules.add(rule);
//加载配置好的规则
FlowRuleManager.loadRules(rules);
}
}
测试效果:
1.引入依赖
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-annotation-aspectjartifactId>
<version>1.8.0version>
dependency>
2.编写测试类
@RequestMapping("/findOrderByUserId/{id}")
@SentinelResource(value = "findOrderByUserId",fallback = "fallBack",fallbackClass = ExceptionUtil.class,
blockHandler = "blockBack",blockHandlerClass = ExceptionUtil.class)
public R findOrderByUserId(@PathVariable("id") Integer id){
String url = "http://mall-order/order/findOrderByUserId/"+id;
R result = restTemplate.getForObject(url,R.class);
if(id==4){
throw new RuntimeException("非法参数异常");
}
return result;
}
3.编写降级异常处理类
public class ExceptionUtil {
public static R fallBack(Integer id,Throwable e){
return R.error(-50001,"服务内部异常,请稍后再试");
}
public static R blockBack(Integer id, BlockException e){
return R.error(-50001,"服务繁忙,请稍后再试");
}
}
4.通过Sentinel dashboard配置规则
需要引入通信的包
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel‐transport‐simple‐httpartifactId>
<version>1.8.0version>
dependency>
4.启动Sentinel 服务端可视化界面
Sentinel 服务端是一个springboot项目
D:\environment\sentinel>java -jar sentinel-dashboard-1.8.3.jar
访问Sentinel Dashboard 输入账户密码 sentinel 即可进入控制台
5.配置规则
6.测试
1.引入依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
2.yaml配置
server:
port: 7001
spring:
application:
name: sentinel-user
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
ephemeral: false
sentinel:
transport:
dashboard: 127.0.0.1:8080 #控制台地址
#暴露端点
management:
endpoints:
web:
exposure:
include: '*'
3.开启Sentinel控制台
4.访问服务下的任一接口
Sentinel 提供一个轻量级的开源控制台,它提供机器发现以及健康情况管理、监控(单机和集群),规则 管理和推送的功能。
Sentinel 控制台包含如下功能:
查看机器列表以及健康情况:收集 Sentinel 客户端发送的心跳包,用于判断机器是否在线。
监控 (单机和集群聚合):通过 Sentinel 客户端暴露的监控 API,定期拉取并且聚合应用监控信 息,最终可以实现秒级的实时监控。
规则管理和推送:统一管理推送规则。
生产环境中鉴权非常重要。这里每个开发者需要根据自己的实际情况进行定制。
监控接口通过的QPS和拒绝的QPS
用来显示微服务的所监控的API
针对QPS或者线程数可以选择不同的流控模式进行链接流控
再时间窗口期内针对不同的熔断策略(慢调用比例、异常比例、异常数)进行熔断,进而保护服务的稳定性。
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用 进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平 均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控 制(黑白名单控制)的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请通过。
为什么要使用集群流控呢?假设我们希望给某个用户限制调用某个 API 的总 QPS 为 50,但机器数可能很 多(比如有 100 台)。这时候我们很自然地就想到,找一个 server 来专门来统计总的调用量,其它的实例 都与这台 server 通信来判断是否可以调用。这就是最基础的集群流控的方式。 另外集群流控还可以解决流量不均匀导致总体限流效果不佳的问题。假设集群中有 10 台机器,我们给每台 机器设置单机限流阈值为 10 QPS,理想情况下整个集群的限流阈值就为 100 QPS。不过实际情况下流量 到每台机器可能会不均匀,会导致总量没有到的情况下某些机器就开始限流。因此仅靠单机维度去限制的话会无法精确地限制总体流量。而集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果
和Sentinel控制台保持通信的微服务机器列表
因为Sentinel配置好规则后,如果触发了规则,都会抛出BlockException异常,Sentinel通过控制台配置规则作用于客户端是通过拦截器实现的,入口类是AbstractSentinelInterceptor,在preHandle方法中对目标方法进行了拦截器并进行规则校验,通过异常捕获,对微服务做出降级处理,对异常的处理是BlockExceptionHandler的实现类 ,我们这里自定义一个BlockExceptionHandler
//处理规则降级的统一异常类
@Slf4j
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
R r = null;
if(e instanceof FlowException){
r = R.error(100001,"接口已被限流");
}
if(e instanceof DegradeException){
r = R.error(100002,"服务已被降级");
}
if(e instanceof ParamFlowException){
r = R.error(100003,"热点参数被限流");
}
if(e instanceof SystemBlockException){
r = R.error(100004,"触发系统保护规则");
}
if(e instanceof AuthorityException){
r = R.error(100004,"未被授权,请稍后再试");
}
response.setStatus(500);
response.setCharacterEncoding("utf-8");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getWriter(),r);
}
}
流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流 量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。同一个资源可以创建多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流 或者所有规则遍历完毕。一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果.具体配置如下图:
对应字段说明:
流量控制主要有两种统计类型,一种是统计并发线程数,另外一种则是统计 QPS。类型由 FlowRule 的 grade 字段来定义。其中,0 代表根据并发数量来限流,1 代表根据 QPS 来进行流量控制。
QPS(Query Per Second):每秒请求数,就是说服务器在一秒的时间内处理了多少个请求。
配置:
效果:
并发数控制用于保护业务线程池不被慢调用耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务
不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致
线程池耗尽。为应对太多线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。这种隔离方案虽然隔离性比较好,但是代价就是线程 数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。
Sentinel 并发控 制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超 出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置。
简而言之:就是配置处理这个请求的线程数量,如果超过阈值,新的请求被拒绝。
配置:
说明:线程数量需要通过jmeter工具来压测出效果,我这边设置了5秒60个线程访问,循环10次,肯定超过每秒2个线程的阈值
效果:
基于调用关系的流量控制。调用关系包括调用方、被调用方;一个方法可能会调用其它方法,形成一个调用链路的层次关系。
资源调用达到设置的阈值后直接被流控抛出异常
当两个业务有关联的时候,比如业务A和业务B服务,对业务A进行关联流控,关联资源为业务B,当业务B到达设置的阈值的时候,此时访问A将请求失败。
配置:
通过jmeter在5s内发送600个请求到/user/info
此时访问/findOrderByUserId/{id}已经被限流
根据调用链路入口限流。
NodeSelectorSlot 中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵
树的根节点是一个名字为 machine-root 的虚拟节点,调用链的入口都是这个虚节点的子节点。
一棵典型的调用树如下图所示:
上图中来自入口 Entrance1 和 Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许只根据某个入口的统计 信息对资源限流。
具体案例:test3/test4都调用getUser资源,当test3调用超过阈值的时候,请求被拒绝,但是test4仍然可以正常访问。
controller:
test3/test4都调用了service中的getUser方法
@RequestMapping(value = "/test3") //CommonFilter
public UserEntity test3() {
UserEntity user = userService.getUser(1);
return user;
}
@RequestMapping(value = "/test4")
public UserEntity test4() {
UserEntity user = userService.getUser(1);
return user;
}
service:
@Override
@SentinelResource("getUser") //资源注册到sentinel中
public UserEntity getUser(Integer id) {
UserEntity userInfo = new UserEntity();
userInfo.setId(1);
userInfo.setAge(18);
userInfo.setUsername("qianyue");
return userInfo;
}
此时访问test/test4后查看sentinel控制台,只有test3下有getUser4
配置getUser:
效果:无论test3刷的多快,都不会被限流,这是sentinel高版本的问题,下边看看如何解决。
导致链路流控不生效原因:
从1.6.3版本开始,Sentinel Web filter默认收敛所有URL的入口context,导致链路限流不生效。
解决方法:
从1.7.0版本开始,官方在CommonFilter引入了WEB_CONTEXT_UNIFY参数,用于控制是否收敛context,将其配置
为false即可根据不同的URL进行链路限流。
(1)1.8.0 需要引入sentinelwebservlet依赖
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-web-servletartifactId>
dependency>
(2)编写配置类
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
// 入口资源关闭聚合 解决流控链路不生效的问题
registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
registration.setName("sentinelFilter");
registration.setOrder(1);
return registration;
}
(3)重启访问test3/test4查看效果
(4)效果展示 快速访问/test3
引发新的问题:没有调用我们自定义的异常处理,而是服务的报错页面。再看看控制台:
原因分析:
1.Sentinel流控规则的处理核心是 FlowSlot, 对getUser资源进行了限流保护,当请求QPS超过阈值2的时
候,就会触发流控规则抛出FlowException异常
3.解决方案:在@SentinelResource注解中指定blockHandler处理BlockException)
@Override
@SentinelResource(value = "getUser",blockHandler = "handleException") //资源注册到sentinel中
public UserEntity getUser(Integer id) {
UserEntity userInfo = new UserEntity();
userInfo.setId(1);
userInfo.setAge(18);
userInfo.setUsername("qianyue");
return userInfo;
}
public UserEntity handleException(Integer id, BlockException ex) {
UserEntity userEntity = new UserEntity();
userEntity.setUsername("自定义限流处理。");
return userEntity;
}
效果展示:
但是我们还是不清楚为什么抛出FlowException还是出现了RuntimeException异常,我们假如没有添加handleException方法处理异常接下往下分析。
当我们没有对FlowException进行处理的时候,通过注解的方式会生成动态代理,AOP将对对异常进行处理,具体代码在CglibAopProxy.proceed
异常再向上抛出,会来到CommonFilter类中,进而抛出一个500的错误页面
上边可以看到,对BlockException进行捕获,需要UrlBlockHandler处理,我们实验一下
添加MyUrlBlockHandler
public class MyUrlBlockHandler implements UrlBlockHandler {
@Override
public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws IOException {
R r = null;
if(e instanceof FlowException){
r = R.error(100001,"接口已被限流");
}
if(e instanceof DegradeException){
r = R.error(100002,"服务已被降级");
}
if(e instanceof ParamFlowException){
r = R.error(100003,"热点参数被限流");
}
if(e instanceof SystemBlockException){
r = R.error(100004,"触发系统保护规则");
}
if(e instanceof AuthorityException){
r = R.error(100004,"未被授权,请稍后再试");
}
httpServletResponse.setStatus(500);
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(httpServletResponse.getWriter(),r);
}
}
注册到CommonFilter
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
// 入口资源关闭聚合 解决流控链路不生效的问题
registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
registration.setName("sentinelFilter");
registration.setOrder(1);
//CommonFilter的BlockException自定义处理逻辑
WebCallbackManager.setUrlBlockHandler(new MyUrlBlockHandler());
//解决授权规则不生效的问题
//com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser
//WebCallbackManager.setRequestOriginParser(new MyRequestOriginParser());
return registration;
}
测试:
发现还是不行
总结:@SentinelResource指定的资源必须在 @SentinelResource注解中指定blockHandler处理BlockException
当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的效果包括以下几种:快速失败(直接 拒绝)、Warm Up(预热)、匀速排队(排队等待)。
默认的流量控制方式,当QPS超过任意规则的阈值后,新 的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况 下,比如通过压测确定了系统的准确水位时。
当系统长期处于低水位的情
况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓 慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。 冷加载因子: codeFactor 默认是3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS阈值。
通常冷启动的过程系统允许通过的 QPS 曲线如下图所示
代码:
@RequestMapping(value = "/testWarmUp")
public String testWarmUp(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "慢慢预热";
}
配置:
效果:
刚开始只能通过3个请求,后边慢慢放开阀门,达到10个请求。
匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。 该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求 到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
注意:匀速排队模式暂时不支持 QPS > 1000 的场景
代码:
@RequestMapping(value = "/testLineUp")
public String testLineUp(){
return "匀速排队";
}
配置:
jmeter压测:10s 100个请求
效果:
设置的等待时间越长,拒绝的请求越少。
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。我们 需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。降级规则有三种策略,分别是慢调用比例、异常比例和异常数。
慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的 响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔
断时长后熔断器会进入探测恢复状态(HALFOPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
代码:
@RequestMapping(value = "/testRT")
public String testRT(){
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
return "测试慢调用比例";
}
配置:
jmeter压测;
效果:
先熔断,后放开。
异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并 且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢 复状态(HALFOPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔 断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
代码:
AtomicInteger atomicInteger = new AtomicInteger(0);
@RequestMapping("/testExceptionBL")
public String testExceptionBL(){
atomicInteger.getAndIncrement();
if(atomicInteger.get()%2==0){
int i=1/0;
}
return "测试异常比例";
}
配置:
jmeter:
效果:
当统计周期内达到异常比例开始熔断,随后测试一个接口能通过则关闭断路器。
异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后 熔断器会进入探测恢复状态(HALFOPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
注意:异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。
代码:
@RequestMapping("/testExceptionCount")
public String testExceptionCount(){
atomicInteger.getAndIncrement();
if(atomicInteger.get()%2==0){
System.out.println(atomicInteger.get());
int i=1/0;
}
return "测试异常数量";
}
配置:
效果:
1s内到达异常数,进行熔断,3s后半开断路器测试请求是否正常,正常则关闭断路器.
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,
并对其访问进行限制。比如:
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用 进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
注意:
热点规则需要使用@SentinelResource(“resourceName”)注解,否则不生效
参数必须是7种基本数据类型才会生效
代码:
测试方法
@RequestMapping("/testHotKey/{id}")
@SentinelResource(value = "testHotKey",blockHandlerClass = CommonBlockHandler.class,blockHandler = "handleException2",fallbackClass = CommonFallback.class,fallback = "fallback")
public String testHotKey(@PathVariable("id") Integer id){
return "测试热点key"+id;
}
公共兜底方法
public class CommonBlockHandler {
/**
* 注意: 必须为 static 函数 多个方法之间方法名不能一样
* @param exception
* @return
*/
public static String handleException(Map<String, Object> params, BlockException exception){
return "被限流啦";
}
public static String handleException2(Integer id, BlockException exception){
return "被限流啦";
}
public static String handleException3(BlockException exception){
return "===被限流啦==="+exception;
}
}
tip:兜底方法中需要满足
1.参数类型要和原方法保持一致
2.返回值类型要和原方法保持一致
3.参数多一个BlockException
4.方法是静态的。
配置:
也可以指定特殊的值的阈值为多少
效果:
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平 均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系 统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
Load 自适应(仅对 Linux/Unixlike 机器生效):系统的 load1 作为启发指标,进行自适应系统 保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系 统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0 —1.0),比较灵敏。
平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
代码:
@RequestMapping("/testSysRule")
public String testSysRule(){
return "测试系统规则";
}
配置:
jmeter:
效果:
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控 制(黑白名单控制)的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过
这里有点小坑1.8和1.7是不一样的
1.8版本:
1.编写RequestOriginParser实现类
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
//RequestOriginParser是com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser包下的 不要导错。
@Component
public class MyRequestOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
String origin = request.getParameter("serviceName");
// if (StringUtil.isBlank(origin)){
// throw new IllegalArgumentException("serviceName参数未指定");
// }
return origin;
}
}
2.配置
3.访问路径中携带请求参数,然后会被拦截器校验
127.0.0.1:7001/user/test3/?serviceName=order
4.效果
1.7版本如果导入了
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-web-servletartifactId>
dependency>
1.编写MyRequestOriginParser
导入com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser下的RequestOriginParser
import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser;
import javax.servlet.http.HttpServletRequest;
public class MyRequestOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
String origin = request.getParameter("serviceName");
// if (StringUtil.isBlank(origin)){
// throw new IllegalArgumentException("serviceName参数未指定");
// }
return origin;
}
}
2.配置类修改
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
// 入口资源关闭聚合 解决流控链路不生效的问题
registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
registration.setName("sentinelFilter");
registration.setOrder(1);
//CommonFilter的BlockException自定义处理逻辑
WebCallbackManager.setUrlBlockHandler(new MyUrlBlockHandler());
//解决授权规则不生效的问题
//com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser
WebCallbackManager.setRequestOriginParser(new MyRequestOriginParser());
return registration;
}
3.配置
4.测试
1.引入依赖
<dependencies>
<dependency>
<groupId>com.qianyue.mallgroupId>
<artifactId>mall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
<scope>compilescope>
dependency>
<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>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
dependencies>
2.yaml配置
server:
port: 8501
spring:
application:
name: mall-user-sentinel-ribbon-demo #微服务名称
#配置nacos注册中心地址
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
# 添加sentinel的控制台地址
dashboard: 127.0.0.1:8080
# 指定应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用的HttpServer
#port: 8719
#暴露actuator端点 http://localhost:8800/actuator/sentinel
management:
endpoints:
web:
exposure:
include: '*'
#true开启sentinel对resttemplate的支持,false则关闭 默认true
resttemplate:
sentinel:
enabled: true
3.编写异常处理类
public class ExceptionUtil {
public static R fallback(Integer id, Throwable e){
return R.error(-102,"=========被异常降级啦===");
}
public static R handleException(Integer id, BlockException e){
return R.error(-101,"==========被限流啦===");
}
}
4.编写配置类
@Configuration
public class SpringConfig {
@Bean
@LoadBalanced //拦截器
@SentinelRestTemplate(
fallbackClass = GlobalExceptionUtil.class,fallback = "fallback",
blockHandler = "handleException" ,blockHandlerClass = GlobalExceptionUtil.class
)
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
5.编写业务代码
@RequestMapping(value = "/findOrderByUserId/{id}")
public R findOrderByUserId(@PathVariable("id") Integer id) {
String url = "http://mall-order/order/findOrderByUserId/"+id;
R result = restTemplate.getForObject(url,R.class);
if(id==5){
throw new RuntimeException("非法参数");
}
return result;
}
5.测试
tip:这里有一个坑,不知道是官网bug还是设计如此,就是我们系统异常的降级方法不会执行,比如我在客户端设置参数=4抛异常,在服务端参数=5抛异常,都没有执行系统异常的降级方法fallback。如图:
经过打断点调试:
1.本地方法制造异常 SentinelProtectInterceptor拦截器只拦截你远程调用的代码,你调用之后和之前的代码不在拦截范围内,抛的异常是通过SpringMVC处理的。
2.远程方法异常,走的this.restTemplate.getErrorHandler().hasError(response)逻辑,也不会进入catch方法。可能继续往下走其他拦截器逻辑。
if (this.restTemplate.getErrorHandler().hasError(response)) {
Tracer.trace(new IllegalStateException("RestTemplate ErrorHandler has error"));
}
因此,fallback方法不会执行。
Sentinel本身已经整合了OpenFeign组件,如下图
Feign接口一定会生成代理对象,会在InvocationHandler的intercept方法中对降级进行处理
有了源码分析,我们就可以对openFigen方式进行降级处理了。
1.引入依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
2.yaml配置
feign:
sentinel:
enabled: true #开启sentinel对feign的支持 默认false
3.主启动类开启openFeign功能
@EnableFeignClients
public class SentinelRibbonApplication8501
4.编写OpenFeign接口
@FeignClient(value = "mall-order",path = "/order",fallbackFactory = FallbackOrderFeignServiceFactory.class)
public interface OrderFeignService {
@RequestMapping("/findOrderByUserId/{userId}")
public R findOrderByUserId(@PathVariable("userId") Integer userId);
}
5.编写FallbackOrderFeignServiceFactory实现类实现降级逻辑
@Component
public class FallbackOrderFeignServiceFactory implements FallbackFactory<OrderFeignService> {
@Override
public OrderFeignService create(Throwable throwable) {
return new OrderFeignService() {
@Override
public R findOrderByUserId(Integer userId) {
if (throwable instanceof FlowException) {
return R.error(100,"接口限流了");
}
return R.error(-1,"=======服务降级了========");
}
};
}
}
6.测试
关闭提供者服务或者提供者服务有异常都会质朴降级逻辑
添加流控规则后,违法流控规则走限流逻辑
这里基于源码去讲解Sentinel做规则持久化的扩展点。
Sentinel推送规则有三种方法:
如果不做任何修改,Dashboard 的推送规则方式是通过 API 将规则推送至客户端并直接更新到内存中:
这种做法的好处是简单,无依赖;坏处是应用重启规则就会消失,仅用于简单测试,不能用于生产环境。
pull 模式的数据源(如本地文件、RDBMS 等)一般是可写入的。使用时需要在客户端注册数据源:将对应的读数据源注册至对应的 RuleManager,将写数据源注册至 transport 的 WritableDataSourceRegistry 中
首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文 件中。使用 pull 模式的数据源时一般不需要对 Sentinel 控制台进行改造。这种实现方法好处是简单,坏处是无法保证监控数据的一致性。
从上图可以看出拉模式在控制台添加规则后通过通信模块把规则推送给客户端,客户端只需要监听通信端口,把规则更新到本地文件和内存中。控制台和客户端是怎么通信的呢?我们先分析一波控制台的源码
1.当我们控制台添加规则后,请求来到FlowControllerV1的apiAddFlowRule方法,很明显publishRules就是用于推送我们的规则
2.publishRules方法中,先通过服务名称、机器ip和端口获取本地内存中的规则然后通过http请求把规则推送到客户端
3.客户端CommandCenter的实现类SimpleHttpCommandCenter的start方法会在客户端启动是时候调用
4.在SimpleHttpCommandCenter的start的方法中,开启一个线程创建监听通信端口的ServerSocket
5.在这个线程中又开启了一个线程处理服务端推送的规则
6.在ServerThread线程中创建一个事件线程HttpEventTask 专门处理服务端推送的规则
7.HttpEventTask是专门处理我们收到的请求报文。在任务中会寻找一个可以处理当前请求的commandHandler对我们的请求进行处理
8.处理流控规则的是ModifyRulesCommandHandler,在handle方法中会把服务端推送的规则加载到内存中
9.注意看的是下边,我们持久化的扩展点
getFlowDataSource()是判断是否有一个数据源进行持久化,如果没有则不进行持久化。如果有则调用writeToDataSource进行持久化。
10.flowDataSource其实是WritableDataSource类型的,Sentinel其实已经有一个数据源WritableDataSource,在扩展包中没有引入进来。一个是读,一个是写
官方也给了使用的demo,在Sentinel源码的sentineldemo/sentineldemodynamicfilerule模块下,想了解的同学可以自行观看官方demo,这里是参考官方demo实现了客户端持久化。
11.现在思考一个问题,我们发现了持久化的扩展点,但是如何把扩展到应用到我们的springcloud应用中呢?其实Sentinel已经给了我们的扩展点,利用SPI机制导入。Sentinel是通过拦截器方式对我们的请求进行流控降级,入口类是AbstractSentinelInterceptor,我们看下preHandle方法
12.进入entry方法
13.我们看一下Env这个类,这个类是我们Sentinel初始化类,进行了Sentinel的初始化工作
14.在doInit方法中,通过SPI机制加载所有InitFunc接口的实现类,并调用init方法。我们刚刚处理服务端推送配置请求的CommandCenter也是通过这种机制引入的
12.所以这里我们可以写一个扩展的模块,进行持久化的扩展。先看下持久化的核心代码。
// FileRefreshableDataSource 会周期性的读取文件以获取规则,当文件有更新时会及时发现,并将规则更新到内存中。
FileRefreshableDataSource<List<FlowRule>> flowRuleDataSource = new FileRefreshableDataSource<>(
flowRulePath, flowRuleListParser);
// 将可读数据源注册至 FlowRuleManager.
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());
//
ReadableDataSource<String, List<FlowRule>> ds = new FileRefreshableDataSource<>(
flowRulePath, source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {})
);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写 入到文件中
FlowRuleManager.register2Property(ds.getProperty());
13.新建一个扩展模块sentinel-datasource-extension-file-pull,导入相关依赖
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sentinel-extensionartifactId>
<groupId>com.alibaba.cspgroupId>
<version>1.8.0version>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>sentinel-datasource-extension-file-pullartifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-coreartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-extensionartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-transport-simple-httpartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-parameter-flow-controlartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-source-pluginartifactId>
<version>3.0.1version>
<executions>
<execution>
<id>attach-sourcesid>
<goals>
<goal>jargoal>
goals>
execution>
executions>
plugin>
plugins>
build>
project>
14.创建持久化代码
规则列表解析工具类RuleListConverterUtils
public class RuleListConverterUtils {
public static final Converter<String, List<FlowRule>> flowRuleListParser = new Converter<String, List<FlowRule>>() {
@Override
public List<FlowRule> convert(String source) {
return JSON.parseObject(source, new TypeReference<List<FlowRule>>() {});
}
};
public static final Converter<String,List<DegradeRule>> degradeRuleListParse = new Converter<String, List<DegradeRule>>() {
@Override
public List<DegradeRule> convert(String source) {
return JSON.parseObject(source,new TypeReference<List<DegradeRule>>(){});
}
};
public static final Converter<String,List<SystemRule>> sysRuleListParse = new Converter<String, List<SystemRule>>() {
@Override
public List<SystemRule> convert(String source) {
return JSON.parseObject(source,new TypeReference<List<SystemRule>>(){});
}
};
public static final Converter<String,List<ParamFlowRule>> paramFlowRuleListParse = new Converter<String, List<ParamFlowRule>>() {
@Override
public List<ParamFlowRule> convert(String source) {
return JSON.parseObject(source,new TypeReference<List<ParamFlowRule>>(){});
}
};
public static final Converter<String,List<AuthorityRule>> authorityRuleParse = new Converter<String, List<AuthorityRule>>() {
@Override
public List<AuthorityRule> convert(String source) {
return JSON.parseObject(source,new TypeReference<List<AuthorityRule>>(){});
}
};
public static final Converter<List<FlowRule>,String> flowFuleEnCoding= new Converter<List<FlowRule>,String>() {
@Override
public String convert(List<FlowRule> source) {
return JSON.toJSONString(source);
}
};
public static final Converter<List<SystemRule>,String> sysRuleEnCoding= new Converter<List<SystemRule>,String>() {
@Override
public String convert(List<SystemRule> source) {
return JSON.toJSONString(source);
}
};
public static final Converter<List<DegradeRule>,String> degradeRuleEnCoding= new Converter<List<DegradeRule>,String>() {
@Override
public String convert(List<DegradeRule> source) {
return JSON.toJSONString(source);
}
};
public static final Converter<List<ParamFlowRule>,String> paramRuleEnCoding= new Converter<List<ParamFlowRule>,String>() {
@Override
public String convert(List<ParamFlowRule> source) {
return JSON.toJSONString(source);
}
};
public static final Converter<List<AuthorityRule>,String> authorityEncoding= new Converter<List<AuthorityRule>,String>() {
@Override
public String convert(List<AuthorityRule> source) {
return JSON.toJSONString(source);
}
};
}
规则持久化目录和文件的工具类RuleFileUtils
public class RuleFileUtils {
public static void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if(!file.exists()) {
file.mkdirs();
}
}
public static void createFileIfNotExits(Map<String,String> ruleFileMap) throws IOException {
Set<String> ruleFilePathSet = ruleFileMap.keySet();
Iterator<String> ruleFilePathIter = ruleFilePathSet.iterator();
while (ruleFilePathIter.hasNext()) {
String ruleFilePathKey = ruleFilePathIter.next();
String ruleFilePath = PersistenceRuleConstant.rulesMap.get(ruleFilePathKey).toString();
File ruleFile = new File(ruleFilePath);
if(!ruleFile.exists()) {
ruleFile.createNewFile();
}
}
}
}
规则持久化常量类
/**
* Sentinel 规则持久化 常量配置类
*/
public class PersistenceRuleConstant {
/**
* 存储文件路径
*/
public static final String storePath = System.getProperty("user.home") + File.separator + "sentinel" + File.separator + "rules";
/**
* 各种存储sentinel规则映射map
*/
public static final Map rulesMap = new HashMap<String,String>();
//流控规则文件
public static final String FLOW_RULE_PATH = "flowRulePath";
//降级规则文件
public static final String DEGRAGE_RULE_PATH = "degradeRulePath";
//授权规则文件
public static final String AUTH_RULE_PATH = "authRulePath";
//系统规则文件
public static final String SYSTEM_RULE_PATH = "systemRulePath";
//热点参数文件
public static final String HOT_PARAM_RULE = "hotParamRulePath";
static {
rulesMap.put(FLOW_RULE_PATH,storePath+ File.separator +"flowRule.json");
rulesMap.put(DEGRAGE_RULE_PATH,storePath+File.separator +"degradeRule.json");
rulesMap.put(SYSTEM_RULE_PATH,storePath+File.separator +"systemRule.json");
rulesMap.put(AUTH_RULE_PATH,storePath+File.separator +"authRule.json");
rulesMap.put(HOT_PARAM_RULE,storePath+File.separator +"hotParamRule.json");
}
}
Initfunc实现类
/**
* InitFunc实现类,处理dataSource初始化逻辑
*/
public class FileDataSourceInit implements InitFunc {
@Override
public void init() throws Exception {
//创建文件存储目录
RuleFileUtils.mkdirIfNotExits(PersistenceRuleConstant.storePath);
//创建规则文件
RuleFileUtils.createFileIfNotExits(PersistenceRuleConstant.rulesMap);
//处理流控规则逻辑 配置读写数据源
dealFlowRules();
// 处理降级规则
dealDegradeRules();
// 处理系统规则
dealSystemRules();
// 处理热点参数规则
dealParamFlowRules();
// 处理授权规则
dealAuthRules();
}
private void dealFlowRules() throws FileNotFoundException {
String ruleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.FLOW_RULE_PATH).toString();
//创建流控规则的可读数据源
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource(
ruleFilePath, RuleListConverterUtils.flowRuleListParser
);
// 将可读数据源注册至FlowRuleManager 这样当规则文件发生变化时,就会更新规则到内存
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<List<FlowRule>>(
ruleFilePath, RuleListConverterUtils.flowFuleEnCoding
);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
}
private void dealDegradeRules() throws FileNotFoundException {
//讲解规则文件路径
String degradeRuleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.DEGRAGE_RULE_PATH).toString();
//创建流控规则的可读数据源
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource(
degradeRuleFilePath, RuleListConverterUtils.degradeRuleListParse
);
// 将可读数据源注册至FlowRuleManager 这样当规则文件发生变化时,就会更新规则到内存
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(
degradeRuleFilePath, RuleListConverterUtils.degradeRuleEnCoding
);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
}
private void dealSystemRules() throws FileNotFoundException {
//讲解规则文件路径
String systemRuleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.SYSTEM_RULE_PATH).toString();
//创建流控规则的可读数据源
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource(
systemRuleFilePath, RuleListConverterUtils.sysRuleListParse
);
// 将可读数据源注册至FlowRuleManager 这样当规则文件发生变化时,就会更新规则到内存
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(
systemRuleFilePath, RuleListConverterUtils.sysRuleEnCoding
);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
}
private void dealParamFlowRules() throws FileNotFoundException {
//讲解规则文件路径
String paramFlowRuleFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.HOT_PARAM_RULE).toString();
//创建流控规则的可读数据源
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource(
paramFlowRuleFilePath, RuleListConverterUtils.paramFlowRuleListParse
);
// 将可读数据源注册至FlowRuleManager 这样当规则文件发生变化时,就会更新规则到内存
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDatjaSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(
paramFlowRuleFilePath, RuleListConverterUtils.paramRuleEnCoding
);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}
private void dealAuthRules() throws FileNotFoundException {
//讲解规则文件路径
String authFilePath = PersistenceRuleConstant.rulesMap.get(PersistenceRuleConstant.AUTH_RULE_PATH).toString();
//创建流控规则的可读数据源
ReadableDataSource<String, List<AuthorityRule>> authRuleRDS = new FileRefreshableDataSource(
authFilePath, RuleListConverterUtils.authorityRuleParse
);
// 将可读数据源注册至FlowRuleManager 这样当规则文件发生变化时,就会更新规则到内存
AuthorityRuleManager.register2Property(authRuleRDS.getProperty());
//创建流控规则的写数据源
WritableDataSource<List<AuthorityRule>> authRuleWDS = new FileWritableDataSource<>(
authFilePath, RuleListConverterUtils.authorityEncoding
);
// 将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
// 这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
WritableDataSourceRegistry.registerAuthorityDataSource(authRuleWDS);
}
}
15.建立META-INF目录下建立services目录 内容为com.qianyue.sentinel.FileDataSourceInit
16.安装到本地仓库
17.导入工程中
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-extension-file-pullartifactId>
<version>1.8.0version>
dependency>
18.测试配置好规则后重新服务发现规则还在
19.查看持久化文件,并修改持久化文件的值
20.查看控制台变化 由1变为2 说明持久化成功
在生产环境下,应用更多的是推的模式,对于 push 模式的数据源,如远程配置中心(ZooKeeper, Nacos, Apollo 等等),推送的操作不应由 Sentinel 客户端进行,而应该经控制台统一进行管理,直接进行推送,数据源仅负责获取配置中心推送的配置并更新到本地。因此推送规则正确做法应该是 配置中心控制台/Sentinel控制台→ 配置中心 →Sentinel 数据源 → Sentinel
所以推模式有两种,一种是通过Sentinel控制台推送规则或者通过nacos控制台推送规则
官网给了我们一个nacos推送的demo.模块是sentinel-demo-nacos-datasource,我们分析一下,下边可以看出我们在流控规则管理器注册了一个NacosDataSource数据源
NacosDataSource数据源是怎么工作的呢?添加了一个监听器,有一个回调方法,当我们配置中心的配置更改后,会调用回调方法,把新的值推送回来,这样我们的Sentinel监听到后会修改内存中的值。
1.引入依赖
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
2.yaml配置
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719
datasource:
ds1: #自定义名称 要求唯一
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow #规则类型
3.nacos控制台添加规则配置
[
{
"resource": "/sentinel/testA",
"controlBehavior": 0,
"count": 1.0,
"grade": 1,
"limitApp": "default",
"strategy": 0
}
]
4.测试
5.修改nacos中的配置,改为qps为5
6.接口1s内超过5次就限流
7.再看Sentinel控制台 阈值也变为5
8.关闭微服务重启,在看sentinel控制台,发现配置还存在,说明我们持久化功能已经实现,怎么持久化就是nacos的事情
9.我们在sentinel控制台修改阈值为2,微服务直接生效
10.我们看下nacos的配置,阈值还是5
11.重启一下微服务,查看sentinel控制台,阈值又变回5,我们的修改并未做出持久化。
12.三者此时的广西如下:Nacos会推送配置给客户端,客户端把规则加载到内存中,此时Sentinel控制台通过通信可以从微服务客户端拉取配置和推送配置,但是当我们Sentinel修改配置的时候,只能作用于微服务客户端的内存中,此时没有一个写数据源可以在微服务客户端把配置推送到Nacos控制台,所以当我们在Sentinel控制台修改配置后,Nacos控制台是无法感知并修改配置的。此时有两种方法,第一种方法可以在微服务客户端添加一个写数据源,当Sentinel推送配置后,写数据源负责推送数据给Nacos客户端。第二种方法可以让Sentinel客户端直接推送给Nacos控制台,此时微服务客户端只需要监听Nacos配置文件的变化即可。这就是我们的第二种推模式
Sentinel Dashboard监听Nacos配置的变化,如发生变化就更新本地缓存。在Sentinel Dashboard端新增或修改规则配置 在保存到内存的同时,直接发布配置到nacos配置中心;Sentinel Dashboard直接从nacos拉取所有的规则配置。 sentinel Dashboard和sentinel client 不直接通信,而是通过nacos配置中心获取到配置的变更。
官方从 Sentinel 1.4.0 开始,Sentinel 控制台提供 DynamicRulePublisher 和 DynamicRuleProvider 接口用于实现应用维度的规则推送和拉取:
官方给的demo是在源码sentinel-dashboard模块下的test目录中
接下来对Sentinel Dashboard的改造就是基于Demo完成的
1.修改sentinel-dashboard的pom文件,把作用域放开,如果不注释掉 ConfigService不会注入进来
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
2.编写常量配置类,定义我们GROUP_ID和规则后缀等。
public final class NacosConfigUtil {
public static final String GROUP_ID = "SENTINEL_GROUP";
public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";
public static final String PARAM_FLOW_DATA_ID_POSTFIX = "-param-rules";
public static final String CLUSTER_MAP_DATA_ID_POSTFIX = "-cluster-map";
/**
* cc for `cluster-client`
*/
public static final String CLIENT_CONFIG_DATA_ID_POSTFIX = "-cc-config";
/**
* cs for `cluster-server`
*/
public static final String SERVER_TRANSPORT_CONFIG_DATA_ID_POSTFIX = "-cs-transport-config";
public static final String SERVER_FLOW_CONFIG_DATA_ID_POSTFIX = "-cs-flow-config";
public static final String SERVER_NAMESPACE_SET_DATA_ID_POSTFIX = "-cs-namespace-set";
private NacosConfigUtil() {}
}
3.像容器中注入Bean、比如转换器、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");
}
}
4.编写推接口和拉接口的实现类,实现配置的推送和拉取功能
FlowRuleNacosProvider 负责拉取配置
@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {
@Autowired
private ConfigService configService;
@Autowired
private Converter<String, List<FlowRuleEntity>> converter;
@Override
public List<FlowRuleEntity> getRules(String appName,String ip,Integer port) throws Exception {
String rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID, 3000);
if (StringUtil.isEmpty(rules)) {
return new ArrayList<>();
}
return converter.convert(rules);
}
}
FlowRuleNacosPublisher 负责推送配置
//注意我们客户端的规则类的FlowRule,服务端用的是FlowRuleEntity,如果两把的字段不一致,会导致数据缺失(其它类型的规则一样,特别是热点key规则,需要特别处理一下,两边的字段是不一致的。它的Entity对象中包了一层rule)。
@Component("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
@Autowired
private ConfigService configService;
@Override
public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
AssertUtil.notEmpty(app, "app name cannot be empty");
if (rules == null) {
return;
}
//发布配置到Nacos配置中心
configService.publishConfig(
app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
NacosConfigUtil.GROUP_ID, NacosConfigUtil.convertToRule(rules));
}
}
5.改造流控规则增删改查接口,由从微服务客户端进行拉取和推送改为从Nacos配置中心进行拉取和推送
FlowControllerV1中注入我们的推送对象和拉取对象
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;
修改拉取方法
修改添加规则方法
修改更新方法
修改删除方法
6.测试 修改Sentinel控制台的数据,查看Nacos控制台配置的是否变化,我这里把阈值1修改为2
7.修改Nacos中的值,查看Sentinel中是否变化
8.通过以上步骤,我们实现了通过Sentinel控制台向Naocs推送和拉取流控规则(其他规则是一样的套路)