Sentinel 中的责任链模式
Sentinel 中的 ProcessorSlot
ProcessorSlot 直译就是处理器插槽,是 Sentinel 实现限流降级、熔断降级、系统自适应降级等功能的切入点。
Sentinel 的核心骨架,将不同的 Slot 按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级、系统保护)组合在一起。slot chain 其实可以分为两部分:统计数据构建部分(statistic)和判断部分(rule checking)。核心结构:
目前的设计是 one slot chain per resource,因为某些 slot 是 per resource 的(比如 NodeSelectorSlot)。
Sentinel 提供的 ProcessorSlot 可以分为两类,一类是辅助完成资源指标数据统计的切入点,一类是实现降级功能的切入点。
辅助资源指标数据统计的 ProcessorSlot:
- NodeSelectorSlot:为当前资源创建 DefaultNode,并且将 DefaultNode 赋值给 Context.curEntry.curNode;
- 如果当前调用链路上只出现过一次 SphU#entry 的情况,将该 DefaultNode 添加到的 Context.entranceNode 的子节点,否则添加到 Context.curEntry.parent 的子节点(childList)。
- ClusterBuilderSlot:如果当前资源未创建 ClusterNode,则为资源创建 ClusterNode;
- 将 ClusterNode 赋值给当前资源的 DefaultNode.clusterNode;如果调用来源(origin)不为空,则为调用来源创建 StatisticNode,用于实现按调用来源统计资源的指标数据,ClusterNode 持有每个调用来源的 StatisticNode。
- StatisticSlot:这是 Sentinel 最为重要的类之一,用于实现指标数据统计。先是调用后续的 ProcessorSlot#entry 判断是否放行请求,再根据判断结果进行相应的指标数据统计操作。
这些辅助ProcessorSlot需要严格的顺序执行
NodeSelectorSlot->ClusterBuilderSlot->StatisticSlot
实现降级功能的 ProcessorSlot:
- AuthoritySlot:实现黑白名单降级
- SystemSlot:实现系统自适应降级
- FlowSlot:实现限流降级
- DegradeSlot:实现熔断降级
Sentinel 会为每个资源创建且仅创建一个 ProcessorSlotChain,只要名称相同就认为是同一个资源。ProcessorSlotChain 被缓存在 CtSph.chainMap 静态字段,key 为资源 ID.
Sentinel 的整体工作流程
如果不借助 Sentinel 提供的适配器,我们可以这样使用 Sentinel。
ContextUtil.enter("上下文名称,例如:sentinel_spring_web_context"); Entry entry = null; try { entry = SphU.entry("资源名称,例如:/rpc/openfein/demo", EntryType.IN (或者 EntryType.OUT)); // 执行业务方法 return doBusiness(); } catch (Exception e) { if (!(e instanceof BlockException)) { Tracer.trace(e); } throw e; } finally { if (entry != null) { entry.exit(1); } ContextUtil.exit(); }
此流程分5个部分
- 调用 ContextUtil#enter 方法;
- 调用 SphU#entry 方法;
- 如果抛出异常,且异常类型非 BlockException 异常,则调用 Tracer#trace 方法记录异常;
- 调用 Entry#exit 方法;
- 调用 ContextUtil#exit 方法。
ContextUtil.enter流程
ContextUtil#enter 方法负责为当前调用链路创建 Context,以及为 Conetxt 创建 EntranceNodeprivate static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>(); //资源入口,生成EntranceNode,如果没有Context的话,生成context并写入到ContextHolder中 protected static Context trueEnter(String name, String origin) { Context context = contextHolder.get(); if (context == null) { Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap; DefaultNode node = localCacheNameMap.get(name); if (node == null) { //生成ResourceWrapper,生成EntranceNode node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null); // Add entrance node. Constants.ROOT.addChild(node); Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1); newMap.putAll(contextNameNodeMap); newMap.put(name, node); contextNameNodeMap = newMap; } context = new Context(node, name); context.setOrigin(origin); contextHolder.set(context); } return context; }
SphU.entry流程
Sentinel 的核心骨架是 ProcessorSlotChain,所以核心的流程是一次 SphU#entry 方法的调用以及一次 CtEntry#exit 方法的调用。- SphU#entry 方法调用 CtSph#entry 方法,
- CtSph 负责为资源创建 ResourceWrapper 对象并为资源构造一个全局唯一的 ProcessorSlotChain、
- 为资源创建 CtEntry 并将 CtEntry 赋值给当前调用链路的 Context.curEntry、
- 最后调用 ProcessorSlotChain#entry 方法完成一次单向链表的 entry 方法调用
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException { //生成resource StringResourceWrapper resource = new StringResourceWrapper(name, type); return entry(resource, count, args); } private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException { Context context = ContextUtil.getContext(); //核心,生成SlotChain ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper); //生成entry Entry e = new CtEntry(resourceWrapper, chain, context); //开始处理具体逻辑 chain.entry(context, resourceWrapper, null, count, prioritized, args); return e; } //生成SlotChain,默认使用SPI机制,加载 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 = new HashMap<ResourceWrapper, ProcessorSlotChain>( chainMap.size() + 1); newMap.putAll(chainMap); newMap.put(resourceWrapper, chain); chainMap = newMap; } } } return chain; }
SPI Slot加载顺序如下
# Sentinel default ProcessorSlots com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot com.alibaba.csp.sentinel.slots.logger.LogSlot com.alibaba.csp.sentinel.slots.statistic.StatisticSlot com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot com.alibaba.csp.sentinel.slots.system.SystemSlot com.alibaba.csp.sentinel.slots.block.flow.FlowSlot com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot
Entry#exit流程
protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException { if (context != null) { //...... // 1、调用 ProcessorSlotChain 的 exit 方法 if (chain != null) { chain.exit(context, resourceWrapper, count, args); } // 2、将当前 CtEntry 的父节点设置为 Context 的当前节点 context.setCurEntry(parent); if (parent != null) { ((CtEntry)parent).child = null; } // ..... } }
CtSph 在创建 CtEntry 时,将资源的 ProcessorSlotChain 赋值给了 CtEntry,所以在调用 CtEntry#exit 方法时,CtEntry 能够拿到当前资源的 ProcessorSlotChain,并调用 ProcessorSlotChain 的 exit 方法完成一次单向链表的 exit 方法调用。其过程与 ProcessorSlotChain 的一次 entry 方法的调用过程一样。 CtEntry 在退出时还会还原 Context.curEntry。CtEntry 用于维护父子 Entry,每一次调用 SphU#entry 都会创建一个 CtEntry,如果应用处理一次请求的路径上会多次调用 SphU#entry,那么这些 CtEntry 会构成一个双向链表。在每次创建 CtEntry,都会将 Context.curEntry 设置为这个新的 CtEntry,双向链表的作用就是在调用 CtEntry#exit 方法时,能够将 Context.curEntry 还原为上一个 CtEntry。
ContextUtil 的 exit 流程
public static void exit() { Context context = contextHolder.get(); if (context != null && context.getCurEntry() == null) { contextHolder.set(null); } }如果 Context.curEntry 为空,则说明所有 SphU#entry 都对应执行了一次 Entry#exit 方法,此时就可以将 Context 从 ThreadLocal 中移除。
Plus:
StatisticSlot
StatisticSlot
是 Sentinel 最为重要的类之一,用于根据规则判断结果进行相应的统计操作。
entry 的时候:依次执行后面的判断 slot。每个 slot 触发流控的话会抛出异常(BlockException
的子类)。若有 BlockException
抛出,则记录 block 数据;若无异常抛出则算作可通过(pass),记录 pass 数据。
exit 的时候:若无 error(无论是业务异常还是流控异常),记录 complete(success)以及 RT,线程数-1。
记录数据的维度:线程数+1、记录当前 DefaultNode 数据、记录对应的 originNode 数据(若存在 origin)、累计 IN 统计数据(若流量类型为 IN)。