Sentinel资源入口

在Sentinel中,一般会在调用目标资源之前先调用SphU类的entry方法来创建资源入口,sentinel的功能都被封装在里面,本文就来分析一下这里面到底做了什么事情。


SentinelResourceAspect

先以这个切面类为例来分析一下资源入口是怎么创建的。

v2.0.0
java
sentinel-extension/sentinel-annotation-aspectj/src/main/java/com/alibaba/csp/sentinel/annotation/aspectj/SentinelResourceAspect.java
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {

    @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
    public void sentinelResourceAnnotationPointcut() {
    }

    @Around("sentinelResourceAnnotationPointcut()")
    public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
        // 获取目标方法
        Method originMethod = resolveMethod(pjp);

        // 获取方法上的@SentinelResource注解
        SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
        if (annotation == null) {
            // Should not go through here.
            throw new IllegalStateException("Wrong state for SentinelResource annotation");
        }
        // 获取资源名称
        String resourceName = getResourceName(annotation.value(), originMethod);
        // 获取入口类型
        EntryType entryType = annotation.entryType();
        // 获取资源类型
        int resourceType = annotation.resourceType();
        Entry entry = null;
        try {
            // 创建入口
            entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
            // 调用目标方法
            return pjp.proceed();
        } catch (BlockException ex) {
            return handleBlockException(pjp, annotation, ex);
        } catch (Throwable ex) {
            Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
            // The ignore list will be checked first.
            if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
                throw ex;
            }
            if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
                traceException(ex);
                return handleFallback(pjp, annotation, ex);
            }

            // No fallback function can handle the exception, so throw it out.
            throw ex;
        } finally {
            if (entry != null) {
                // 退出入口
                entry.exit(1, pjp.getArgs());
            }
        }
    }
}

SphU

v2.0.0
entry_12
Env
<
>
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/SphU.java
public static Entry entry(String name, int resourceType, EntryType trafficType, Object[] args)
    throws BlockException {
    // 申请的资源数量传入的是1
    return Env.sph.entryWithType(name, resourceType, trafficType, 1, args);
}
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/Env.java
public class Env {

    public static final Sph sph = new CtSph();

    static {
        // If init fails, the process will exit.
        InitExecutor.doInit();
    }

}

该类中有很多entry相关方法,都委托给Env.sph来执行,所以SphU类可以看作是CtSph类的门面类。

实在是想吐槽一下这里的类名称用简写,根本不知道是什么意思,找了半天资料也没有明确的说法。

CtSph

v2.0.0
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java
@Override
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, Object[] args)
    throws BlockException {
    return entryWithType(name, resourceType, entryType, count, false, args);
}
@Override
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized,
                           Object[] args) throws BlockException {
    // 创建wrapper对象,封装了资源的属性
    StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
    return entryWithPriority(resource, count, prioritized, args);
}
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);
    }

    // 如果还不存在context,那么新建一个
    if (context == null) {
        // Using default context.
        // 这里默认创建的context的名称为 sentinel_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);
    }

    // 获取资源对应的处理器插槽链,如果插槽数量超过了最大值,那么会返回null
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

    /*
     * Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
     * so no rule checking will be done.
     */
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }

    /*
     * 创建CtEntry,异步环境下会创建AsyncEntry,Entry抽象类就这两个实现类,
     * 传入context,会把创建好的entry对象设置到context的curEntry属性中
     */
    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;
}

entryWithType方法中,将资源属性封装为了wrapper对象。然后在entryWithPriority方法中创建了Context(如果不存在的话),查找或创建并执行处理器插槽链。

上下文对象

获取上下文对象

v2.0.0
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextUtil.java
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
public static Context getContext() {
    return contextHolder.get();
}

从这里可以看出来,上下文对象是存放在线程本地存储中的。

创建上下文对象

v2.0.0
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/CtSph.java
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);
    }
}

调用了父类ContextUtil中的trueEnter方法。

v2.0.0
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextUtil.java
protected static Context trueEnter(String name, String origin) {
    Context context = contextHolder.get();
    if (context == null) {
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        /*
         * 对于context不存在的情况需要新建context,这种情况传入的name就是Constants.CONTEXT_DEFAULT_NAME
         * 而类加载时会执行initDefaultContext方法,就已经创建了entrance节点了,所以这里获取的node应该不会是null
         */
        DefaultNode node = localCacheNameMap.get(name);
        if (node == null) {
            // 判断contextNameNodeMap的size是不是超过了2000
            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 {
                            // 这里的逻辑就相当于是initDefaultContext方法
                            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;
                        }
                    }
                } finally {
                    LOCK.unlock();
                }
            }
        }
        // 创建Context对象
        context = new Context(node, name);
        context.setOrigin(origin);
        // 将context保存到ThreadLocal中
        contextHolder.set(context);
    }

    return context;
}

这里除了创建上下文对象以外,还创建了入口节点EntranceNode。另外对上下文对象和入口节点对象的个数做了限制,上限为2000个。

处理器插槽链

v2.0.0
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/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) {
            // 再次尝试获取,DCL实现
            chain = chainMap.get(resourceWrapper);
            if (chain == null) {
                // Entry size limit.
                // 判断插槽的数量是否超过最大值,默认是6000
                if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                    return null;
                }

                // 初始化新的插槽链
                chain = SlotChainProvider.newSlotChain();
                // 创建一个新的map,然后填充新旧数据,并赋值给chainMap属性,典型的COW操作
                Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
                    chainMap.size() + 1);
                newMap.putAll(chainMap);
                newMap.put(resourceWrapper, chain);
                chainMap = newMap;
            }
        }
    }
    return chain;
}

如果还没有创建过过处理器插槽链,则新建一个,同样做了限制,最多只能有6000个处理器插槽链。

创建处理器插槽链

v2.0.0
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/slotchain/SlotChainProvider.java
public static ProcessorSlotChain newSlotChain() {
    if (slotChainBuilder != null) {
        return slotChainBuilder.build();
    }

    // Resolve the slot chain builder SPI.
    // 通过SPI机制获取SlotChainBuilder实现,默认是DefaultSlotChainBuilder
    slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();

    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();
}

这里利用了SPI机制来加载SlotChainBuilder的实现类,在sentinel-core模块的META-INF.service目录下配置了该类型的实现类。

txt
# Default slot chain builder  
com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder

下面就来看看是怎么构建的。

v2.0.0
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/DefaultSlotChainBuilder.java
@Spi(isDefault = true)
public class DefaultSlotChainBuilder implements SlotChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        // 创建插槽链对象
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();

        /*
         * 通过SPI机制加载ProcessorSlot实现,并排序
         * 参考sentinel-core模块的resources/META-INF.services目录下面的配置文件,默认配置了一些类
         */
        List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();
        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;
            }

            /*
             * 将一个个slot加载chain的末尾,串联成为一条slot链条
             * 整个链条结构可参考图片链接:https://upload-images.jianshu.io/upload_images/3397380-10649f456ff62747.png?imageMogr2/auto-orient/strip|imageView2/2/w/712/format/webp
             * chain的first属性指向的是NodeSelector,该类是链条的入口,所以首先分析该类
             * 第二个是ClusterBuilderSlot,这两个比较固定
             * */
            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
        }

        return chain;
    }
}

这里再次用SPI机制来加载ProcessorSlot的实现类,同样是在sentinel-core模块下配置了实现类。

txt
# 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  
com.alibaba.csp.sentinel.slots.block.degrade.DefaultCircuitBreakerSlot

然后将加载到的这些插槽对象添加到插槽链中。下面是插槽链的执行流程图。