7. Sentinel SlotChain的创建和调用源码解析
1. 入口简介
通过5小节,我们知道了是同SphU.entry方法来作为流控的入口的,也就是说该方法会执行流控相关逻辑,那么它是如何实现的呢? 通过Sentinel结构图我们知道它里面应该会构建一个调用链,这个调用链就是SlotChain,那么我们接下来着重解析这个调用链。
2. 各种Slot介绍
在 Sentinel里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象,Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如默认情况下会创建一下7个插槽。
具体如下:
- NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
- ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据,对应簇点链路;
- StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
- FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制,对应流控规则;
- AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制,对应授权规则;
- DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级,对应熔断规则;
- SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量,对应系统规则;
3. SlotChain 的创建
我们需要从SphU.entry入手,最终会进入到方法
public class CtSph implements Sph {
...
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
...
// 获取chain链
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
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) {
// This should not happen, unless there are errors existing in Sentinel internal.
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
...
}
通过代码可以看出,我们在entryWithPriority方法中会获取调用链,通过chain的entry方法来实现对链的调用执行。
所以lookProcessChain方法是比较重要的,它是如何创建并获取到这个调用链的呢,所以我们需要跟踪这个方法的实现。
//CtSph.java
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
= new HashMap<ResourceWrapper, ProcessorSlotChain>();
...
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;
}
从以上代码可以看出一个重要的知识点,就是调用链最终是以资源为Key缓存下来的,也就是说调用链是资源级别的。每个资源有一个自己的调用链。
可以看出重要的方法是:SlotChainProvider.newSlotChain(); 从方法名字来看,应该是创建调用链,进去看看。
public final class SlotChainProvider {
private static volatile SlotChainBuilder slotChainBuilder = null;
public static ProcessorSlotChain newSlotChain() {
if (slotChainBuilder != null) {
return slotChainBuilder.build();
}
slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();
if (slotChainBuilder == null) {
slotChainBuilder = new DefaultSlotChainBuilder();
}
return slotChainBuilder.build();
}
}
这里的创建是通过Builder来创建的, 所以先要创建Builder, 可以看到Builder是可以被SpiLoader替换的,如果没有对应的,兜底的Builder类是DefaultSlotChainBuilder,这里的SpiLoader(Service Provider Interface)主要是读取META-INF/services/ 目录下的文件进行映射,如果里面有声明则会使用到。 我们通过源码得知, 在目录中有对应的映射,也是指向了DefaultSlotChainBuilder。 所以DefaultSlotChainBuilder为ProcessorSlotChain 的Builder。
所以我们跟踪到DefaultSlotChainBuilder的build方法
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
//SlotChain的第一个Slot是DefaultProcessorSlotChain从这里可看出
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
//然后又通过SPI加载其他的Slot,从可以看出我们扩展的话可以通过实现ProcessorSlot来加入到Chain中
//从下图可以看出默认的Slot都有哪些,在META-INF/services/下面的文件中声明了
List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();
for (ProcessorSlot slot : sortedSlotList) {
...
chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
}
return chain;
}
}
# 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
这里也就和我们架构图中的调用链对应上了
4. SlotChain的调用
让我们再回到CtSph.entryWithPriority方法中,以上解析,当方法CtSph.lookProcessChain调用完成后我们就获得了调用链,有了调用链后,就会对它进行逐个进行调用,具体实现如下:
//CtSph.java
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
...
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
...
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) {
// This should not happen, unless there are errors existing in Sentinel internal.
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
通过链头节点调用chain.entry方法,从之前的分析来看链头应该是DefaultProcessorSlotChain,所以调用后应该会进入DefaultProcessorSlotChain的entry方法。链中的每一个节点执行完自己内部逻辑后,要负责调用下一个节点的entry方法。
//DefaultProcessorSlotChain.java
AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
throws Throwable {
//这里调用了父类的AbstractLinkedProcessorSlot.fireEntry方法
super.fireEntry(context, resourceWrapper, t, count, prioritized, args);
}
...
};
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
throws Throwable {
//调用first的transformEntry方法,从而调用了父类AbstractLinkedProcessorSlot的fireEntry
first.transformEntry(context, resourceWrapper, t, count, prioritized, args);
}
//AbstractLinkedProcessorSlot.java
@Override
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
if (next != null) {
//调用了下一个节点的transformEntry方法,里面就会调用entry方法
next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
}
}
void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args)
throws Throwable {
T t = (T)o;
//调用entry方法,执行本Slot的内部业务逻辑,并且触发调用下一个节点的entry方法
entry(context, resourceWrapper, t, count, prioritized, args);
}
至此我们调用链创建完成并,开始调用了。