编者注:前段时间笔者在团队内部分享了sentinel原理设计与实现,主要讲解了sentinel基础概念和工做原理,工做原理部分你们听了基本都了解了,可是对于sentinel的几个概念及其之间的关系还有挺多同窗有点模糊的,趁着这几天比较空,针对sentinel的几个核心概念,作了一些总结,但愿能帮助一些sentinel初学者理清这些概念之间的关系。node
PS:本文主要参考sentinel源码实现和部分官方文档,建议小伙伴阅读本文的同时也大体看下官方文档和源码,学习效果更好呦 : ) 官方文档讲解的其实仍是挺详细的,可是对于这些概念之间的关系可能对于初学者来讲还有点不够。git
估计挺多小伙伴还不知道Sentinel是个什么东东,Sentinel是一个以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性的框架。github地址为:https://github.com/alibaba/Sentinelgithub
资源是 Sentinel 的关键概念。它能够是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至能够是一段代码。只要经过 Sentinel API 定义的代码,就是资源,可以被 Sentinel 保护起来。大部分状况下,可使用方法签名,URL,甚至服务名称做为资源名来标示资源。数组
围绕资源的实时状态设定的规则,能够包括流量控制规则、熔断降级规则以及系统保护规则。全部规则能够动态实时调整。缓存
sentinel中调用SphU或者SphO的entry方法获取限流资源,不一样的是前者获取限流资源失败时会抛BlockException异常,后者或捕获该异常并返回false,两者的实现都是基于CtSph类完成的。简单的sentinel示例:安全
1 Entry entry = null; 2 try { 3 entry = SphU.entry(KEY); 4 System.out.println("entry ok..."); 5 } catch (BlockException e1) { 6 // 获取限流资源失败 7 } catch (Exception e2) { 8 // biz exception 9 } finally { 10 if (entry != null) { 11 entry.exit(); 12 } 13 } 14 15 Entry entry = null; 16 if (SphO.entry(KEY)) { 17 System.out.println("entry ok"); 18 } else { 19 // 获取限流资源失败 20 }
SphU和SphO两者没有孰优孰略问题,底层实现是同样的,根据不一样场景选举合适的一个便可。看了简单示例以后,一块儿来看下sentinel中的核心概念,便于理解后续内容。数据结构
resource是sentinel中最重要的一个概念,sentinel经过资源来保护具体的业务代码或其余后方服务。sentinel把复杂的逻辑给屏蔽掉了,用户只须要为受保护的代码或服务定义一个资源,而后定义规则就能够了,剩下的统统交给sentinel来处理了。而且资源和规则是解耦的,规则甚至能够在运行时动态修改。定义完资源后,就能够经过在程序中埋点来保护你本身的服务了,埋点的方式有两种:app
try-catch 方式(经过 SphU.entry(...)
),当 catch 到BlockException时执行异常处理(或fallback)框架
if-else 方式(经过 SphO.entry(...)
),当返回 false 时执行异常处理(或fallback)async
以上这两种方式都是经过硬编码的形式定义资源而后进行资源埋点的,对业务代码的侵入太大,从0.1.1版本开始,sentinel加入了注解的支持,能够经过注解来定义资源,具体的注解为:SentinelResource 。经过注解除了能够定义资源外,还能够指定 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主要属性以下:
1 public class Context { 2 // context名字,默认名字 "sentinel_default_context" 3 private final String name; 4 // context入口节点,每一个context必须有一个entranceNode 5 private DefaultNode entranceNode; 6 // context当前entry,Context生命周期中可能有多个Entry,全部curEntry会有变化 7 private Entry curEntry; 8 // The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP). 9 private String origin = ""; 10 private final boolean async; 11 }
注意:一个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清空。
刚才在Context身影中也看到了Entry的出现,如今就谈谈Entry。每次执行 SphU.entry() 或 SphO.entry() 都会返回一个Entry,Entry表示一次资源操做,内部会保存当前invocation信息。在一个Context生命周期中屡次资源操做,也就是对应多个Entry,这些Entry造成parent/child结构保存在Entry实例中,entry类CtEntry结构以下:
1 class CtEntry extends Entry { 2 protected Entry parent = null; 3 protected Entry child = null; 4 5 protected ProcessorSlot<Object> chain; 6 protected Context context; 7 } 8 public abstract class Entry implements AutoCloseable { 9 private long createTime; 10 private Node curNode; 11 /** 12 * {@link Node} of the specific origin, Usually the origin is the Service Consumer. 13 */ 14 private Node originNode; 15 private Throwable error; // 是否出现异常 16 protected ResourceWrapper resourceWrapper; // 资源信息 17 }
Entry实例代码中出现了Node,这个又是什么东东呢 :(,我们接着往下看:
Node(关于StatisticNode的讨论放到下一小节)默认实现类DefaultNode,该类还有一个子类EntranceNode;context有一个entranceNode属性,Entry中有一个curNode属性。
EntranceNode:该类的建立是在初始化Context时完成的(ContextUtil.trueEnter方法),注意该类是针对Context维度的,也就是一个context有且仅有一个EntranceNode。
DefaultNode:该类的建立是在NodeSelectorSlot.entry完成的,当不存在context.name对应的DefaultNode时会新建(new DefaultNode(resourceWrapper, null),对应resouce)并保存到本地缓存(NodeSelectorSlot中private volatile Map<String, DefaultNode> map);获取到context.name对应的DefaultNode后会将该DefaultNode设置到当前context的curEntry.curNode属性,也就是说,在NodeSelectorSlot中是一个context有且仅有一个DefaultNode。
看到这里,你是否是有疑问?为何一个context有且仅有一个DefaultNode,咱们的resouece跑哪去了呢,其实,这里的一个context有且仅有一个DefaultNode是在NodeSelectorSlot范围内,NodeSelectorSlot是ProcessorSlotChain中的一环,获取ProcessorSlotChain是根据Resource维度来的。总结为一句话就是:针对同一个Resource,多个context对应多个DefaultNode;针对不一样Resource,(不论是否是同一个context)对应多个不一样DefaultNode。这还没看明白 : (,好吧,我不bb了,上图吧:
DefaultNode结构以下:
1 public class DefaultNode extends StatisticNode { 2 private ResourceWrapper id; 3 /** 4 * The list of all child nodes. 5 * 子节点集合 6 */ 7 private volatile Set<Node> childList = new HashSet<>(); 8 /** 9 * Associated cluster node. 10 */ 11 private ClusterNode clusterNode; 12 }
一个Resouce只有一个clusterNode,多个defaultNode对应一个clusterNode,若是defaultNode.clusterNode为null,则在ClusterBuilderSlot.entry中会进行初始化。
同一个Resource,对应同一个ProcessorSlotChain,这块处理逻辑在lookProcessChain方法中,以下:
1 ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) { 2 ProcessorSlotChain chain = chainMap.get(resourceWrapper); 3 if (chain == null) { 4 synchronized (LOCK) { 5 chain = chainMap.get(resourceWrapper); 6 if (chain == null) { 7 // Entry size limit. 8 if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) { 9 return null; 10 } 11 12 chain = SlotChainProvider.newSlotChain(); 13 Map<ResourceWrapper, ProcessorSlotChain> newMap = newHashMap<ResourceWrapper, ProcessorSlotChain>( 14 chainMap.size() + 1); 15 newMap.putAll(chainMap); 16 newMap.put(resourceWrapper, chain); 17 chainMap = newMap; 18 } 19 } 20 } 21 return chain; 22 }
StatisticNode中保存了资源的实时统计数据(基于滑动时间窗口机制),经过这些统计数据,sentinel才能进行限流、降级等一系列操做。StatisticNode属性以下:
1 public class StatisticNode implements Node { 2 /** 3 * 秒级的滑动时间窗口(时间窗口单位500ms) 4 */ 5 private transient volatile Metric rollingCounterInSecond = newArrayMetric(SampleCountProperty.SAMPLE_COUNT, 6 IntervalProperty.INTERVAL); 7 /** 8 * 分钟级的滑动时间窗口(时间窗口单位1s) 9 */ 10 private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000,false); 11 /** 12 * The counter for thread count. 13 * 线程个数用户触发线程数流控 14 */ 15 private LongAdder curThreadNum = new LongAdder(); 16 } 17 public class ArrayMetric implements Metric { 18 private final LeapArray<MetricBucket> data; 19 } 20 public class MetricBucket { 21 // 保存统计值 22 private final LongAdder[] counters; 23 // 最小rt 24 private volatile long minRt; 25 }
其中MetricBucket.counters数组大小为MetricEvent枚举值的个数,每一个枚举对应一个统计项,好比PASS表示经过个数,限流可根据经过的个数和设置的限流规则配置count大小比较,得出是否触发限流操做,全部枚举值以下:
public enum MetricEvent { PASS, // Normal pass. BLOCK, // Normal block. EXCEPTION, SUCCESS, RT, OCCUPIED_PASS }
slot是另外一个sentinel中很是重要的概念,sentinel的工做流程就是围绕着一个个插槽所组成的插槽链来展开的。须要注意的是每一个插槽都有本身的职责,他们各司其职无缺的配合,经过必定的编排顺序,来达到最终的限流降级的目的。默认的各个插槽之间的顺序是固定的,由于有的插槽须要依赖其余的插槽计算出来的结果才能进行工做。
可是这并不意味着咱们只能按照框架的定义来,sentinel 经过 SlotChainBuilder 做为 SPI 接口,使得 Slot Chain 具有了扩展的能力。咱们能够经过实现 SlotsChainBuilder 接口加入自定义的 slot 并自定义编排各个 slot 之间的顺序,从而能够给 sentinel 添加自定义的功能。
那SlotChain是在哪建立的呢?是在 CtSph.lookProcessChain() 方法中建立的,而且该方法会根据当前请求的资源先去一个静态的HashMap中获取,若是获取不到才会建立,建立后会保存到HashMap中。这就意味着,同一个资源会全局共享一个SlotChain。默认生成ProcessorSlotChain为:
1 // DefaultSlotChainBuilder 2 public ProcessorSlotChain build() { 3 ProcessorSlotChain chain = new DefaultProcessorSlotChain(); 4 chain.addLast(new NodeSelectorSlot()); 5 chain.addLast(new ClusterBuilderSlot()); 6 chain.addLast(new LogSlot()); 7 chain.addLast(new StatisticSlot()); 8 chain.addLast(new SystemSlot()); 9 chain.addLast(new AuthoritySlot()); 10 chain.addLast(new FlowSlot()); 11 chain.addLast(new DegradeSlot()); 12 13 return chain;
到这里本文结束了,谢谢小伙伴们的阅读~ 在理解了这些核心概念以后,相信聪明的你回过头再看sentinel源码就不会以为有很大难度了 : )
往期精选