写点什么

【Spring Cloud 8】熔断与限流 Sentinel,java 现在的主流技术

作者:Java高工P7
  • 2021 年 11 月 10 日
  • 本文字数:4420 字

    阅读完需:约 15 分钟

| 功能 | Sentinel | Hystrix |


| --- | --- | --- |


| 隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 |


| 熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 |


| 实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于 RxJava) |


| 动态规则配置 | 支持多种数据源 | 支持多种数据源 |


| 扩展性 | 多个扩展点 | 插件形式 |


| 基于注解的支持 | 支持 | 支持 |


| 限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 |


| 流量整形 | 支持预热模式、匀速器模式、预热排队模式 | 不支持 |


| 系统自适应保护 | 支持 | 不支持 |


| 控制台 | 可配置规则、查看秒级监控、机器发现等 | 简单的监控查看 |


三、Sentinel 特性





1、丰富的应用场景


Sentinel 承接了阿里巴巴近十年双十一大促流量的核心场景,例如秒杀(突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等。


2、完备的实时监控


Sentinel 同时提供实时的监控功能。我们可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总进行情况。


3、广泛的开源生态


Sentinel 提供开箱即用的与其它开源框架的整合模块,例如与 spring cloud、dubbo、grpc 的整合。我们只需要引入响应的依赖并进行简单的配置即可快速的接入 Sentinel。


4、完美的 SPI 扩展点


Sentinel 提供简单易用、完善的 SPI 扩展点。我们可以通过实现扩展点,快速的定制逻辑。例如定制规则管理、适配数据源等。


四、资源和规则




资源是 Sentinel 的关键概念。它可以是 java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。只是通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来,大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来表示资源。


围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。


Sentinel 中调用 SphU 或者 SphO 的 entry 方法获取限流资源,不同的是前者获取限流资源失败时会跑 BlockException 异常,后者返回 false,二者的实现都是基于 CtSph 类完成的。



五、核心概念



1、Resource

resource 是 Sentinel 中最重要的一个概念,Sentinel 通过资源来保护具体的业务代码或其它后方服务。Sentinel 把复杂的逻辑给屏蔽了,用户只需要为受保护的代码或服务定义一个资源,然后定义规则就可以了,剩下的通通交给 Sentinel 来处理。并且资源和规则是解耦的,规则甚至可以在运行时动态修改。定义完资源后,就可以通过在程序中埋点来保护你自己的服务,埋点的方式有两种:


(1)try-catch 方式(通过 SphU.entry(...)),当 catch 到 BlockException 时执行异常处理或 fallback。


(2)if-else 方式(通过 SphO.entry(...)),当返回 false 时执行异常处理或 fallback。


以上两种方式都是通过硬编码的形式定义资源然后进行资源埋点的,对业务代码的侵入太大,从 0.1.1 版本开始,Sentinel 加入了注解的支持,可以通过注解来定义资源,具体的注解为:SentinelResource。通过注解除了可以定义资源外,还可以指定 blockHandler 和 fallback 方法。


在 Sentinel 中具体表示资源的类:ResourceWrapper,它是一个抽象的包装类,包装了资源的 Name 和 EntryType。他有两个实现类,分别是:StringResourceWrapper 和 MethodResourceWrapper。顾名思义,StringResourceWrapper 是通过对一串字符进行包装,是一个通用的资源包装类,MethodResourceWrapper 是对方法调用的包装。

2、Context

Context 是对资源操作时的上下文环境,每个资源操作(针对 resource 的 entry 和 exit)必须属于一个 Context,如果程序中未指定 Context,会创建 name 为“Sentinel_default_context”的默认 Context。一个 Context 生命周期内可能有多个资源操作,Context 生命周期内的最后一个资源 exit 时会清理该 Context,这也预示着整个 Context 生命周期的结束。Context 主要属性如下:


public class Context {


// context 名字,默认名字 "sentinel_default_context"


private final String name;


// context 入口节点,每个 context 必须有一个 entranceNode


private DefaultNode entranceNode;


// context 当前 entry,Context 生命周期中可能有多个 Entry,所有 curEntry 会有变化


private Entry curEntry;


// The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP).


private String origin = "";


private final boolean async;


}


一个 Context 生命周期内 Context 只能初始化一次,存到 ThreadLocal 中,并且只有在非 NULL 时才会进行初始化。如果想在调用 SphU.entry()或 SphO.entry()前,自定义一个 context,则通过 ContextUtil.enter()方法来创建。context 保存在 ThreadLocal 中,每次执行的时候会优先到 ThreadLocal 中获取,为 null 时会创建一个 context。当 Entry 执行 exit 方法时,如果 entry 的 parent 节点为 null,表示当前 context 中最外层的 entry 了,此时将 threadLocals 中的 context 清空。

3、Entry

每次执行 SphU.entry()或 SphO.entry()都会返回一个 Entry,Entry 表示一次资源操作,内部会保存单签 invocation 信息。在一个 context 声明周期中多次资源操作,也就是对应多个 Entry,parent/child 结构保存在 Entry 实例中,Entry 类 CtEntry 结构如下:


class CtEntry extends Entry {


protected Entry parent = null;


protected Entry child = null;


protected ProcessorSlot<Object> chain;


protected Context context;


}


public abstract class Entry implements AutoCloseable {


private long createTime;


private Node curNode;


/**


  • {@link Node} of the specific origin, Usually the origin is the Service Consumer.


*/


private Node originNode;


private Throwable error; // 是否出现异常


protected ResourceWrapper resourceWrapper; // 资源信息


}


4、DefaultNode

Node 默认实现类 DefaultNode,该类还有一个子类 EntranceNode;context 有一个 entranceNode 属性,Entry 中有一个 curNode 属性。


  • EntranceNode:该类的创建在初始化 context 时完成的,注意该类是针对 context 维度的,也就是一个 context 有且仅有一个 EntranceNode。

  • DefaultNode:该类的创建是在 NodeSelectorSlot.entry 完成的,当不存在 context.name 对应的 DefaultNode 时会创建并保存在本地缓存;获取到 context.name 对应的 DefaultNode 后将该 DefaultNode 设置到当前 context 的 curEntry.curNode 属性,也就是说,在 DefaultSelectorSlot 中是一个 context 有且仅有一个 DefaultNode。


看到这里,你是不是有疑问?为什么一个 context 有且仅有一个 DefaultNode,我们的 resouece 跑哪去了呢,其实,这里的一个 context 有且仅有一个 DefaultNode 是在 NodeSelectorSlot 范围内,NodeSelectorSlot 是 ProcessorSlotChain 中的一环,获取 ProcessorSlotChain 是根据 Resource 维度来的。总结为一句话就是:针对同一个 Resource,多个 context 对应多个 DefaultNode;针对不同 Resource,(不管是否是同一个 context)对应多个不同 DefaultNode。这还没看明白 : ),好吧,我不 bb 了,上图吧:



DefaultNode 结构如下:


public class DefaultNode extends StatisticNode {


private ResourceWrapper id;


/**


  • The list of all child nodes.

  • 子节点集合


*/


private volatile Set<Node> childList = new HashSet<>();


/**


  • Associated cluster node.


*/


private ClusterNode clusterNode;


}


一个 Resouce 只有一个 clusterNode,多个 defaultNode 对应一个 clusterNode,如果 defaultNode.clusterNode 为 null,则在 ClusterBuilderSlot.entry 中会进行初始化。


同一个 Resource,对应同一个 ProcessorSlotChain,这块处理逻辑在 lookProcessChain 方法中,如下:


ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {


ProcessorSlotChain chain = chainMap.get(resourceWrapper);


if (chain == null) {


synchronized (LOCK) {


chain = chainMap.get(resourceWrapper);


if (chain == null) {


// Entry size limit.


if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {


return null;


}


chain = SlotChainProvider.newSlotChain();


Map<ResourceWrapper, ProcessorSlotChain> newMap = newHashMap<ResourceWrapper, ProcessorSlotChain>(


chainMap.size() + 1);


newMap.putAll(chainMap);


newMap.put(resourceWrapper, chain);


chainMap = newMap;


}


}


}


return chain;


}

5、StatisticNode

StatisticNode 中保存了资源的实时统计数据(基于滑动时间窗口机制),通过这些统计数据,sentinel 才能进行限流、降级等一系列操作。StatisticNode 属性如下:


public class StatisticNode implements Node {


/**


  • 秒级的滑动时间窗口(时间窗口单位 500ms)


*/


private transient volatile Metric rollingCounterInSecond = newArrayMetric(SampleCountProperty.SAMPLE_COUNT,


IntervalProperty.INTERVAL);


/**


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


  • 分钟级的滑动时间窗口(时间窗口单位 1s)


*/


private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000,false);


/**


  • The counter for thread count.

  • 线程个数用户触发线程数流控


*/


private LongAdder curThreadNum = new LongAdder();


}


public class ArrayMetric implements Metric {


private final LeapArray<MetricBucket> data;


}


public class MetricBucket {


// 保存统计值


private final LongAdder[] counters;


// 最小 rt


private volatile long minRt;


}


其中 MetricBucket.counters 数组大小为 MetricEvent 枚举值的个数,每个枚举对应一个统计项,比如 PASS 表示通过个数,限流可根据通过的个数和设置的限流规则配置 count 大小比较,得出是否触发限流操作,所有枚举值如下:


public enum MetricEvent {


PASS, // Normal pass.


BLOCK, // Normal block.


EXCEPTION,


SUCCESS,


RT,


OCCUPIED_PASS


}

6、Slot

Slot 是 sentinel 中非常重要的概念,sentinel 的工作流程就是围绕着一个个插槽所组成的插槽链来展开的。需要注意的是每个插槽都有自己的职责,他们各司其职完美的配合,通过一定的编排顺序,来达到最终的限流降级。默认的各个插槽之间的顺序是固定的,因为有的插槽需要依赖其他的插槽计算出来的结果才能进行工作。


sentinel 通过 SlotChainBuilder 作为 SPI 接口,使得 Slot Chain 具备了扩展的能力。我们可以通过实现 SlotChainBuilder 接口加入自定义 Slot 并且定义编排各个 slot 之间的排序,从而可以给 sentinel 添加自定义的功能。


那 SlotChain 是在哪创建的呢?是在 CtSph.lookProcessChain() 方法中创建的,并且该方法会根据当前请求的资源先去一个静态的 HashMap 中获取,如果获取不到才会创建,创建后会保存到 HashMap 中。这就意味着,同一个资源会全局共享一个 SlotChain。默认生成 ProcessorSlotChain 为:


// DefaultSlotChainBuilder


public ProcessorSlotChain build() {


ProcessorSlotChain chain = new DefaultProcessorSlotChain();

用户头像

Java高工P7

关注

还未添加个人签名 2021.11.08 加入

还未添加个人简介

评论

发布
暂无评论
【Spring Cloud 8】熔断与限流Sentinel,java现在的主流技术