丰富的线上&线下活动,深入探索云世界
做任务,得社区积分和周边
最真实的开发者用云体验
让每位学生受益于普惠算力
让创作激发创新
资深技术专家手把手带教
遇见技术追梦人
技术交流,直击现场
海量开发者使用工具、手册,免费下载
极速、全面、稳定、安全的开源镜像
开发手册、白皮书、案例集等实战精华
为开发者定制的Chrome浏览器插件
在40岁老架构师尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,并且拿了很多大厂offer。
比如小伙伴在面试蚂蚁的时候,就遇到以下面试题:
Sentinel熔断降级,是如何实现的?
小伙伴由于之前没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,联合社群小伙伴,来一个Sentinel学习圣经:从入门到精通Sentinel。
特别说明,本文属于穿透SpringCloud工业级底座工程(一共包括15大学习圣经)的其中之一,并且,此系列15大学习圣经底座正在录制视频,帮助大家一举成为技术高手。
15大圣经,使得大家内力猛增,可以充分展示一下大家雄厚的“技术肌肉”,让面试官爱到“不能自已、口水直流”,然后实现”offer直提”。
其中,专题3为:注册发现治理架构:Nacos学习圣经,具体如下:
其中,专题5为RPC治理架构Dubb3.0,具体如下:
本文,就是Sentinel学习圣经。稍后会录制视频。录完之后,Sentinel学习圣经正式版本会有更新,最新版本找尼恩获取。
在40岁老架构师尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
问题1:Sentinel高可用熔断降级,是如何实现的?
在这里,借助《Sentinel学习圣经》尼恩给大家做一下系统化、体系化的Sentinel梳理,使得大家内力猛增,展示一下雄厚的“技术肌肉、技术实力”,让面试官爱到“不能自已、口水直流”,然后实现”offer直提,offer自由”。
首先给面试官来一个总体的介绍:
在微服务架构中,Sentinel作为一种流量控制、熔断降级和服务降级的解决方案,得到了广泛的应用。
Sentinel是一个开源的流量控制和熔断降级库,用于保护分布式系统免受大量请求的影响。
然后,尼恩建议大家,从以下的几个维度去全面介绍Sentinel:
一:熔断机制
二:降级机制
三:高可用性机制
Sentinel的高可用性主要通过以下方式来实现:
a.多节点部署:将Sentinel配置为多节点部署,确保即使一个节点发生故障,其他节点仍然能够继续工作。
b.持久化配置:Sentinel支持将配置信息持久化到外部存储,如Nacos、Redis等。这样,即使Sentinel节点重启,它可以加载之前的配置信息。
c.集群流控规则:Sentinel支持集群流控规则,多个节点可以共享流量控制规则,以协同工作来保护系统。
d.实时监控:Sentinel提供了实时监控和仪表板,可以查看系统的流量控制和熔断降级情况,帮助及时发现问题并采取措施。
四:自适应控制
Sentinel具有自适应控制的功能,它可以根据系统的实际情况自动调整流量控制和熔断降级策略,以适应不同的负载和流量模式。
总的来说,Sentinel的高可用性熔断降级机制是通过多节点部署、持久化配置、实时监控、自适应控制等多种手段来实现的。
这使得Sentinel能够在分布式系统中保护关键资源免受异常流量的影响,并保持系统的稳定性和可用性。
那么,Sentinel是如何实现这些功能的呢?在说说Sentinel的基本组件。
Sentinel主要包括以下几个部分:资源(Resource)、规则(Rule)、上下文(Context)和插槽(Slot)。
在Sentinel的运行过程中,主要分为以下几个核心步骤:
通过以上分析,我们可以看出,Sentinel的核心思想是通过规则来管理和控制资源。这种设计使得Sentinel具有很强的可扩展性和灵活性。我们可以根据业务需求,定制各种复杂的规则。
回到源码层面,在Sentinel源码,包括以下二大架构:
尼恩说明:两大架构的源码,简单说说就可以了,具体可以参见《Sentinel学习圣经》最新版本。
总指挥,Sentinel是一种非常强大的流量控制、熔断降级和服务降级的解决方案。已经成为了替代Hystrix的主要高可用组件。
以上的内容,如果大家能对答如流,如数家珍,基本上面试官会被你震惊到、吸引到。
最终,让面试官爱到“不能自已、口水直流”。offer,也就来了。
在刷题过程中,如果有啥问题,大家可以来找40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。
Sentinel是一个系统性的高可用保障工具,提供了限流、降级、熔断等一系列的能力,基于这些能力做了语意化概念抽象,这些概念对于理解实现机制特别有帮助,所以这里也复述一下。
流量控制有以下几个角度:
Sentinel的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
它考虑了请求的历史分布,更适用于应对突发流量和周期性负载的情况。
我们知道,Sentinel可以用来帮助我们实现流量控制、服务降级、服务熔断,而这些功能的实现都离不开接口被调用的实时指标数据,本文便是关于Sentinel是如何实现指标数据统计的。
上图中的右上角就是滑动窗口的示意图,是StatisticSlot的具体实现。
StatisticSlot是Sentinel的核心功能插槽之一,用于统计实时的调用数据。
Sentinel是基于滑动窗口实现的实时指标数据收集统计,底层采用高性能的滑动窗口数据结构LeapArray来统计实时的秒级指标数据,可以很好地支撑写多于读的高并发场景。
publicclassArrayMetricimplementsMetric{privatefinalLeapArray
array类型为AtomicReferenceArray
这些指标数据包括请求总数、成功总数、异常总数、总耗时、最小耗时、最大耗时等,
这里没有用AtomicLong,而是用LongAdder统计,LongAdder保证了数据修改的原子性,又采用分段模式,性能比AtomicLong表现更好。
publicenumMetricEvent{PASS,BLOCK,EXCEPTION,SUCCESS,RT,OCCUPIED_PASS}当需要获取Bucket记录总的成功请求数或者异常总数、总的请求处理耗时,可根据事件类型(MetricEvent)从Bucket的LongAdder数组中获取对应的LongAdder,并调用sum方法获取总数。
publiclongget(MetricEventevent){returncounters[event.ordinal()].sum();}当需要Bucket记录一个成功请求或者一个异常请求、处理请求的耗时,可根据事件类型(MetricEvent)从LongAdder数组中获取对应的LongAdder,并调用其add方法。
如果我们希望能够知道某个接口的统计数据:如每秒处理成功请求数(成功QPS)、每秒处理失败请求数(失败QPS),以及处理每个成功请求的平均耗时(avgRT),
注意这里我们只需要控制Bucket统计一秒钟的指标数据即可,但如何才能确保Bucket存储的就是精确到1秒内的数据呢?
由于只需要保存最近一分钟的数据。
内存资源是有限的,而这个数组可以循环使用,并且永远只保存最近1分钟的数据,这样可以避免频繁的创建Bucket,减少内存资源的占用。
那如何定位Bucket呢?
calculateTimeIdx方法中,取余数就是实现循环利用数组。
MetricBucket定义一个LongAdder[]类型的成员变量counter数组来进行窗口内数据的统计。
JDK1.8时,java.util.concurrent.atomic包中提供了一个新的原子类:LongAdder。
根据Oracle官方文档的介绍,LongAdder在高并发的场景下会比它的前辈————AtomicLong具有更好的性能,代价是消耗更多的内存空间:
我们知道,AtomicLong中有个内部变量value保存着实际的long值,所有的操作都是针对该变量进行。也就是说,高并发环境下,value变量其实是一个热点,也就是N个线程竞争一个热点。
LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
这种做法有没有似曾相识的感觉?没错,ConcurrentHashMap中的“分段锁”其实就是类似的思路。
尼恩强烈建议:Sentinel最好是和Hystrix对比学习
先看看老牌的hystrix
hystrix作为老牌SpringCloud微服务保护的组件,很多项目仍然在使用,
另外,底层原理都是想通的,大家可以和sentinel对比学习
sentinel是SpringCloud阿里巴巴的微服务保护组件,
所以,在学习的时候,sentinel最好与hystrix对比学习,
在微服务架构系统中通常会有多个服务,在服务调用中如果出现基础服务故障,可能会导致级联故障,即一个服务不可用,可能导致所有调用它或间接调用它的服务都不可用,进而造成整个系统不可用的情况,这种现象也被称为服务雪崩效应。
服务雪崩效应是一种因“服务提供者不可用”(原因)导致“服务调用者不可用”(结果),并将不可用逐渐放大的现象。
服务雪崩效应示意如图所示,A为服务提供者,B为A的服务调用者,C为B的服务调用者。
当服务A因为某些原因导致不可用时,会引起服务B的不可用,并将不可用放大到服务C进而导致整个系统瘫痪,这样就形成了服务雪崩效应。
出现服务雪崩效应的原因如下:
如何解决服务器雪崩的方法有以下这些:
Sentinel为我们提供了多种的解决服务雪崩的方法:如超时机制、限流机制、熔断机制、降级机制等等,后面会为大家进行介绍
2012年,Sentinel诞生于阿里巴巴,其主要目标是流量控制。2013-2017年,Sentinel迅速发展,并成为阿里巴巴所有微服务的基本组成部分。它已在6000多个应用程序中使用,涵盖了几乎所有核心电子商务场景。2018年,Sentinel演变为一个开源项目。2020年,SentinelGolang发布。
Sentinel的官方使用手册
对于sentinel的介绍,我们这里先引入官方的说法
分布式系统的流量防卫兵随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
然后来看看它的特性
丰富的应用场景:Sentinel承接了阿里巴巴近10年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控:Sentinel同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至500台以下规模的集群的汇总运行情况。
广泛的开源生态:Sentinel提供开箱即用的与其它开源框架/库的整合模块,例如与SpringCloud、Dubbo、gRPC的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。
完善的SPI扩展点:Sentinel提供简单易用、完善的SPI扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel的生态圈
Sentinel很多的特性和Hystrix有很多类似的功能。以下是Sentinel和Hystrix的对比。
Sentinel官方提供了详细的由Hystrix迁移到Sentinel的方法
①资源:
资源是Sentinel的关键概念。它可以是Java应用程序中的任何内容,例如由应用程序提供的服务或者是服务里的方法,甚至可以是一段代码。Sentinel定义资源的方式有下面几种:适配主流框架自动定义资源、通过SphU手动定义资源、通过SphO手动定义资源、注解方式定义资源。这个稍后会有使用方法教程。
其中注解方式定义资源@SentinelResource参数介绍如下:
②规则:围绕资源而设定的规则。
您可以从官方网站中下载最新版本的控制台jar包,下载地址如下:
普通进程
java-server-Xms64m-Xmx256m-Dserver.port=8849-Dcsp.sentinel.dashboard.server=localhost:8849-Dproject.name=sentinel-dashboard-jar/work/sentinel-dashboard-1.8.6.jarjava-server-Xms64m-Xmx256m-Dserver.port=8849-Dcsp.sentinel.dashboard.server=localhost:8849-Dproject.name=sentinel-dashboard-jarsentinel-dashboard-1.8.6.jar守护进程
nohupjava-server-Xms64m-Xmx256m-Dserver.port=8849-Dcsp.sentinel.dashboard.server=localhost:8849-Dproject.name=sentinel-dashboard-jar/work/sentinel-dashboard-1.8.6.jar2>&1&或者/usr/bin/su-root-c"nohupjava-server-Xms64m-Xmx256m-Dserver.port=8849-Dcsp.sentinel.dashboard.server=localhost:8849-Dproject.name=sentinel-dashboard-jar/work/sentinel-dashboard-1.8.6.jar2>&1&"开机启动:启动命令可以加入到启动的rc.local配置文件,之后做到开机启动
启动Sentinel控制台需要JDK版本为1.8及以上版本,
使用如下命令启动控制台:
其中-Dserver.port=8849用于指定Sentinel控制台端口为8849。
启动日志如下
打开浏览器即可展示Sentinel的管理控制台
默认情况下Sentinel会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包。
也可以配置sentinel.eager=true,取消Sentinel控制台懒加载。
控制台启动后,客户端需要按照以下步骤接入到控制台。
父工程引入alibaba实现的SpringCloud
spring:cloud:sentinel:transport:dashboard:192.168.180.137:8849#sentinel控制台的请求地址这里的spring.cloud.sentinel.transport.dashboard配置控制台的请求路径。
Sentinel可以简单的分为Sentinel核心库和Dashboard。核心库不依赖Dashboard,但是结合Dashboard可以取得最好的效果。使用Sentinel来进行熔断保护,主要分为几个步骤:
Sentinel的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效.先把可能需要保护的资源定义好,之后再配置规则。
也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。
如果您的应用使用了Maven,则在pom.xml文件中加入以下代码即可:
资源是Sentinel中的核心概念之一。最常用的资源是我们代码中的Java方法。当然,您也可以更灵活的定义你的资源,例如,把需要控制流量的代码用SentinelAPISphU.entry("HelloWorld")和entry.exit()包围起来即可。在下面的例子中,我们将System.out.println("helloworld");作为资源(被保护的逻辑),用API包装起来。参考代码如下:
publicstaticvoidmain(String[]args){//配置规则.initFlowRules();while(true){//1.5.0版本开始可以直接利用try-with-resources特性try(Entryentry=SphU.entry("HelloWorld")){//被保护的逻辑System.out.println("helloworld");}catch(BlockExceptionex){//处理被流控的逻辑System.out.println("blocked!");}}}完成以上两步后,代码端的改造就完成了。
@SentinelResource("HelloWorld")publicvoidhelloWorld(){//资源中的逻辑System.out.println("helloworld");}这样,helloWorld()方法就成了我们的一个资源。注意注解支持模块需要配合SpringAOP或者AspectJ一起使用。
接下来,通过流控规则来指定允许该资源通过的请求次数,例如下面的代码定义了资源HelloWorld每秒最多只能通过20个请求。
Demo运行之后,我们可以在日志~/logs/csp/${appName}-metrics.log.xxx里看到下面的输出:
|--timestamp-|------datetime----|--resource-|p|block|s|e|rt1529998904000|2018-06-2615:41:44|helloworld|20|0|20|0|01529998905000|2018-06-2615:41:45|helloworld|20|5579|20|0|7281529998906000|2018-06-2615:41:46|helloworld|20|15698|20|0|01529998907000|2018-06-2615:41:47|helloworld|20|19262|20|0|01529998908000|2018-06-2615:41:48|helloworld|20|19502|20|0|01529998909000|2018-06-2615:41:49|helloworld|20|18386|20|0|0其中p代表通过的请求,block代表被阻止的请求,s代表成功执行完成的请求个数,e代表用户自定义的异常,rt代表平均响应时长。
可以看到,这个程序每秒稳定输出"helloworld"20次,和规则中预先设定的阈值是一样的。
客户端需要引入Transport模块来与Sentinel控制台进行通信。您可以通过pom.xml引入JAR包:
资源是Sentinel的关键概念。
它可以是Java应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,RPC接口方法,甚至可以是一段代码。
只要通过SentinelAPI定义的代码,就是资源,能够被Sentinel保护起来。
大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
把需要控制流量的代码用Sentinel的关键代码SphU.entry("资源名")和entry.exit()包围起来即可。
实例代码:
Entryentry=null;try{//定义一个sentinel保护的资源,名称为test-sentinel-apientry=SphU.entry(resourceName);//模拟执行被保护的业务逻辑耗时Thread.sleep(100);returna;}catch(BlockExceptione){//如果被保护的资源被限流或者降级了,就会抛出BlockExceptionlog.warn("资源被限流或降级了",e);return"资源被限流或降级了";}catch(InterruptedExceptione){return"发生InterruptedException";}finally{if(entry!=null){entry.exit();}ContextUtil.exit();}}在下面的例子中,用try-with-resources来定义资源。参考代码如下:
publicstaticvoidmain(String[]args){//配置规则.initFlowRules();while(true){//1.5.0版本开始可以直接利用try-with-resources特性try(Entryentry=SphU.entry("HelloWorld")){//被保护的逻辑System.out.println("helloworld");}catch(BlockExceptionex){//处理被流控的逻辑System.out.println("blocked!");}}}资源注解@SentinelResource也可以使用Sentinel提供的注解@SentinelResource来定义资源,实例如下:
@SentinelResource("HelloWorld")publicvoidhelloWorld(){//资源中的逻辑System.out.println("helloworld");}@SentinelResource注解注意:注解方式埋点不支持private方法。
@SentinelResource用于定义资源,并提供可选的异常处理和fallback配置项。
@SentinelResource注解包含以下属性:
blockHandler对应处理BlockException的函数名称,可选项。blockHandler函数访问范围需要是public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为BlockException。blockHandler函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定blockHandlerClass为对应的类的Class对象,注意对应的函数必需为static函数,否则无法解析。
规则主要有流控规则、熔断降级规则、系统规则、权限规则、热点参数规则等:
一段硬编码的方式定义流量控制规则如下:
privatevoidinitSystemRule(){List
由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。
熔断机模型:
当下游的服务因为某种原因突然变得不可用或响应过慢,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。熔断器模型
熔断器模型的状态机有3个状态。
熔断降级规则包含下面几个重要的属性:
我们通常用以下几种降级策略:
异常比率的阈值范围是[0.0,1.0],代表0%-100%。
可以通过调用DegradeRuleManager.loadRules()方法来用硬编码的方式定义流量控制规则。
异常数(DEGRADE_GRADE_EXCEPTION_COUNT)熔断:
@PostConstructpublicvoidinitSentinelRule(){//熔断规则:5s内调用接口出现异常次数超过5的时候,进行熔断List
参数
Hystrix常用的线程池隔离会造成线程上下切换的overhead比较大;
Hystrix使用的信号量隔离对某个资源调用的并发数进行控制,效果不错,但是无法对慢调用进行自动降级;
Sentinel通过并发线程数的流量控制提供信号量隔离的功能;
此外,Sentinel支持的熔断降级维度更多,可对多种指标进行流控、熔断,且提供了实时监控和控制面板,功能更为强大。
流量控制(FlowControl),原理是监控应用流量的QPS或并发线程数等指标,当达到指定阈值时对流量进行控制,避免系统被瞬时的流量高峰冲垮,保障应用高可用性。
通过流控规则来指定允许该资源通过的请求次数,例如下面的代码定义了资源HelloWorld每秒最多只能通过20个请求。
参考的规则定义如下:
privatestaticvoidinitFlowRules(){List
资源名:唯一名称,默认请求路径
阈值类型/单机阈值:
是否集群:是否为集群
配置
频繁刷新请求,1秒访问2次请求,正常,超过设置的阈值,将报默认的错误。
再次的1秒访问2次请求,访问正常。超过2次,访问异常
调用关系包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关系。Sentinel通过NodeSelectorSlot建立不同资源间的调用的关系,并且通过ClusterBuilderSlot记录每个资源的实时统计信息。
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。
比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢.
举例来说,read_db和write_db这两个资源分别代表数据库读写,我们可以给read_db设置限流规则来达到写优先的目的。具体的方法:
设置`strategy`为`RuleConstant.STRATEGY_RELATE`设置`refResource`为`write_db`。这样当写库操作过于频繁时,读数据的请求会被限流。还有一个例子,电商的下订单和支付两个操作,需要优先保障支付,可以根据支付接口的流量阈值,来对订单接口进行限制,从而保护支付的目的。
添加2个请求
选择QPS,单机阈值为1,选择关联,关联资源为/test_ref,这里用Jmeter模拟高并发,请求/test_ref。
在大批量线程高并发访问/test_ref,导致/test失效了
链路类型的关联也类似,就不再演示了。多个请求调用同一微服务。
场景1:JVM刚启动场景需要预热,具体的原因,去阅读下面的博客
如秒杀系统在开启瞬间,会有很多流量上来,很可能把系统打死,预热方式就是为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
WarmUp(冷启动,预热)模式就是为了实现这个保护系统目的的。
默认coldFactor为3,即请求QPS从threshold/3开始,经预热时长逐渐升至设定的QPS阈值。
@SentinelResource(value="testWarmUP",blockHandler="exceptionHandlerOfWarmUp")@GetMapping("/testWarmUP")publicStringtestWarmUP(){log.info(Thread.currentThread().getName()+"\t"+"...test1");return"-------hellobaby,iamtestWarmUP";}代码预热规则案例:阈值为10qps,预热时长设置为5s;
FlowRulewarmUPRule=newFlowRule();warmUPRule.setResource("testWarmUP");warmUPRule.setCount(10);warmUPRule.setGrade(RuleConstant.FLOW_GRADE_QPS);warmUPRule.setLimitApp("default");warmUPRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);warmUPRule.setWarmUpPeriodSec(5);1)默认冷加载因子codeFactor为3,即请求QPS从(threshold/3)开始,经过多少预热时长才逐渐升至设定的QPS阈值;
系统初始化阈值为10/3约等于3,即阈值刚开始为3;然后过了5s后,阈值才慢慢升到10;
就是5s前阈值是3,5s后阈值为10;
3)testWarmUP刚开始每秒访问4次,就会报错,超过了10/3=3,5s后每秒访问1~10次都正常;
先在单机阈值10/3,3的时候,预热10秒后,慢慢将阈值升至20。
通常冷启动的过程系统允许通过的QPS曲线如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。
想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
某瞬时来了大流量的请求,而如果此时要处理所有请求,很可能会导致系统负载过高,影响稳定性。但其实可能后面几秒之内都没有消息投递,若直接把多余的消息丢掉则没有充分利用系统处理消息的能力。
模拟2个用户同时并发的访问资源,发出100个请求,
何为热点?
热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的TopK数据,并对其访问进行限制。比如:
热点参数规则(ParamFlowRule)类似于流量控制规则(FlowRule):
@GetMapping("/byHotKey")@SentinelResource(value="byHotKey",blockHandler="userAccessError")publicStringtest4(@RequestParam(value="userId",required=false)StringuserId,@RequestParam(value="goodId",required=false)intgoodId){log.info(Thread.currentThread().getName()+"\t"+"...byHotKey");return"-----------byHotKey:UserId";}限流规则代码:可以通过ParamFlowRuleManager的loadRules方法更新热点参数规则,下面是官方实例:
ParamFlowRulerule=newParamFlowRule(resourceName).setParamIdx(0).setCount(5);//针对int类型的参数PARAM_B,单独设置限流QPS阈值为10,而不是全局的阈值5.ParamFlowItemitem=newParamFlowItem().setObject(String.valueOf(PARAM_B)).setClassType(int.class.getName()).setCount(10);rule.setParamFlowItemList(Collections.singletonList(item));ParamFlowRuleManager.loadRules(Collections.singletonList(rule));具体的限流代码如下:
在开始之前,我们先了解一下系统保护的目的:
长期以来,系统保护的思路是根据硬指标,即系统的负载(load1)来做系统过载保护。当系统负载高于某个阈值,就禁止或者减少流量的进入;当load开始好转,则恢复流量的进入。这个思路给我们带来了不可避免的两个问题:
系统保护的目标是在系统不被拖垮的情况下,提高系统的吞吐率,而不是load一定要到低于某个阈值。如果我们还是按照固有的思维,超过特定的load就禁止流量进入,系统load恢复就放开流量,这样做的结果是无论我们怎么调参数,调比例,都是按照果来调节因,都无法取得良好的效果。
系统规则支持以下的模式:
系统保护规则是从应用级别的入口流量进行控制,从单台机器的load、CPU使用率、平均RT、入口QPS和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如Web服务或Dubbo服务端接收的请求,都属于入口流量。
系统规则的参数说明:
硬编码的方式定义流量控制规则如下:
调用方信息通过ContextUtil.enter(resourceName,origin)方法中的origin参数传入。也可以在spring容器中注册RequestOriginParser
Sentinel可以简单的分为Sentinel核心库和Dashboard。
核心库不依赖Dashboard,可以通过代码手段定义资源。
我们说的资源,可以是任何东西,服务,服务里的方法,甚至是一段代码。
使用Sentinel来进行资源保护,主要分为几个步骤:
先把可能需要保护的资源定义好(埋点),之后再配置规则。
对于主流的框架,我们提供适配,只需要按照适配中的说明配置,Sentinel就会默认定义提供的服务,方法等为资源。
为了减少开发的复杂程度,我们对大部分的主流框架,例如WebServlet、Dubbo、SpringCloud、gRPC、SpringWebFlux、Reactor等都做了适配。您只需要引入对应的依赖即可方便地整合Sentinel。
SphU包含了try-catch风格的API。用这种方式,当资源发生了限流之后会抛出BlockException。这个时候可以捕捉异常,进行限流之后的逻辑处理。示例代码如下:
//1.5.0版本开始可以利用try-with-resources特性(使用有限制)//资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。try(Entryentry=SphU.entry("resourceName")){//被保护的业务逻辑//dosomethinghere...}catch(BlockExceptionex){//资源访问阻止,被限流或被降级//在此处进行相应的处理操作}特别地,若entry的时候传入了热点参数,那么exit的时候也一定要带上对应的参数(exit(count,args)),否则可能会有统计错误。这个时候不能使用try-with-resources的方式。
另外通过Tracer.trace(ex)来统计异常信息时,由于try-with-resources语法中catch调用顺序的问题,会导致无法正确统计异常数,因此统计异常信息时也不能在try-with-resources的catch块中调用Tracer.trace(ex)。
手动exit示例:
Entryentry=null;//务必保证finally会被执行try{//资源名可使用任意有业务语义的字符串,注意数目不能太多(超过1K),超出几千请作为参数传入而不要直接作为资源名//EntryType代表流量类型(inbound/outbound),其中系统规则只对IN类型的埋点生效entry=SphU.entry("自定义资源名");//被保护的业务逻辑//dosomething...}catch(BlockExceptionex){//资源访问阻止,被限流或被降级//进行相应的处理操作}catch(Exceptionex){//若需要配置降级规则,需要通过这种方式记录业务异常Tracer.traceEntry(ex,entry);}finally{//务必保证exit,务必保证每个entry与exit配对if(entry!=null){entry.exit();}}热点参数埋点示例:
Entryentry=null;try{//若需要配置例外项,则传入的参数只支持基本类型。//EntryType代表流量类型,其中系统规则只对IN类型的埋点生效//count大多数情况都填1,代表统计为一次调用。entry=SphU.entry(resourceName,EntryType.IN,1,paramA,paramB);//Yourlogichere.}catch(BlockExceptionex){//Handlerequestrejection.}finally{//注意:exit的时候也一定要带上对应的参数,否则可能会有统计错误。if(entry!=null){entry.exit(1,paramA,paramB);}}SphU.entry()的参数描述:
注意:SphU.entry(xxx)需要与entry.exit()方法成对出现,匹配调用,否则会导致调用链记录异常,抛出ErrorEntryFreeException异常。常见的错误:
SphO提供if-else风格的API。用这种方式,当资源发生了限流之后会返回false,这个时候可以根据返回值,进行限流之后的逻辑处理。示例代码如下:
//资源名可使用任意有业务语义的字符串if(SphO.entry("自定义资源名")){//务必保证finally会被执行try{/***被保护的业务逻辑*/}finally{SphO.exit();}}else{//资源访问阻止,被限流或被降级//进行相应的处理操作}注意:SphO.entry(xxx)需要与SphO.exit()方法成对出现,匹配调用,位置正确,否则会导致调用链记录异常,抛出ErrorEntryFreeException`异常。
Sentinel支持通过@SentinelResource注解定义资源并配置blockHandler和fallback函数来进行限流之后的处理。示例:
Sentinel支持异步调用链路的统计。在异步调用中,需要通过SphU.asyncEntry(xxx)方法定义资源,并通常需要在异步的回调函数中调用exit方法。以下是一个简单的示例:
try{AsyncEntryentry=SphU.asyncEntry(resourceName);//异步调用.doAsync(userId,result->{try{//在此处处理异步调用的结果.}finally{//在回调结束后exit.entry.exit();}});}catch(BlockExceptionex){//Requestblocked.//Handletheexception(e.g.retryorfallback).}SphU.asyncEntry(xxx)不会影响当前(调用线程)的Context,因此以下两个entry在调用链上是平级关系(处于同一层),而不是嵌套关系:
//调用链类似于://-parent//---asyncResource//---syncResourceasyncEntry=SphU.asyncEntry(asyncResource);entry=SphU.entry(normalResource);若在异步回调中需要嵌套其它的资源调用(无论是entry还是asyncEntry),只需要借助Sentinel提供的上下文切换功能,在对应的地方通过ContextUtil.runOnContext(context,f)进行Context变换,将对应资源调用处的Context切换为生成的异步Context,即可维持正确的调用链路关系。示例如下:
publicvoidhandleResult(Stringresult){Entryentry=null;try{entry=SphU.entry("handleResultForAsync");//Handleyourresulthere.}catch(BlockExceptionex){//Blockedfortheresulthandler.}finally{if(entry!=null){entry.exit();}}}publicvoidsomeAsync(){try{AsyncEntryentry=SphU.asyncEntry(resourceName);//Asynchronousinvocation.doAsync(userId,result->{//在异步回调中进行上下文变换,通过AsyncEntry的getAsyncContext方法获取异步ContextContextUtil.runOnContext(entry.getAsyncContext(),()->{try{//此处嵌套正常的资源调用.handleResult(result);}finally{entry.exit();}});});}catch(BlockExceptionex){//Requestblocked.//Handletheexception(e.g.retryorfallback).}}此时的调用链就类似于:
Sentinel实现限流、隔离、降级、熔断等功能,本质要做的就是两件事情:
这里的资源就是希望被Sentinel保护的业务,例如项目中定义的controller方法就是默认被Sentinel保护的资源。
实现上述功能的核心骨架是一个叫做ProcessorSlotChain的类。这个类基于责任链模式来设计,将不同的功能(限流、降级、系统保护)封装为一个个的Slot,请求进入后逐个执行即可。
责任链中的Slot也分为两大类:
Sentinel中的簇点链路是由一个个的Node组成的,Node是一个接口,包括下面的实现:
所有的节点都可以记录对资源的访问统计数据,所以都是StatisticNode的子类。
按照作用分为两类Node:
DefaultNode记录的是资源在当前链路中的访问数据,用来实现基于链路模式的限流规则。ClusterNode记录的是资源在所有链路中的访问数据,实现默认模式、关联模式的限流规则。
例如:我们在一个SpringMVC项目中,有两个业务:
创建的链路图如下:
Sentinel中核心对象的关系如下图:
默认情况下,Sentinel会将controller中的方法作为被保护资源,那么问题来了,我们该如何将自己的一段代码标记为一个Sentinel的资源呢?
//资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。try(Entryentry=SphU.entry("resourceName")){//被保护的业务逻辑//dosomethinghere...}catch(BlockExceptionex){//资源访问阻止,被限流或被降级//在此处进行相应的处理操作}8.3.1.自定义资源例如,我们在order-service服务中,将OrderService的queryOrderById()方法标记为一个资源。
1)首先在order-service中引入sentinel依赖
spring:cloud:sentinel:transport:dashboard:localhost:8089#这里我的sentinel用了8089的端口3)修改OrderService类的queryOrderById方法
代码这样来实现:
publicOrderqueryOrderById(LongorderId){//创建Entry,标记资源,资源名为resource1try(Entryentry=SphU.entry("resource1")){//1.查询订单,这里是假数据Orderorder=Order.build(101L,4999L,"小米MIX4",1,1L,null);//2.查询用户,基于Feign的远程调用Useruser=userClient.findById(order.getUserId());//3.设置order.setUser(user);//4.返回returnorder;}catch(BlockExceptione){log.error("被限流或降级",e);returnnull;}}4)访问
然后打开sentinel控制台,查看簇点链路:
在之前学习Sentinel的时候,我们知道可以通过给方法添加@SentinelResource注解的形式来标记资源。
这个是怎么实现的呢?
来看下我们引入的Sentinel依赖包:
我们来看下SentinelAutoConfiguration这个类:
我们发现簇点链路中除了controller方法、service方法两个资源外,还多了一个默认的入口节点:
sentinel_spring_web_context,是一个EntranceNode类型的节点
这个节点是在初始化Context的时候由Sentinel帮我们创建的。
那么,什么是Context呢?
对应的API如下:
我们先看SentinelWebAutoConfiguration这个类:
这个类实现了WebMvcConfigurer,我们知道这个是SpringMVC自定义配置用到的类,可以配置HandlerInterceptor:
可以看到这里配置了一个SentinelWebInterceptor的拦截器。
发现它继承了AbstractSentinelInterceptor这个类。
HandlerInterceptor拦截器会拦截一切进入controller的方法,执行preHandle前置拦截方法,而Context的初始化就是在这里完成的。
我们来看看这个类的preHandle实现:
我们进入该方法:
publicstaticContextenter(Stringname,Stringorigin){if(Constants.CONTEXT_DEFAULT_NAME.equals(name)){thrownewContextNameDefineException("The"+Constants.CONTEXT_DEFAULT_NAME+"can'tbepermittodefined!");}returntrueEnter(name,origin);}进入trueEnter方法:
首先,回到一切的入口,AbstractSentinelInterceptor类的preHandle方法:
还有,SentinelResourceAspect的环绕增强方法:
可以看到,任何一个资源必定要执行SphU.entry()这个方法:
publicstaticEntryentry(Stringname,intresourceType,EntryTypetrafficType,Object[]args)throwsBlockException{returnEnv.sph.entryWithType(name,resourceType,trafficType,1,args);}继续进入Env.sph.entryWithType(name,resourceType,trafficType,1,args);:
@OverridepublicEntryentryWithType(Stringname,intresourceType,EntryTypeentryType,intcount,booleanprioritized,Object[]args)throwsBlockException{//将资源名称等基本信息封装为一个StringResourceWrapper对象StringResourceWrapperresource=newStringResourceWrapper(name,entryType,resourceType);//继续returnentryWithPriority(resource,count,prioritized,args);}进入entryWithPriority方法:
privateEntryentryWithPriority(ResourceWrapperresourceWrapper,intcount,booleanprioritized,Object...args)throwsBlockException{//获取ContextContextcontext=ContextUtil.getContext();if(context==null){//Usingdefaultcontext.context=InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);}、//获取Slot执行链,同一个资源,会创建一个执行链,放入缓存ProcessorSlot
获取ProcessorSlotChain以后会保存到一个Map中,key是ResourceWrapper,值是ProcessorSlotChain.
所以,一个资源只会有一个ProcessorSlotChain.
我们进入DefaultProcessorSlotChain的entry方法:
@Overridepublicvoidentry(Contextcontext,ResourceWrapperresourceWrapper,Objectt,intcount,booleanprioritized,Object...args)throwsThrowable{//first,就是责任链中的第一个slotfirst.transformEntry(context,resourceWrapper,t,count,prioritized,args);}这里的first,类型是AbstractLinkedProcessorSlot:
看下继承关系:
因此,first一定是这些实现类中的一个,按照最早讲的责任链顺序,first应该就是NodeSelectorSlot。
不过,既然是基于责任链模式,所以这里只要记住下一个slot就可以了,也就是next:
next确实是NodeSelectSlot类型。
而NodeSelectSlot的next一定是ClusterBuilderSlot,依次类推:
责任链就建立起来了。
NodeSelectorSlot负责构建簇点链路中的节点(DefaultNode),将这些节点形成链路树。
核心代码:
@Overridepublicvoidentry(Contextcontext,ResourceWrapperresourceWrapper,Objectobj,intcount,booleanprioritized,Object...args)throwsThrowable{//尝试获取当前资源的DefaultNodeDefaultNodenode=map.get(context.getName());if(node==null){synchronized(this){node=map.get(context.getName());if(node==null){//如果为空,为当前资源创建一个新的DefaultNodenode=newDefaultNode(resourceWrapper,null);HashMap
下一个slot,就是ClusterBuilderSlot
ClusterBuilderSlot负责构建某个资源的ClusterNode,核心代码:
@OverridepublicvoidaddPassRequest(intcount){//DefaultNode的计数器,代表当前链路的计数器super.addPassRequest(count);//ClusterNode计数器,代表当前资源的总计数器this.clusterNode.addPassRequest(count);}具体计数方式,我们后续再看。
核心API:
@Overridepublicvoidentry(Contextcontext,ResourceWrapperresourceWrapper,DefaultNodenode,intcount,booleanprioritized,Object...args)throwsThrowable{//校验黑白名单checkBlackWhiteAuthority(resourceWrapper,context);//进入下一个slotfireEntry(context,resourceWrapper,node,count,prioritized,args);}黑白名单校验的逻辑:
@Overridepublicvoidentry(Contextcontext,ResourceWrapperresourceWrapper,DefaultNodenode,intcount,booleanprioritized,Object...args)throwsThrowable{//系统规则校验SystemRuleManager.checkSystem(resourceWrapper);//进入下一个slotfireEntry(context,resourceWrapper,node,count,prioritized,args);}来看下SystemRuleManager.checkSystem(resourceWrapper);的代码:
是针对进入资源的请求,针对不同的请求参数值分别统计QPS的限流方式。
@Overridepublicvoidentry(Contextcontext,ResourceWrapperresourceWrapper,DefaultNodenode,intcount,booleanprioritized,Object...args)throwsThrowable{//如果没有设置热点规则,直接放行if(!ParamFlowRuleManager.hasRules(resourceWrapper.getName())){fireEntry(context,resourceWrapper,node,count,prioritized,args);return;}//热点规则判断checkFlow(resourceWrapper,count,args);//进入下一个slotfireEntry(context,resourceWrapper,node,count,prioritized,args);}9.8.1.令牌桶热点规则判断采用了令牌桶算法来实现参数限流,为每一个不同参数值设置令牌桶,Sentinel的令牌桶有两部分组成:
这两个Map的key都是请求的参数值,value却不同,其中:
当一个携带参数的请求到来后,基本判断流程是这样的:
2.9.FlowSlot
FlowSlot是负责限流规则的判断,如图:
包括:
三种流控模式,从底层数据统计角度,分为两类:
三种流控效果,从限流算法来看,分为两类:
核心API如下:
@Overridepublicvoidentry(Contextcontext,ResourceWrapperresourceWrapper,DefaultNodenode,intcount,booleanprioritized,Object...args)throwsThrowable{//限流规则检测checkFlow(resourceWrapper,context,node,count,prioritized);//放行fireEntry(context,resourceWrapper,node,count,prioritized,args);}checkFlow方法:
voidcheckFlow(ResourceWrapperresource,Contextcontext,DefaultNodenode,intcount,booleanprioritized)throwsBlockException{//checker是FlowRuleChecker类的一个对象checker.checkFlow(ruleProvider,resource,context,node,count,prioritized);}跟入FlowRuleChecker:
publicvoidcheckFlow(Function
publicbooleancanPassCheck(/*@NonNull*/FlowRulerule,Contextcontext,DefaultNodenode,intacquireCount,booleanprioritized){//获取限流资源名称StringlimitApp=rule.getLimitApp();if(limitApp==null){returntrue;}//校验规则returnpassLocalCheck(rule,context,node,acquireCount,prioritized);}进入passLocalCheck():
privatestaticbooleanpassLocalCheck(FlowRulerule,Contextcontext,DefaultNodenode,intacquireCount,booleanprioritized){//基于限流模式判断要统计的节点,//如果是直连模式,关联模式,对ClusterNode统计,如果是链路模式,则对DefaultNode统计NodeselectedNode=selectNodeByRequesterAndStrategy(rule,context,node);if(selectedNode==null){returntrue;}//判断规则returnrule.getRater().canPass(selectedNode,acquireCount,prioritized);}这里对规则的判断先要通过FlowRule#getRater()获取流量控制器TrafficShapingController,然后再做限流。
而TrafficShapingController有3种实现:
最终的限流判断都在TrafficShapingController的canPass方法中。
StatisticSlot部分,有这样一段代码:
就是在统计通过该节点的QPS,我们跟入看看,这里进入了DefaultNode内部:
现同时对DefaultNode和ClusterNode在做QPS统计,我们知道DefaultNode和ClusterNode都是StatisticNode的子类,这里调用addPassRequest()方法,最终都会进入StatisticNode中。
随便跟入一个:
这里有秒、分两种纬度的统计,对应两个计数器。找到对应的成员变量,可以看到:
两个计数器都是ArrayMetric类型,并且传入了两个参数:
接下来,我们进入ArrayMetric类的addPass方法:
这里的data是一个LeapArray:
LeapArray的四个属性:
因为滑动窗口最多分成sampleCount数量的小窗口,因此数组长度只要大于sampleCount,那么最近的一个滑动窗口内的2个小窗口就永远不会被覆盖,就不用担心旧数据被覆盖的问题了。
我们跟入data.currentWindow();方法:
这里只负责统计每个窗口的请求量,不负责拦截。限流拦截要看FlowSlot中的逻辑。
FlowSlot的限流判断最终都由TrafficShapingController接口中的canPass方法来实现。该接口有三个实现类:
因此,我们跟入默认的DefaultController中的canPass方法来分析:
@OverridepublicbooleancanPass(Nodenode,intacquireCount,booleanprioritized){//计算目前为止滑动窗口内已经存在的请求量intcurCount=avgUsedTokens(node);//判断:已使用请求量+需要的请求量(1)是否大于窗口的请求阈值if(curCount+acquireCount>count){//大于,说明超出阈值,返回falseif(prioritized&&grade==RuleConstant.FLOW_GRADE_QPS){longcurrentTime;longwaitInMs;currentTime=TimeUtil.currentTimeMillis();waitInMs=node.tryOccupyNext(currentTime,acquireCount,count);if(waitInMs privateintavgUsedTokens(Nodenode){if(node==null){returnDEFAULT_AVG_USED_TOKENS;}returngrade==RuleConstant.FLOW_GRADE_THREADnode.curThreadNum():(int)(node.passQps());}因为我们采用的是限流,走node.passQps()逻辑: 因此,我们跟入默认的RateLimiterController中的canPass方法来分析: Sentinel的降级是基于状态机来实现的: 对应的实现在DegradeSlot类中,核心API: @Overridepublicvoidentry(Contextcontext,ResourceWrapperresourceWrapper,DefaultNodenode,intcount,booleanprioritized,Object...args)throwsThrowable{//熔断降级规则判断performChecking(context,resourceWrapper);//继续下一个slotfireEntry(context,resourceWrapper,node,count,prioritized,args);}继续进入performChecking方法: voidperformChecking(Contextcontext,ResourceWrapperr)throwsBlockException{//获取当前资源上的所有的断路器CircuitBreakerList protectedbooleanfromOpenToHalfOpen(Contextcontext){//基于CAS修改状态,从OPEN到HALF_OPENif(currentState.compareAndSet(State.OPEN,State.HALF_OPEN)){//状态变更的事件通知notifyObservers(State.OPEN,State.HALF_OPEN,null);//得到当前资源Entryentry=context.getCurEntry();//给资源设置监听器,在资源Entry销毁时(资源业务执行完毕时)触发entry.whenTerminate(newBiConsumer 请求经过所有插槽后,一定会执行exit方法,而在DegradeSlot的exit方法中: 会调用CircuitBreaker的onRequestComplete方法。而CircuitBreaker有两个实现: 我们这里以异常比例熔断为例来看,进入ExceptionCircuitBreaker的onRequestComplete方法: @OverridepublicvoidonRequestComplete(Contextcontext){//获取资源EntryEntryentry=context.getCurEntry();if(entry==null){return;}//尝试获取资源中的异常Throwableerror=entry.getError();//获取计数器,同样采用了滑动窗口来计数SimpleErrorCountercounter=stat.currentWindow().value();if(error!=null){//如果出现异常,则error计数器+1counter.getErrorCount().add(1);}//不管是否出现异常,total计数器+1counter.getTotalCount().add(1);//判断异常比例是否超出阈值handleStateChangeWhenThresholdExceeded(error);}来看阈值判断的方法: sentinel分为2部分: 1.控制台(Dashboard)基于SpringBoot开发,打包后可以直接运行,不需要额外的Tomcat等应用容器。也就是是控制台,能够实时监控各个资源的流量情况,同时提供各种规则的配置。 关于sentinel的主要功能其实从它的Dashboard控制台里面就能知道 流量控制:对某个资源的qps,并发线程数进行控制,提供了快速失败,预热启动,匀速排队各种流控方式。降级控制:可以根据某个资源的rt,异常比例,异常数各种策略进行降级。热点控制:这个就是对某个资源的热点参数进行流控系统控制:这个就是对整个服务进行控制,控制类型有:系统负载,rt,入口qps,并发线程数权限控制:这个就是黑白名单的限制集群控制:从集群的角度对资源进行控制 sentinel是个maven项目,我们介绍下它的各个子项目都是干啥的 resource是sentinel中最重要的一个概念,sentinel通过资源来保护具体的业务代码或其他后方服务。sentinel把复杂的逻辑给屏蔽掉了,用户只需要为受保护的代码或服务定义一个资源,然后定义规则就可以了,剩下的通通交给sentinel来处理了。并且资源和规则是解耦的,规则甚至可以在运行时动态修改。 定义完资源后,就可以通过在程序中埋点来保护你自己的服务了,埋点的方式有两种: 以上这两种方式都是通过硬编码的形式定义资源然后进行资源埋点的,对业务代码的侵入太大,从0.1.1版本开始,sentinel加入了注解的支持,可以通过注解来定义资源,具体的注解为:SentinelResource。 也可以使用Sentinel提供的注解@SentinelResource来定义资源,实例如下: @SentinelResource("HelloWorld")publicvoidhelloWorld(){//资源中的逻辑System.out.println("helloworld");}通过注解除了可以定义资源外,还可以指定blockHandler和fallback方法。 在源码中,在sentinel中具体表示资源的类是:ResourceWrapper,他是一个抽象的包装类,包装了资源的Name和EntryType。 他有两个实现类,分别是:StringResourceWrapper和MethodResourceWrapper。 顾名思义,StringResourceWrapper是通过对一串字符串进行包装,是一个通用的资源包装类,MethodResourceWrapper是对方法调用的包装。 Context是对资源操作时的上下文环境,每个资源操作(针对Resource进行的entry/exit)必须属于一个Context,如果程序中未指定Context,会创建name为"sentinel_default_context"的默认Context。 一个Context生命周期内可能有多个资源操作,Context生命周期内的最后一个资源exit时会清理该Context,这也预示这整个Context生命周期的结束。 Context主要属性如下: publicclassContext{//context名字,默认名字"sentinel_default_context"privatefinalStringname;//context入口节点,每个context必须有一个entranceNodeprivateDefaultNodeentranceNode;//context当前entry,Context生命周期中可能有多个Entry,所有curEntry会有变化privateEntrycurEntry;//Theoriginofthiscontext(usuallyindicatedifferentinvokers,e.g.serviceconsumernameororiginIP).privateStringorigin="";privatefinalbooleanasync;}注意:一个Context生命期内Context只能初始化一次,因为是存到ThreadLocal中,并且只有在非null时才会进行初始化。 如果想在调用SphU.entry()或SphO.entry()前,自定义一个context,则通过ContextUtil.enter()方法来创建。 context是保存在ThreadLocal中的,每次执行的时候会优先到ThreadLocal中获取,为null时会调用MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME,"",resourceWrapper.getType())创建一个context。 当Entry执行exit方法时,如果entry的parent节点为null,表示是当前Context中最外层的Entry了,此时将ThreadLocal中的context清空。 首先我们要清楚的一点就是,每次执行entry()方法,试图冲破一个资源时,都会生成一个上下文。 这个上下文中会保存着调用链的根节点和当前的入口。 Context是通过ContextUtil创建的,具体的方法是trueEntry,代码如下: protectedstaticContexttrueEnter(Stringname,Stringorigin){//先从ThreadLocal中获取Contextcontext=contextHolder.get();if(context==null){//如果ThreadLocal中获取不到Context//则根据name从map中获取根节点,只要是相同的资源名,就能直接从map中获取到nodeMap 那保存在ThreadLocal中的上下文什么时候会清除呢?从代码中可以看到具体的清除工作在ContextUtil的exit方法中,当执行该方法时,会将保存在ThreadLocal中的context对象清除,具体的代码非常简单,这里就不贴代码了。 那ContextUtil.exit方法什么时候会被调用呢?有两种情况:一是主动调用ContextUtil.exit的时候,二是当一个入口Entry要退出,执行该Entry的trueExit方法的时候,此时会触发ContextUtil.exit的方法。但是有一个前提,就是当前Entry的父Entry为null时,此时说明该Entry已经是最顶层的根节点了,可以清除context。 刚才在Context身影中也看到了Entry的出现,现在就谈谈Entry。 每次执行SphU.entry()或SphO.entry()都会返回一个Entry,Entry表示一次资源操作,内部会保存当前invocation信息。 在一个Context生命周期中多次资源操作,也就是对应多个Entry,这些Entry形成parent/child结构保存在Entry实例中,entry类CtEntry结构如下: classCtEntryextendsEntry{protectedEntryparent=null;protectedEntrychild=null;protectedProcessorSlot Node(关于StatisticNode的讨论放到下一小节)默认实现类DefaultNode,该类还有一个子类EntranceNode;context有一个entranceNode属性,Entry中有一个curNode属性。 看到这里,你是不是有疑问? 为什么一个context有且仅有一个DefaultNode,我们的resouece跑哪去了呢? 其实,这里的一个context有且仅有一个DefaultNode是在NodeSelectorSlot范围内,NodeSelectorSlot是ProcessorSlotChain中的一环,获取ProcessorSlotChain是根据Resource维度来的。 总结为一句话就是: publicclassDefaultNodeextendsStatisticNode{privateResourceWrapperid;/***Thelistofallchildnodes.*子节点集合*/privatevolatileSet 如果defaultNode.clusterNode为null,则在ClusterBuilderSlot.entry中会进行初始化。 同一个Resource,对应同一个ProcessorSlotChain,这块处理逻辑在lookProcessChain方法中,如下: StatisticNode属性如下: publicenumMetricEvent{PASS,//Normalpass.BLOCK,//Normalblock.EXCEPTION,SUCCESS,RT,OCCUPIED_PASS}9、插槽Slot源码分析slot是另一个sentinel中非常重要的概念,sentinel的工作流程就是围绕着一个个插槽所组成的插槽链来展开的。 需要注意的是每个插槽都有自己的职责,他们各司其职完好的配合,通过一定的编排顺序,来达到最终的限流降级的目的。 默认的各个插槽之间的顺序是固定的,因为有的插槽需要依赖其他的插槽计算出来的结果才能进行工作。 Sentinel的整体工具流程就是使用责任链模式将所有的ProcessorSlot按照一定的顺序串成一个单向链表。 Sentinel将ProcessorSlot串成一个单向链表的是ProcessorSlotChain,这个ProcessorSlotChain是由SlotChainBuilder构造的。 这个和Netty类似,Netty也是一个责任链,在责任链上的每一个处理器,叫做handler。 entinel中的责任链模式,在责任链上的每一个处理器,叫做Slot。 换句话说,Sentinel在内部创建了一个责任链,责任链是由一系列ProcessorSlot接口的实现类组成的,每个ProcessorSlot对象负责不同的功能,外部请求想要访问资源需要责任链层层校验和处理。 每个Slot需要执行(例如配置过降级规则DegradeSlot)则处理,不需要执行则交给下一个Slot。每一个Slot如果处理失败或者/如果校验失败,会抛出BlockException异常。 责任链上的处理或者/校验顺序,大致如下: ProcessorSlot接口: 是一个基于责任链模式的接口,定义了一个entry()方法,用于处理入口参数和出口参数的限流和降级逻辑;一个exit()方法,用于将权限交给下一个抽象处理人(实际会传参具体处理人)。 ProcessorSlot实现类,大致如下: 但是这并不意味着我们只能按照框架的定义来,sentinel通过SlotChainBuilder作为SPI接口,使得SlotChain具备了扩展的能力。 我们可以通过实现SlotsChainBuilder接口加入自定义的slot并自定义编排各个slot之间的顺序,从而可以给sentinel添加自定义的功能。 那SlotChain是在哪创建的呢? 是在CtSph.lookProcessChain()方法中创建的,并且该方法会根据当前请求的资源先去一个静态的HashMap中获取,如果获取不到才会创建,创建后会保存到HashMap中。 这就意味着,同一个资源会全局共享一个SlotChain。默认生成ProcessorSlotChain为: //DefaultSlotChainBuilderpublicProcessorSlotChainbuild(){ProcessorSlotChainchain=newDefaultProcessorSlotChain();chain.addLast(newNodeSelectorSlot());chain.addLast(newClusterBuilderSlot());chain.addLast(newLogSlot());chain.addLast(newStatisticSlot());chain.addLast(newSystemSlot());chain.addLast(newAuthoritySlot());chain.addLast(newFlowSlot());chain.addLast(newDegradeSlot());returnchain;这里大概的介绍下每种Slot的功能职责: 每个Slot执行完业务逻辑处理后,会调用fireEntry()方法,该方法将会触发下一个节点的entry方法,下一个节点又会调用他的fireEntry,以此类推直到最后一个Slot,由此就形成了sentinel的责任链。 下面我们就来详细研究下这些Slot的原理。 在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。 责任链模式是一种对象行为型模式,其主要优点如下。 其主要缺点如下。 责任链模式的重要性 NodeSelectorSlot是用来构造调用链的,具体的是将资源的调用路径,封装成一个一个的节点,再组成一个树状的结构来形成一个完整的调用链,NodeSelectorSlot是所有Slot中最关键也是最复杂的一个Slot, NodeSelectorSlot涉及到以下几个核心的概念: 资源是Sentinel的关键概念。它可以是Java应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它服务,甚至可以是一段代码。 只要通过SentinelAPI定义的代码,就是资源,能够被Sentinel保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。 打个比方,我有一个服务A,请求非常多,经常会被陡增的流量冲垮, 为了防止这种情况,简单的做法,我们可以定义一个Sentinel的资源,通过该资源来对请求进行调整,使得允许通过的请求不会把服务A搞崩溃。 每个资源的状态也是不同的,这取决于资源后端的服务,有的资源可能比较稳定,有的资源可能不太稳定。 那么在整个调用链中,Sentinel需要对不稳定资源进行控制。 当调用链路中某个资源出现不稳定,例如表现为timeout,或者异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终导致雪崩的后果。 Sentinel中,资源就是用来保护系统的一个媒介。 源码中用来包装资源的类是:com.alibaba.csp.sentinel.slotchain.ResourceWrapper, 他有两个子类: 通过名字就知道可以将一段字符串或一个方法包装为一个资源。 上下文是一个用来保存调用链当前状态的元数据的类,每次进入一个资源时,就会创建一个上下文。 相同的资源名可能会创建多个上下文。 一个Context中包含了三个核心的对象: 1)当前调用链的根节点:EntranceNode 2)当前的入口:Entry 3)当前入口所关联的节点:Node 上下文中只会保存一个当前正在处理的入口Entry,另外还会保存调用链的根节点。 需要注意的是,每次进入一个新的资源时,都会创建一个新的上下文。 每次调用SphU#entry()都会生成一个Entry入口,该入口中会保存了以下数据: Entry是一个抽象类,他只有一个实现类,在CtSph中的一个静态类:CtEntry 节点是用来保存某个资源的各种实时统计信息的,他是一个接口,通过访问节点,就可以获取到对应资源的实时状态,以此为依据进行限流和降级操作。 可能看到这里,大家还是比较懵,这么多类到底有什么用, 接下来就让我们更进一步,挖掘一下这些类的作用,在这之前,我先给大家展示一下他们之间的关系, 如下图所示: 这里把几种Node的作用先大概介绍下: 当在一个上下文中多次调用了SphU#entry()方法时,就会创建一棵调用链树。 Sentinel实现流控,隔离,降级等功能,本质要做两件事: ProcessorSlotChian实现上述功能的骨架,这个类是基于责任链模式设计,将不同功能(限流,降级,系统保护)封装为一个个的Slot,请求进入后逐个执行责任链中Solt也分为两大类 第一大类:统计数据的构建 第一大类:规则判断部分 具体的代码在entry方法中创建CtEntry对象时: CtEntry(ResourceWrapperresourceWrapper,ProcessorSlot context的创建在上面已经分析过了,初始化的时候,context中的curEntry属性是没有值的,如下图所示: 每创建一个新的Entry对象时,都会重新设置context的curEntry,并将context原来的curEntry设置为该新Entry对象的父节点,如下图所示: 某个Entry退出时,将会重新设置context的curEntry,当该Entry是最顶层的一个入口时,将会把ThreadLocal中保存的context也清除掉,如下图所示: 上面的过程是构造了一棵调用链的树,但是这棵树只有树干,没有叶子,那叶子节点是在什么时候创建的呢? DefaultNode就是叶子节点,在叶子节点中保存着目标资源在当前状态下的统计信息。 通过分析,我们知道了叶子节点是在NodeSelectorSlot的entry方法中创建的。 具体的代码如下: 1)获取当前上下文对应的DefaultNode,如果没有的话会为当前的调用新生成一个DefaultNode节点,它的作用是对资源进行各种统计度量以便进行流控; 2)将新创建的DefaultNode节点,添加到context中,作为「entranceNode」或者「curEntry.parent.curNode」的子节点; 3)将DefaultNode节点,添加到context中,作为「curEntry」的curNode。 上面的第2步,不是每次都会执行。我们先看第3步,把当前DefaultNode设置为context的curNode,实际上是把当前节点赋值给context中curEntry的curNode,用图形表示就是这样: 多次创建不同的Entry,并且执行NodeSelectorSlot的entry方法后,就会变成这样一棵调用链树: PS:这里图中的node0,node1,node2可能是相同的node,因为在同一个context中从map中获取的node是同一个,这里只是为了表述的更清楚所以用了不同的节点名。 上面已经分析了叶子节点的构造过程,叶子节点是保存在各个Entry的curNode属性中的。 我们知道context中只保存了入口节点和当前Entry,那子节点是什么时候保存的呢,其实子节点就是上面代码中的第2步中保存的。 下面我们来分析上面的第2步的情况: 第一次调用NodeSelectorSlot的entry方法时,map中肯定是没有DefaultNode的,那就会进入第2步中,创建一个node,创建完成后会把该节点加入到context的lastNode的子节点中去。我们先看一下context的getLastNode方法: publicNodegetLastNode(){//如果curEntry不存在时,返回entranceNode//否则返回curEntry的lastNode,//需要注意的是curEntry的lastNode是获取的parent的curNode,//如果每次进入的资源不同,就会每次都创建一个CtEntry,则parent为null,//所以curEntry.getLastNode()也为nullif(curEntry!=null&&curEntry.getLastNode()!=null){returncurEntry.getLastNode();}else{returnentranceNode;}}代码中我们可以知道,lastNode的值可能是context中的entranceNode也可能是curEntry.parent.curNode,但是他们都是「DefaultNode」类型的节点,DefaultNode的所有子节点是保存在一个HashSet中的。 第一次调用getLastNode方法时,context中curEntry是null,因为curEntry是在第3步中才赋值的。所以,lastNode最初的值就是context的entranceNode。那么将node添加到entranceNode的子节点中去之后就变成了下面这样: 紧接着再进入一次,资源名不同,会再次生成一个新的Entry,上面的图形就变成下图这样: 此时再次调用context的getLastNode方法,因为此时curEntry的parent不再是null了,所以获取到的lastNode是curEntry.parent.curNode,在上图中可以很方便的看出,这个节点就是node0。那么把当前节点node1添加到lastNode的子节点中去,上面的图形就变成下图这样: 然后将当前node设置给context的curNode,上面的图形就变成下图这样: 假如再创建一个Entry,然后再进入一次不同的资源名,上面的图就变成下面这样: 至此NodeSelectorSlot的基本功能已经大致分析清楚了。 PS:以上的分析是基于每次执行SphU.entry(name)时,资源名都是不一样的前提下。如果资源名都一样的话,那么生成的node都相同,则只会再第一次把node加入到entranceNode的子节点中去,其他的时候,只会创建一个新的Entry,然后替换context中的curEntry的值。 NodeSelectorSlot的entry方法执行完之后,会调用fireEntry方法,此时会触发ClusterBuilderSlot的entry方法。 ClusterBuilderSlot的entry方法比较简单,具体代码如下: @Overridepublicvoidentry(Contextcontext,ResourceWrapperresourceWrapper,DefaultNodenode,intcount,Object...args)throwsThrowable{if(clusterNode==null){synchronized(lock){if(clusterNode==null){//Createtheclusternode.clusterNode=Env.nodeBuilder.buildClusterNode();//将clusterNode保存到全局的map中去HashMap 一、为每个资源创建一个clusterNode,然后把clusterNode塞到DefaultNode中去 二、将clusterNode保持到全局的map中去,用资源作为map的key PS:一个资源只有一个ClusterNode,但是可以有多个DefaultNode StatisticSlot负责来统计资源的实时状态,具体的代码如下: 这些统计的实时数据会被后续的校验规则所使用,具体的统计方式是通过滑动窗口来实现的。 SystemSlot就是根据总的请求统计信息,来做流控,主要是防止系统被搞垮,具体的代码如下: 当前的统计值和系统配置的进行比较,各个维度超过范围抛BlockException AuthoritySlot做的事也比较简单,主要是根据黑白名单进行过滤,只要有一条规则校验不通过,就抛出异常。 @Overridepublicvoidentry(Contextcontext,ResourceWrapperresourceWrapper,DefaultNodenode,intcount,booleanprioritized,Object...args)throwsThrowable{checkBlackWhiteAuthority(resourceWrapper,context);fireEntry(context,resourceWrapper,node,count,prioritized,args);}voidcheckBlackWhiteAuthority(ResourceWrapperresource,Contextcontext)throwsAuthorityException{//通过监听来的规则集Map @Overridepublicvoidentry(Contextcontext,ResourceWrapperresourceWrapper,DefaultNodenode,intcount,booleanprioritized,Object...args)throwsThrowable{checkFlow(resourceWrapper,context,node,count,prioritized);fireEntry(context,resourceWrapper,node,count,prioritized,args);}voidcheckFlow(ResourceWrapperresource,Contextcontext,DefaultNodenode,intcount,booleanprioritized)throwsBlockException{checker.checkFlow(ruleProvider,resource,context,node,count,prioritized);}DegradeSlotDegradeSlot主要是根据前面统计好的信息,与设置的降级规则进行匹配校验,如果规则校验不通过则进行降级,具体的代码如下: @Overridepublicvoidentry(Contextcontext,ResourceWrapperresourceWrapper,DefaultNodenode,intcount,booleanprioritized,Object...args)throwsThrowable{performChecking(context,resourceWrapper);fireEntry(context,resourceWrapper,node,count,prioritized,args);}voidperformChecking(Contextcontext,ResourceWrapperr)throwsBlockException{List 创建DefaultProcessorSlotChain对象时,首先创建了首节点,然后把首节点赋值给了尾节点,可以用下图表示: 将第一个节点添加到链表中后,整个链表的结构变成了如下图这样: 将所有的节点都加入到链表中后,整个链表的结构变成了如下图所示: 这样就将所有的Slot对象添加到了链表中去了,每一个Slot都是继承自AbstractLinkedProcessorSlot。 而AbstractLinkedProcessorSlot是一种责任链的设计,每个对象中都有一个next属性,指向的是另一个AbstractLinkedProcessorSlot对象。其实责任链模式在很多框架中都有,比如Netty中是通过pipeline来实现的。 知道了SlotChain是如何创建的了,那接下来就要看下是如何执行Slot的entry方法的了 从这里可以看到,从fireEntry方法中就开始传递执行entry了,这里会执行当前节点的下一个节点transformEntry方法,上面已经分析过了,transformEntry方法会触发当前节点的entry,也就是说fireEntry方法实际是触发了下一个节点的entry方法。 从最初的调用Chain的entry()方法,转变成了调用SlotChain中Slot的entry()方法。 从@SpiOrder(-10000)知道,SlotChain中的第一个Slot节点是NodeSelectorSlot。 sentinel的限流降级等功能,主要是通过一个SlotChain实现的。 在链式插槽中,有7个核心的Slot,这些Slot各司其职,可以分为以下几种类型: 一、进行资源调用路径构造的NodeSelectorSlot和ClusterBuilderSlot 二、进行资源的实时状态统计的StatisticsSlot 三、进行系统保护,限流,降级等规则校验的SystemSlot、AuthoritySlot、FlowSlot、DegradeSlot 后面几个Slot依赖于前面几个Slot统计的结果。至此,每种Slot的功能已经基本分析清楚了。 因此,Sentinel消耗的内存,至少是资源总数乘以每个资源对应的Node占用的内存大小,每个Node占用的内存大小即为一个大小为2的Bucket数组和一个大小为60的Bucket数组所占用的内存。 可以看到Sentinel在性能方面所做出的努力,Sentinel尽最大可能降低自身对应用的影响。 滑动窗口可以先拆为滑动跟窗口两个词,先介绍下窗口, 这个样子我们就能将1分钟就可以划分成60个窗口了,这个没毛病吧。 如下图我们就分成了60个窗口(这个多了我们就画5个表示一下) 比如现在处于第1秒上,那1s那个窗口就是当前窗口,就如下图中红框表示。 好了,窗口就介绍完了,现在在来看下滑动, 这个时候下一个窗口就变成了当前窗口,之前那个当前窗口就变成了上一个窗口,这个过程其实就是滑动。 好了,介绍完了滑动窗口,我们再来介绍下这个sentinel的滑动窗口的实现原理。 其实你要是理解了上面这个滑动窗口的意思,sentinel实现原理就简单了。 先是介绍下窗口中里面都存储些啥。也就是上面这个小框框都有啥。 说完了这一个小窗口里面的东西,就得来说说是怎么划分这个小窗口,怎么管理这些小窗口的了,也就是我们的视野得往上提高一下了,不能总聚在这个小窗口上。 窗口startTime=1609085401454-1609085401454%1000(窗口长度)=454这里1609085401454%1000(窗口长度)能算出来它的毫秒值,也就是454,减去这个后就变成了1609085401000 好了,sentinel滑动窗口原理就介绍完成了。 我们来介绍下使用这个滑动窗口都来统计啥 publicenumMetricEvent{/***Normalpass.*/PASS,//通过/***Normalblock.*/BLOCK,//拒绝的EXCEPTION,//异常SUCCESS,//成功RT,//耗时/***Passedinfuturequota(pre-occupied,since1.5.0).*/OCCUPIED_PASS}这是最基本的指标,然后通过这些指标,又可以计算出来比如说最大,最小,平均等等的一些指标。 我们先来看下这个窗口里面的统计指标的实现MetricBucket 这个MetricBucket是由LongAdder数组组成的,一个LongAdder就是一个MetricEvent,也就是第二小节里面的PASS,BLOCK等等。我们稍微看下就可以了 可以看到它在实例化的时候创建一个LongAdder数据,个数就是那堆event的数量。这个LongAdder是jdk8里面的原子操作类,你可以把它简单认为AtomicLong。然后下面就是一堆get跟add的方法了,这里我们就不看了。接下来再来看看那这个窗口的实现WindowWrap类 先来看下这个成员 窗口长度,窗口startTime,指标统计的都有了,下面的就没啥好看的了,我们再来看下的一个方法吧 接下来再来看下这个管理窗口的类LeapArray 看下它的成员,窗口长度,样本数sampleCount也就是窗口个数,intervalInMs,再就是窗口数组看看它的构造方法 这个构造方法其实就是计算出来这个窗口长度,创建了窗口数组。 问题说明:考察对限流算法的掌握情况 限流:对应用服务的请求做限制,避免因过多的请求而导致服务器过载甚至宕机。限流算法常见的包括两种:1.计算器算法,有包括窗口计算器算法,滑动窗口算法 2.令牌桶算法(TokenBucket) 3.漏桶算法(LeakyBucket) 观上图,这种算法使用问题的,在4500-5500ms这1s内有6个请求通过。 滑动窗口计数器算法会对一个窗口分为n个更小的区间,例如: 0500100015002000比如是1250ms时来个请求,1250-1000=250,250后面的第一个时区是500-100。而1250在1000-1500中;所以这个滑动窗口是500-1000。 观上图,其实还是有问题,可以将区间数量设置越小,限流就越准确,但是还是不能100%准确。 漏桶算法是对令牌桶算法的改进 漏桶实现用阻塞队列 例如:QPS=5,意味这没200ms处理一个队列中的请求,timeout=2000,意味着预期等待超过2000ms的请求会被拒绝并抛出异常。 线程隔离有两种方式实现: Sentinel与Hystix的线程隔离有什么差别? Hystix默认是基于线程池实现线程隔离,每个被隔离的业务都要创建一个独立的线程池,线程过多会带来额外的CPU开销,性能一般,但是隔离性更强 Sentinel是基于信号量(计算器)实现的线程隔离,不用线程池,性能较好,但是隔离性一般。 Sentinel提供两种方式修改规则: 手动通过API修改比较直观,可以通过以下几个API修改不同的规则: FlowRuleManager.loadRules(List 大量规则场景,一般通过动态规则源的方式来动态管理规则。 一般来说,规则的推送有下面三种模式: 上述loadRules()方法只接受内存态的规则对象,但更多时候规则存储在文件、数据库或者配置中心当中。DataSource接口给我们提供了对接任意配置源的能力。相比直接通过API修改规则,实现DataSource接口是更加可靠的做法。 生产环境下一般更常用的是push模式的数据源。对于push模式的数据源,如远程配置中心(ZooKeeper,Nacos,Apollo等等),推送的操作不应由Sentinel客户端进行,而应该经控制台统一进行管理,直接进行推送,数据源仅负责获取配置中心推送的配置并更新到本地。因此推送规则正确做法应该是配置中心控制台/Sentinel控制台→配置中心→Sentinel数据源→Sentinel,而不是经Sentinel数据源推送至配置中心。这样的流程就非常清晰了: DataSource扩展常见的实现方式有: Sentinel目前支持以下数据源扩展: 详细说明,可参考官网: [c.a.c.s.datasource.converter.SentinelConverter]line80:convertercannotconvertrulesbecausesourceisemptynaocs如下: 这里对于dataId使用了${spring.application.name}变量,这样可以根据应用名来区分不同的规则配置。 在Nacos控制台,对应的namespace,新建一个json配置文件:service-order-flow-rules,如下: 其中:DataID、Group就是上面第5点中配置的内容。 配置格式选择JSON,并在配置内容中填入下面的内容: [{"resource":"/test","limitApp":"default","grade":1,"count":10,"strategy":0,"controlBehavior":0,"clusterMode":false}]可以看到上面配置规则是一个数组类型,数组中的每个对象是针对每一个保护资源的配置对象,每个对象中的属性解释如下: 启动service-order应用,注册到nacos到,打开Sentinel控制台,可以看到上面nacos新建的限流规则,如下: 注意: 在完成了上面的整合之后,对于接口流控规则的修改就存在两个地方了:Sentinel控制台、Nacos控制台。 这个时候,通过Nacos修改该条规则是可以同步到Sentinel的,但是通过Sentinel控制台修改或新增却不可以同步到Nacos。 因为当前版本的Sentinel控制台不具备同步修改Nacos配置的能力,而Nacos由于可以通过在客户端中使用Listener来实现自动更新。 所以,在整合了Nacos做规则存储之后,需要知道在下面两个地方修改存在不同的效果: 下面我们进通过修改,使得Nacos与Sentinel可以互相同步限流规则: 要通过Sentinel控制台配置集群流控规则,需要对控制台进行改造。主要改造规则可以参考: 控制台监听Nacos配置变化,如发生变化就更新本地缓存。从而让控制台本地缓存总是和Nacos一致。 2.1通关git官网下载Sentinel源代码,如下: 2.2修改sentinel-dashboard控制台模块的pom.xml,将test注释掉 sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar.html并找到如下代码段后,并把注释打开,名称也稍作修改。 修改后: 找到如下目录(位于test目录) sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos将整个目录拷贝到 sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos修改com.alibaba.csp.sentinel.dashboard.controller.v2.FlowControllerV2.java 修改如下: 其中,注入的两个bean: FlowRuleNacosProvider.java如下,无需修改 spring.cloud.sentinel.datasource.ds1.nacos.server-addr=192.168.100.80:8848spring.cloud.sentinel.datasource.ds1.nacos.dataId=${spring.application.name}-flow-rulesFlowRuleNacosPublisher.java如下,无需修改: 打开NacosConfigUtil.java,如下两个地方,需要和上面使用nacos存储时的配置一致。注意:两边的DataId和GroupId必须对应上。 打开NacosConfig.java,修改如下,主要是nacos配置中心的地址与namespace隔离环境的配置修改,如果没有设置namespace,就可以不设置PropertyKeyConst.NAMESPACE。 经过以上步骤就已经把流控规则改造成推模式持久化了。 2.5编译生成jar包 执行命令 mvncleanpackage-DskipTests编译成功后,在项目的target目录可以找到sentinel-dashboard.jar,执行以下命令可以启动控制台: java-jarsentinel-dashboard.jar打开Sentinel控制台,可以看到上面通过nacos新建的限流规则 我们可以尝试在Sentinel控制台修改该规则,看是否能同步推送到Nacos,这里我们修改阈值为15,打开Nacos配置中心,可以看到已经更新过来了。 下面我们通过修改Nacos将阈值再修改为20,刷新Sentinel,也能同步过来,如下: 通过测试发现,在Sentinel控制台修改规则可以同步到Nacos,或者在Nacos上修改规则也可以同步到Sentinel控制台。 尼恩团队15大技术圣经,使得大家内力猛增, 可以充分展示一下大家雄厚的“技术肌肉”,让面试官爱到“不能自已、口水直流”,然后实现”offer直提”。 很多小伙伴刷完后,吊打面试官,大厂横着走。 ……完整版尼恩技术圣经PDF集群,请找尼恩领取 《尼恩架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓