高并发高可用必须掌握这块
采用sentinel
sentinel和hystrix的区别
https://sentinelguard.io/zh-cn/blog/sentinel-vs-hystrix.html
执行模式
Hystrix资源其实采用了命令模式,将调用和调用结果封装到一个HystrixCommand 的命令类里边。底层的一个执行其实是基于RX java实现的。不过我们在需要需要在使用的时候,需要对每一个命令类规定一个 commandKey 和 groupKey来区分不同的调用
sentinel只是将我们的这个resource,我们的资源和这个slotChain进行一个对应,然后put到我们的这个chainMap里边。然后再进行调用的过程中,通过链式的对这个里边的这个slot进行一个调用,达到这种效果,这是两种不同的机制
隔离设计
Hystrix隔离机制有两种,一种是线程池,一种是信号量
sentinel这边是不支持线程池的。使用信号量,curThreadNum按资源所能并发占用的最大线程数实现限流
线程池这部分其实怎么说呢?它能够非常有效的把资源进行隔离。但是在使用Hystrix如果使用这个线程池隔离的话,也会带来一些问题,比如说线程的这个开销问题。线程资源浪费。而且一个服务的话,如果有多个重要接口,然后创建多个command,然后创建多个线程的隔离机制,那么就会导致线程资源的一个碎片化
熔断降级
sentinel支持的这个种类比较多。Hystrix它只支持一些比如说异常调用比例,而这个sentinel还能支持平均响应时间来进行这个熔断降级
实时指标统计的实现
都是基于滑动窗口的
Hystrix 1.5将指标统计数据结构抽象成了响应式流(reactive stream)
sentinel默认的实现是基于 LeapArray 的高性能滑动窗口
限流
sentinel有,且丰富
流量整形策略
- 直接拒绝模式:即超出的请求直接拒绝
- 慢启动预热模式:当流量激增的时候,控制流量通过的速率,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮
- 匀速器模式:利用 Leaky Bucket 算法实现的匀速模式,严格控制了请求通过的时间间隔
- 支持 基于调用关系的限流,包括基于调用方限流、基于调用链入口限流、关联流量限流等
- 支持更细维度的 热点参数限流
系统负载保护
sentinel有
对系统的维度提供自适应保护策略 让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。SystemSlot实现
黑白名单
AuthoritySlot实现
插件机制
责任链、SPI支持自定义的slot扩展
控制台
sentinel基本概念
资源
资源可以是一个方法、一段代码、一个Servlet接口、一个RPC接口,通常用于指代一个接口
![[Pasted image 20240717215240.png]]
规则
围绕资源的实时指标数据设置的规则,包括流量控制规则、熔断降级规则、系统自我保护规则和自定义的规则
资源指标数据
Sentinel以资源为维度统计指标数据,资源的实时指标数据反映了资源的实时状态,这些指标包括每秒请求数、请求平均耗时、每秒异常总数等。
使用ClusterNode统计每个资源的全局指标数据,以及针对不同调用来源,分别统计资源的指标数据
Sentinel会创建一个名称为sentinel_spring_web_context的入口节点,类型为EntranceNode。一个Web应用可能有多个接口,但它们的入口节点都是sentinel_spring_web_context,而入口节点的childList就用于存储每个接口的DefaultNode实例。
两类processorSlot
- 负责资源指标统计的
- NodeSelectorSlot 为调用链上的资源创建DefaultNode实例,相同调用链上的每个资源仅会创建一个DefaultNode实例
- ClusterBuilderSlot 为调用链上的资源创建ClusterNode实例,以及对于不同调用来源,为调用链上的资源都创建一个StatisticNode实例
- StatisticSlot 真正用于实现资源指标数据统计的处理器插槽,它会先调用后续的ProcessorSlot类的entry方法判断是否放行请求,再根据结果执行相应的资源指标数据统计操作
- 负责限流熔断等流控功能的
- AuthoritySlot:实现黑白名单限流
- SystemSlot:实现系统自适应限流
- FlowSlot:实现QPS/Threads限流
- DegradeSlot:实现熔断降级
SPI的使用
增强:在加载完后,还会再根据order排序
默认顺序
责任链模式
由前一个AbstractLinkedProcessorSlot实例调用fireEntry方法或fireExit方法,在fireEntry方法与fireExit方法中调用下一个AbstractLinkedProcessorSlot实例(next)的entry方法或exit方法
会先创建一个资源的entry。每一个资源它会对应一个slot chain,而且会将资源和slot chain进行一个关系对应,把它保存到一个全局的一个静态变量的chain map里边。像系统中所有的资源访问都要经过这个chainmap。所以对于chain map来说,它在整个的一个调用过程中,它是一个热点的,而且会有线程频繁竞争的这样一个热点数据源码
它的一个设计其实让我感觉到共鸣非常大。因为它并没有直接去使用concurrent哈希map,而依然使用的是这种线程非安全的hash map。而且它的一个代码设计其实是模仿的是双重检查锁的这样一个代码模型。也就是说在我们这个chainMap进行读取操作的时候,它先在外层进行一次判空。如果为空的话才进行synchronized的加锁。如果获取锁成功,它在内层还会进行二次的一个判空。也就是说如果还是为空的话,才会为当前的一个资源创建新的resource对应的slot chain,然后添加到的这个chainMap里边。
而且这一块儿对chainMap的一个添加,它并没有直接用map.put的方法,而是采用了一种copyOnWrite的一种思想,进行了一个新的临时的map的创建。把这个之前的china map里边的元素全部放到新的临时的map里边。而且再把新的添加的对应关系也放到新的临时的map里边。然后再把这个临时的map赋值给之前的chainMap,完成了最后的一个赋值。这样其实在很大程度上提高了这种并发的一个读写能力。
为啥不用concurrentHashMap?
concurrentHashMap直接使用的话,其实在普通并发场景下面性能应该还行。而且对于JDK1.8开始,他对concurrentHashMap由之前的这种分段锁,然后细力度到了单个数组桶位首节点的这样锁。而且通过synchronized的锁加CAS的形式提高了这部分并发的一个性能。在JDK里边concurrentHashMap应该是属于并发性能最高的这种map结构。但是对于现在目前互联网这种这么大量的一个并发操作访问的话,单独的去使用JDK自带的这个concurrentHashMap这个数据结构来保证并发安全的话,应该不会太好。
这部分代码之所以去采用了一个双重检查锁的这样的一个机制,其实就是为了避免某些情况下对synchronized锁的一个争抢。因为它在外层它会先进行一次判空,在这种情况下,如果他不为空的话,就会放弃对synchronized锁的一个争抢。这样就避免了当前线程从用户态到内核态的一个转化。也就是说一次上下文的一个切换。而用双重检查锁的话,有一部分场景也就是说在这个chainMap.get这个resource的时候,如果不为none的时候,这部分它不会经历这个线程阻塞上下文切换,状态转化这一部分内容。这样的话它的性能是非常高的。
而对于concurrentHashMap,因为它没有外层的这种判空,一旦发生这种场景的话,concurrentHashMap就会直接会访问到synchronized的这个锁上。那么就会造成现场的一个竞争,也会导这一个线程的状态的一个转化,会造成这种线程的一个耗时。所以说从这个角度说,他不会直接去使用concurrentHashMap,因为这样的话没有目前这种代码设计效率高。
为什么要先创建一个临时的map,然后再去做它的一个赋值的操作呢?
这部分其实我也思考了一段时间。起初看这部分代码,觉得它就是一个浪费空间,而且没有任何意义的一个代码操作。随后琢磨明白了,因为这个chain map它是一个用volatile这个关键词修饰的一个静态变量,而且volatile关键词它其实是为了保证这个chain map的一个可见性,也就是说如果一个线程对chain map有进行修改的话,其他的线程应该是能够对这个修改是可见的。如果直接使用chain map.put进行这种新的元素的一个添加的话,那么对于chain map来说,它的一个内存的一个指向并没有发生改变。也就是说对其他线程来说,认为当前的chain map并没有发生改变,那么这个时候其他线程就不可见,只有当前线程当前修改的这条线程,它所持有的chain map里边的这个数据才是最新的一个数据。那么就会导致其他的线程与当前线程。所持有的chain map的数据不一致,这就会造成一个问题。所以他会通过一个copy on right的一个思想,他先会创建一个临时的一个map,然后把之前的拆map里边的内容和现在先要添加的这些内容全部放到里边。
然后最终给这个chain map进行一个赋值的一个操作。也就是让这个源chain map把它的这个地址指针指向新的一个内存地址,这样的话其他线程就能够有这种可见性。因为这个chain map它确确实实是改变了,这样的话保证它的一个可见性。那么对于所有的线程来说,对于同一个静态资源访问的这种情况,保证可见性,它就能保证数据的一个一致性,这是他这样做的一个原因。所以从这个角度来说,他应该是采用的是用空间去换去保证数据的一致性这种情况。
那么copy on write这种思想的话,还有没有其他的一些好处?
其实更类似于有点像MVCC那样,它会做一个snapshot,读取的过程里边会走snapshot,然后写入的时候会在新创建的这个view里边去做。所以copy on write这一块其实它在新创建一个临时的一个chain map的时候,对于原来的chain map来说就是一个snapshot。如果在这种读写环境下,读的话,其实还是从之前的chain map里进行读,类似于读一个snapshot的一个副本。写的时候它会在新的这个chain map里边去写。这种情况也可以很好的去保证它的一个读写的一个并发效果
然后其次还有一点就是对于源码里边,它在进行新的临时的chain map创建的时候,它对于cchain map的size其实是进行了一个初始化,是用的原chain map的size加1。这其实也就避免在map进行新元素添加过程中而引发的扩容导致的一个性能的一个问题。
双重检查锁创建对象那三步导致的一个问题。除了用volitale修饰这个对象以外,有没有其他的一个方式解决这个问题?
有其他方式。其实这部分代码如果不在它前面加volatile这个关键词的话,可以直接在这个双重检查锁里边处理。
New对象这部分的代码从一句换成两句。我说一下示例代码像一句的话,我会直接instance等于new一个instance。那么如果不用这个volatile修饰的话,我们可以把这一句代码拆成两句。第一句代码就是temp instance=new一个instance。然后第二句代码就是instance=temp instance。也就是说因为我们最终在外层判断的是这个instance对象的一个空与否,并不关注里边的这个temp instance。所以对里边temp instance的一个指令重排,创建过程中的一个指令重排序,对我们外层来说没有任何影响。
而最终有影响的就是我们最终的将我们的temp instance赋值给instance这一步。而这一步因为它是在这个new一个temp instance后边执行的,而且是完全不同的两行代码。这个时候的话其实最终赋值给instance的已经是一个完完全全的new的一个对象了。所以这个时候外层再进行判空的话,哪怕没有了volatile修饰的话也不会出问题。
那么添加上volatile这个关键词之后,它还会不会进行一个重排序呢?
其实添加上volatile的话也会重排序,volatile添加屏障的这个禁止指令重排序,它不会是深入到new对象更更底层更深层次的这个创建步骤里边。这里边volatile它其实不是禁止了对象创建的这个步骤的重排序,它是禁止了外层线程判空和内层线程创建对象的一个重排序。也就是说因为volatile修饰这个对象,内层线程在new这个对象的时候,其实是对这个对象的写。
对于volatile这一块的话,它有一个读写的一个屏障,也就是store load屏障。那么这个时候如果外层线程想要对这个对象进行读取的话,这个时候都是不允许的。因为在这个汇编层面也有lock指令去做这个缓存的一个锁定。所以这个时候他必须等待这个对象完完全全的创建完成之后,他才能够真正的去读取到这个对象。这个时候其实对象创建完了,哪怕是通过第二步和第三步创建对象的一个指令乱序也不会对外层造成一个影响。
当然了其实volatile本身它就不能禁止这个创建对象里边这三个步骤的指令重排序。只不过是从这个效果上来看,好像是禁止了里边的重排序,但是实际意义上并没有禁止。
还有其他觉得有共鸣的地方吗?
有的,其实整个slot chain的一个设计让我感到比较有共鸣。你像看到这个slot chain的一个设计,其实我会想到spring里面的filter的设计,也会想到ninety里边的这个handler管道的设计。像这样的设计其实有很多异曲同工的地方。而且slot chain它的一个设计也加入了自己的一个独有的特色,详细的展开说一说这部分。其实spring的filter就是一个筛选的一个链条,他有自己默认的filter,我们也可以通过继承的方式实现自定义的filter。而且每一个filter都有自己的职责,职责单一,这些filter可以相互配合,而且互不影响,包括netty里边的这个handler管道也是这样的。
在接收到IO消息以后,我们可以通过用不同的handler对数据进行一些清洗验证或者转化入库等操作。slot它的一个设计也是在这种理念上进行的。如果我们从代码上看的话,它在初始创建entry的话,就会通过这个processor slot chain的build方法,把这些slot添加到我们的这个chain里边。像默认的比较重要的有这个node selector,cluster builder slot, 还有我们的这个static slot这是一个比较核心的我们的slot,还有像system系统的,还有我们的这个auth的slot 、flow,还有degree slot。
每一种slot它都有自己去处理的这种职责。而且也可以自定义,sentinel它里边的slot chain builder,它是SPI接口进行扩展的。所以我们要是想自定义的话,也是没有任何问题的。这样的设计其实能够感觉到他的一个职责非常清晰。每一种甭管是filter或者是handler,还是现在咱们提的这种slot也好,他都有这种单一职责在里边,而且扩展性非常高。我们可以复写也好,或者是通过一些配置,将他们的这个顺序进行一些调整都可以,而且灵活性也非常高。
slot chain的一个特色,那特色体现在哪里?
这个特色其实是体现在这些slot之间,它也是由相互合作相互调用的。但是对于这个spring的filter或者是ninety的这个handler,他们filter与filter之间,handler与handler之间都是非常独立的。就是说每一个保持单一职责互不影响,但是对于slot chain的一个设计里边,就有非常明显的一个互相调用互相合作的这种情况在里边。
我先来说一下slot chain整体的一个调用流程,然后在这个流程里边会有两处地方能够体现出来这种互相调用互相合作的来保证这个sentinel它对这个数据统计的一个正确性。
默认情况下的话,它会先进入node selector slot这一块。它其实主要负责记录这个访问源。它会生成这个调用链的一个根结点,然后去访问我们的集群,创建slot,也就是cluster build slot。
这一部分其实它有两个工作。第一就是它会生成这个统计数据结构,然后把这部分数据结构给static slot。它还有另外一个功能,就是它要存储这些统计上来的一个数据。所以这个时候static slot收到这个消息之后,他会把自己统计上来的实时的监控数据回传给我们的cluster build slot。然后在cluster build slot里最终完成这个数据的一个存储。当然cluster build slot它也会连着一个控制台,可以直接把这部分数据输出到控制台里边,然后供我们实时观看。也就是说这一块其实就体现了这两个slot之间它其实是有互相沟通互相调用的。
然后过完这个static slot,它会依次进入链条式的一个传递,比如说先过我们的这个system slot,一个总流量的一个入口。它会进入我们的这个auth slot,进行一个权限控制黑白名单,然后再过我们的flow slot。这里边主要是去检查之前的一些预设规则,然后根据这个实时统计的一些数据流量,然后做最后的一些流量限制。最后它会经过一个叫degree slot。这一块儿的话,其实就会进行最终的熔断或者是一个降级处理。
但是进行这个degrade slot的时候,它又有一个回调的一个问题。因为degree slope它还会将最终的一个验证结果发还给static slot。因为static slot它其实是一个监控数据的一个比较核心的这样一个slot。发回去之后,slot也作为自己监控数据的一部分,最终再传回给cluster build slot。然后cluster build slot它会连着控制台去这样做显示。所以整个的话与其说它是一个调用链路,不如说它是一个半调用环路。因为它slot与slot之间其实是有这种配合机制在里边的这是它的一个特色。这样它能够保证整个的一个数据统计是非常正确的。
有没有看过具体数据统计那部分的代码?
底层采用高性能的滑动窗口数据结构 LeapArray
来统计实时的秒级指标数据,