Sentinel
做为ali开源的一款轻量级流控框架,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。相比于Hystrix
,Sentinel
的设计更加简单,在 Sentinel
中资源定义和规则配置是分离的,也就是说用户能够先经过Sentinel API
给对应的业务逻辑定义资源(埋点),而后在须要的时候再配置规则,经过这种组合方式,极大的增长了Sentinel
流控的灵活性。html
引入Sentinel
带来的性能损耗很是小。只有在业务单机量级超过25W QPS的时候才会有一些显著的影响(5% - 10% 左右),单机QPS不太大的时候损耗几乎能够忽略不计。java
Sentinel
提供两种埋点方式:node
try-catch
方式(经过 SphU.entry(...)
),用户在 catch 块中执行异常处理 / fallbackif-else
方式(经过 SphO.entry(...)
),当返回 false 时执行异常处理 / fallback在此以前,须要先了解一下Sentinel
的工做流程
在 Sentinel
里面,全部的资源都对应一个资源名称(resourceName
),每次资源调用都会建立一个 Entry
对象。Entry
能够经过对主流框架的适配自动建立,也能够经过注解的方式或调用 SphU API
显式建立。Entry
建立的时候,同时也会建立一系列功能插槽(slot chain
),这些插槽有不一样的职责,例如默认状况下会建立一下7个插槽:缓存
NodeSelectorSlot
负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;ClusterBuilderSlot
则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count
等等,这些信息将用做为多维度限流,降级的依据;StatisticSlot
则用于记录、统计不一样纬度的 runtime
指标监控信息;FlowSlot
则用于根据预设的限流规则以及前面 slot
统计的状态,来进行流量控制;AuthoritySlot
则根据配置的黑白名单和调用来源信息,来作黑白名单控制;DegradeSlot
则经过统计信息以及预设的规则,来作熔断降级;SystemSlot
则经过系统的状态,例如 load1
等,来控制总的入口流量注意:这里的插槽链都是一一对应资源名称的app
上面的所介绍的插槽(slot chain
)是Sentinel
很是重要的概念。同时还有一个很是重要的概念那就是Node
,为了帮助理解,尽我所能画了下面这张图,能够看到整个结构很是的像一棵树:框架
简单解释下上图:ide
node
节点为根节点,全局惟一CentextName
(上下文名称)一一对应一个
ResourceName
)以上2个概念务必要理清楚,以后再一步一步看源码会比较清晰源码分析
下面咱们将从入口源码开始一步一步分析整个调用过程:性能
下面的是一个Sentinel
使用的示例代码,咱们就从这里切入开始分析ui
// 建立一个名称为entrance1,来源为appA 的上下文Context ContextUtil.enter("entrance1", "appA"); // 建立一个资源名称nodeA的Entry Entry nodeA = SphU.entry("nodeA"); if (nodeA != null) { nodeA.exit(); } // 清除上下文 ContextUtil.exit();
public static Context enter(String name, String origin) { // 判断上下文名称是否为默认的名称(sentinel_default_context) 是的话直接抛出异常 if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) { throw new ContextNameDefineException( "The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!"); } return trueEnter(name, origin); } protected static Context trueEnter(String name, String origin) { // 先从ThreadLocal中尝试获取,获取到则直接返回 Context context = contextHolder.get(); if (context == null) { Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap; // 尝试从缓存中获取该上下文名称对应的 入口节点 DefaultNode node = localCacheNameMap.get(name); if (node == null) { // 判断缓存中入口节点数量是否大于2000 if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { setNullContext(); return NULL_CONTEXT; } else { try { // 加锁 LOCK.lock(); // 双重检查锁 node = contextNameNodeMap.get(name); if (node == null) { // 判断缓存中入口节点数量是否大于2000 if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { setNullContext(); return NULL_CONTEXT; } else { // 根据上下文名称生成入口节点(entranceNode) node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null); // 加入至全局根节点下 Constants.ROOT.addChild(node); // 加入缓存中 Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1); newMap.putAll(contextNameNodeMap); newMap.put(name, node); contextNameNodeMap = newMap; } } } finally { LOCK.unlock(); } } } // 初始化上下文对象 context = new Context(node, name); context.setOrigin(origin); // 设置到当前线程中 contextHolder.set(context); } return context; }
主要作了2件事情
ContextName
生成entranceNode
,并加入缓存,每一个ContextName
对应一个入口节点entranceNode
ContextName
和entranceNode
初始化上下文对象,并将上下文对象设置到当前线程中这里有几点须要注意:
ContextName
对应一个入口节点entranceNode
entranceNode
都有共同的父节点。也就是根节点// SphU.class public static Entry entry(String name) throws BlockException { // 默认为 出口流量类型,单位统计数为1 return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0); } // CtSph.class public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException { // 生成资源对象 StringResourceWrapper resource = new StringResourceWrapper(name, type); return entry(resource, count, args); } public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException { return entryWithPriority(resourceWrapper, count, false, args); }
上面的代码比较简单,不指定EntryType
的话,则默认为出口流量类型,最终会调用entryWithPriority
方法,主要业务逻辑也都在这个方法中
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException { // 获取当前线程上下文对象 Context context = ContextUtil.getContext(); // 上下文名称对应的入口节点是否已经超过阈值2000,超过则会返回空 CtEntry if (context instanceof NullContext) { return new CtEntry(resourceWrapper, null, context); } if (context == null) { // 若是没有指定上下文名称,则使用默认名称,也就是默认入口节点 context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME); } // 全局开关 if (!Constants.ON) { return new CtEntry(resourceWrapper, null, context); } // 生成插槽链 ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper); /* * 表示资源(插槽链)超过6000,所以不会进行规则检查。 */ if (chain == null) { return new CtEntry(resourceWrapper, null, context); } // 生成 Entry 对象 Entry e = new CtEntry(resourceWrapper, chain, context); try { // 开始执行插槽链 调用逻辑 chain.entry(context, resourceWrapper, null, count, prioritized, args); } catch (BlockException e1) { // 清除上下文 e.exit(count, args); throw e1; } catch (Throwable e1) { // 除非Sentinel内部存在错误,不然不该发生这种状况。 RecordLog.info("Sentinel unexpected exception", e1); } return e; }
这个方法能够说是涵盖了整个Sentinel的核心逻辑
lookProcessChain(resourceWrapper);
中,下文会分析lookProcessChain
方法为指定资源生成插槽链,下面咱们来看下它的初始化逻辑
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) { // 根据资源尝试从全局缓存中获取 ProcessorSlotChain chain = chainMap.get(resourceWrapper); if (chain == null) { // 很是常见的双重检查锁 synchronized (LOCK) { chain = chainMap.get(resourceWrapper); if (chain == null) { // 判断资源数是否大于6000 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; }
SlotChainProvider.newSlotChain()
方法中)下面咱们看下初始化插槽链上的插槽的逻辑
public static ProcessorSlotChain newSlotChain() { // 判断是否已经初始化过 if (builder != null) { return builder.build(); } // 加载 SlotChain resolveSlotChainBuilder(); // 加载失败则使用默认 插槽链 if (builder == null) { RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default"); builder = new DefaultSlotChainBuilder(); } // 构建完成 return builder.build(); } /** * java自带 SPI机制 加载 slotChain */ private static void resolveSlotChainBuilder() { List<SlotChainBuilder> list = new ArrayList<SlotChainBuilder>(); boolean hasOther = false; // 尝试获取自定义SlotChainBuilder,经过JAVA SPI机制扩展 for (SlotChainBuilder builder : LOADER) { if (builder.getClass() != DefaultSlotChainBuilder.class) { hasOther = true; list.add(builder); } } if (hasOther) { builder = list.get(0); } else { // 未获取到自定义 SlotChainBuilder 则使用默认的 builder = new DefaultSlotChainBuilder(); } RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: " + builder.getClass().getCanonicalName()); }
SlotChainBuilder
来构建插槽链,自定义的SlotChainBuilder
能够经过JAVA SPI机制来扩展SlotChainBuilder
,则会使用默认的DefaultSlotChainBuilder
来构建插槽链,DefaultSlotChainBuilder
所构建的插槽就是文章开头咱们提到的7种Slot
。每一个插槽都有其对应的职责,各司其职,后面咱们会详细分析这几个插槽的源码,及所承担的职责。文章开头的提到的两个点(插槽链和Node),这是Sentinel的重点,理解这两点对于阅读源码来讲事半功倍
Sentinel系列