我们在上篇文中章已经分析了Sentinel是怎么让SentinelResource注解生效的,保留了一个疑问,每个被SentinelResource注解的方法都会在环绕通知中先调用一下如下代码
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs())
,本篇继续分析这个entry方法,分析这个方法之前,首先要了解这几个概念
Resourceresource是sentinel中最重要的一个概念,sentinel通过资源来保护具体的业务代码或其他后方服务。
sentinel把复杂的逻辑给屏蔽掉了,用户只需要为受保护的代码或服务定义一个资源,然后定义规则就可以了,剩下的通通交给sentinel来处理了。并且资源和规则是解耦的,规则甚至可以在运行时动态修改。定义完资源后,就可以通过在程序中埋点来保护你自己的服务了,埋点的方式有两种
try-catch 方式(通过 SphU.entry(…)),当 catch 到BlockException时执行异常处理(或fallback)
if-else 方式(通过 SphO.entry(…)),当返回 false 时执行异常处理(或fallback)
以上这两种方式都是通过硬编码的形式定义资源然后进行资源埋点的,对业务代码的侵入太大,从0.1.1版本开始,sentinel加入了注解的支持,可以通过注解来定义资源,具体的注解为:SentinelResource 。通过注解除了可以定义资源外,还可以指定 blockHandler 和 fallback 方法。
看一下ResourceWrapper的结构
protected final String name;
protected final EntryType entryType;
protected final int resourceType;
public ResourceWrapper(String name, EntryType entryType, int resourceType) {
AssertUtil.notEmpty(name, "resource name cannot be empty");
AssertUtil.notNull(entryType, "entryType cannot be null");
this.name = name;
this.entryType = entryType;
this.resourceType = resourceType;
}
其中在SentinelResource中entryType默认是OUTEntryType entryType() default EntryType.OUT;,区别是当entryType为IN时,在StatisticSlot中会被统计到全局流量
resourType默认为0int resourceType() default 0;表示资源分类
public final class ResourceTypeConstants {
public static final int COMMON = 0;
public static final int COMMON_WEB = 1;
public static final int COMMON_RPC = 2;
public static final int COMMON_API_GATEWAY = 3;
public static final int COMMON_DB_SQL = 4;
private ResourceTypeConstants() {}
}
Rule
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
ContextContext是对资源操作时的上下文环境,每个资源操作(针对Resource进行的entry/exit)必须属于一个Context,如果程序中未指定Context,会创建name为"sentinel_default_context"的默认Context。一个Context生命周期内可能有多个资源操作,Context生命周期内的最后一个资源exit时会清理该Context,这也预示这整个Context生命周期的结束。Context主要属性如下:
private final String name;
private DefaultNode entranceNode;
private Entry curEntry;
private String origin = "";
private final boolean async;
如果想在调用 SphU.entry() 或 SphO.entry() 前,自定义一个context,则通过ContextUtil.enter()方法来创建。当Entry执行exit方法时,如果entry的parent节点为null,表示是当前Context中最外层的Entry了,此时将ThreadLocal中的context清空。
Entry刚才在Context身影中也看到了Entry的出现,现在就谈谈Entry。每次执行 SphU.entry() 或 SphO.entry() 都会返回一个Entry,Entry表示一次资源操作,内部会保存当前invocation信息。在一个Context生命周期中多次资源操作,也就是对应多个Entry,这些Entry形成parent/child结构保存在Entry实例中(举个列子,resourceA中调用了resourceB,那么B返回的entry的parent就是
A返回的entry),entry类CtEntry结构如下:
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot
也就是返回的entry都会作为当前entry的子节点
Node继续看Entry本尊的结构
public abstract class Entry implements AutoCloseable {
private static final Object[] OBJECTS0 = new Object[0];
private final long createTimestamp;
private long completeTimestamp;
private Node curNode;
private Node originNode;
private Throwable error;
private BlockException blockError;
protected final ResourceWrapper resourceWrapper;
}
实例代码中出现了Node,这个又是什么呢
Node实现类StatisticNode,StatisticNode保存了资源的实时统计数据(基于滑动时间窗口机制),通过这些统计数据,sentinel才能进行限流、降级等一系列操作.,StatisticNode属性如下:
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
IntervalProperty.INTERVAL);
private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
private LongAdder curThreadNum = new LongAdder();
private long lastFetchTime = -1;
详细介绍会单独写一篇文章
该类还有以下三个子类
(1)EntranceNode:该类的创建是在初始化Context时完成的(ContextUtil.trueEnter方法),注意该类是针对Context维度的,也就是一个context有且仅有一个EntranceNode。
(2)DefaultNode:该类的创建是在NodeSelectorSlot.entry完成的,当不存在context.name对应的DefaultNode时会新建(new DefaultNode(resourceWrapper, null),对应resouce)并保存到本地缓存(NodeSelectorSlot中private volatile Map
为什么是一个context有且仅有一个DefaultNode,为什么不是resouece对应一个defaultNode?
其实,这里的一个context有且仅有一个DefaultNode是NodeSelectorSlot范围内,NodeSelectorSlot是ProcessorSlotChain中的一环,获取ProcessorSlotChain是根据Resource维度来的。
无论在哪个上下文中,相同的资源都将在全局范围内共享相同的 ProcessorSlotChain。因此,如果代码NodeSelectorSlot.entry方法,则资源名称必须相同,但上下文名称可能不同。
如果我们使用 com.alibaba.csp.sentinel.SphUentry(String resource)在不同的上下文中进入相同的资源,使用上下文名称作为映射键可以区分相同的资源。在这种情况下,将为每个不同的上下文(不同的上下文名称)创建多个具有相同资源名称的DefaultNode。
(3)ClusterNode:考虑另一个问题。一个资源可能有多个 DefaultNode,那么获取同一资源总统计信息的最快方法是什么?答案是所有具有相同资源名称的DefaultNode共享一个 ClusterNode。有关ClusterBuilderSlot详细信息,同样放在下文讲解。
一个Resouce只有一个clusterNode,而一个Resouce可以有多个defaultNode,多个defaultNode对应一个clusterNode,如果defaultNode.clusterNode为null,则在ClusterBuilderSlot.entry中会进行初始化。
一个resource下设置统一的cluster node,存储resource粒度的统计信息;每个cluster node根据来源下挂不同origin node,存储各个来源的统计信息
总结一下:配置中不同限流模式其实最终对应的就是选择不同的node进行计算:
直接模式: 选择cluster node
关联模式: 选择关联resource的cluster node
应用来源: 选择origin node
链路模式: 选择default node
slot是另一个sentinel中非常重要的概念,sentinel的工作流程就是围绕着一个个插槽所组成的插槽链来展开的。需要注意的是每个插槽都有自己的职责,他们各司其职完好的配合,通过一定的编排顺序,来达到最终的限流降级的目的。默认的各个插槽之间的顺序是固定的,因为有的插槽需要依赖其他的插槽计算出来的结果才能进行工作。
但是这并不意味着我们只能按照框架的定义来,sentinel 通过 SlotChainBuilder 作为 SPI 接口,使得 Slot Chain 具备了扩展的能力。我们可以通过实现 SlotsChainBuilder 接口加入自定义的 slot 并自定义编排各个 slot 之间的顺序,从而可以给 sentinel 添加自定义的功能。
至此介绍完了核心概念,我们继续肝源码
源码 entry入口方法直接跟进代码
public static Entry entry(String name, int resourceType, EntryType trafficType, Object[] args)
throws BlockException {
return Env.sph.entryWithType(name, resourceType, trafficType, 1, args);
}
调用了Rnv.shp
- name – 受保护资源的唯一名称resourceType – 资源的分类(例如 Web 或 RPC)trafficType – 流量类型(入站、出站或内部)。这个用来标记系统不稳定时是否可以阻塞,只有入站流量可以被 SystemRule 阻塞batchCount - 流量args – args 用于参数流控或自定义槽
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized,
Object[] args) throws BlockException {
StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
return entryWithPriority(resource, count, prioritized, args);
}
最终调用了entryWithType方法
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
// The {@link NullContext} indicates that the amount of context has exceeded the threshold,
// so here init the entry only. No rule checking will be done.
return new CtEntry(resourceWrapper, null, context);
}
if (context == null) {
// Using default context.
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
// Global switch is close, no rule checking will do.
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
ProcessorSlot
这个方法可以说是涵盖了整个Sentinel的核心逻辑
- 获得上下文,NullContext表示上下文的数量已经超过阈值,所以这里只初始化条目。不会进行任何规则检查。如果context为null,则使用默认context,从下面的代码可以看到,调用的trueRnter方法,其中resource参数为"",trueRnter方法下文会继续分析
private final static class InternalContextUtil extends ContextUtil {
static Context internalEnter(String name) {
return trueEnter(name, "");
}
static Context internalEnter(String name, String origin) {
return trueEnter(name, origin);
}
}
- 如果全局开关已关闭,不会进行任何规则检查。return new CtEntry(resourceWrapper, null, context);通过调用lookProcessChain方法查看所有的流程链,插槽链是跟资源相关的,Sentinel最关键的逻辑也都在各个插槽中,这个方法下文单独分析,比较重要如果上一步获得流程链为null,表示资源量(插槽链)超过 Constants.MAX_SLOT_CHAIN_SIZE,因此不会进行规则检查,直接返回return new CtEntry(resourceWrapper, null, context);构建完整的三个参数的CtEntry,并 调用第四步获取的插槽链
protected static Context trueEnter(String name, String origin) {
Context context = contextHolder.get();
if (context == null) {
Map localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
LOCK.lock();
try {
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// Add entrance node.
Constants.ROOT.addChild(node);
Map 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;
}
- 先从ThreadLocal中尝试获取,获取到则直接返回如果第一步没有获取,尝试从缓存中获取该上下文名称对应的 入口节点判断缓存中入口节点数量是否大于2000public final static int MAX_CONTEXT_NAME_SIZE = 2000;如果已经大于2000,返回一个NULL_CONTEXT以上检查都通过根据上下文名称生成入口节点(entranceNode),期间会进行双关检索确保线程安全加入至全局根节点下,并加入缓存,注意每个ContextName对应一个入口节点entranceNode根据ContextName和entranceNode初始化上下文对象,并将上下文对象设置到当前线程中
可以看看到 插槽链是resource维度的
ProcessorSlot
- 从缓存中获取缓存没有则双关检索并在此检查插槽链的个数初始化插槽链
public static ProcessorSlotChain newSlotChain() {
if (slotChainBuilder != null) {
return slotChainBuilder.build();
}
// Resolve the slot chain builder SPI.
slotChainBuilder = SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);
if (slotChainBuilder == null) {
// Should not go through here.
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
slotChainBuilder = new DefaultSlotChainBuilder();
} else {
RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: "
+ slotChainBuilder.getClass().getCanonicalName());
}
return slotChainBuilder.build();
}
如果已经初始化过过,直接返回if (slotChainBuilder != null) { return slotChainBuilder.build(); }
主要方法还是还是通过SPI机制来初始化
SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);
查看这个方法
public staticT loadFirstInstanceOrDefault(Class clazz, Class extends T> defaultClass) { AssertUtil.notNull(clazz, "SPI class cannot be null"); AssertUtil.notNull(defaultClass, "default SPI class cannot be null"); try { String key = clazz.getName(); // Not thread-safe, as it's expected to be resolved in a thread-safe context. ServiceLoader serviceLoader = SERVICE_LOADER_MAP.get(key); if (serviceLoader == null) { serviceLoader = ServiceLoaderUtil.getServiceLoader(clazz); SERVICE_LOADER_MAP.put(key, serviceLoader); } //加载第一个找到的特定 SPI 实例(不包括提供的默认 SPI 类) for (T instance : serviceLoader) { if (instance.getClass() != defaultClass) { return instance; } } //如果没有找到其他 SPI 实现,则创建一个默认 SPI 实例 return defaultClass.newInstance(); } catch (Throwable t) { RecordLog.error("[SpiLoader] ERROR: loadFirstInstanceOrDefault failed", t); t.printStackTrace(); return null; } }
加载第一个找到的特定 SPI 实例(不包括提供的默认 SPI 类)。如果没有找到其他 SPI 实现,则创建一个默认 SPI 实例。
最后看一下默认的SPI实例,方法很简单:加载提供的 SPI 接口的排序和原型 SPI 实例列表。
注意:每个调用返回不同的实例,即原型实例,而不是单例实例。
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
// Note: the instances of ProcessorSlot should be different, since they are not stateless.
List sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
for (ProcessorSlot slot : sortedSlotList) {
if (!(slot instanceof AbstractlinkedProcessorSlot)) {
RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractlinkedProcessorSlot, can't be added into ProcessorSlotChain");
continue;
}
chain.addLast((AbstractlinkedProcessorSlot>) slot);
}
return chain;
}
关于插槽链:在sentinel-core包下面默认提供了8个插槽连(com.alibaba.csp.sentinel.slotchain.ProcessorSlot)
# 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
如果应用到自己的项目中,可以通过spi机制来扩展插槽链,比如添加一个MonitorSlot来加入自己公司内部的监控打点
另外在sentinel-parameter-flow-control中,还还提供了一个com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot这些内容我们都会在后面有专门的文章来详解,本文不做详细讲解
- 获取上下文对象,如果上下文对象还未初始化,则使用默认名称初始化。判断全局开关根据给定的资源生成插槽链,插槽链是跟资源相关的,Sentinel最关键的逻辑也都在各个插槽中。调用插槽链
至此,我们分析完了入口方法,也知道了Sentinel的核心流程,下一篇文章我们看看生成的插槽链到底有什么作用



