@Author:zxw

1.前言

上篇文章已经讲了资源类CtEntry中的具体属性,那接下来通过一段代码看下Sentinel在初始化Entry的过程中做了哪些操作。

public static void main(String[] args) {
        initFlowRules();
        while (true) {
            Entry entry = null;
            try {
                // 自定义资源名,需要和exit()方法承兑出现
                entry = SphU.entry("HelloWorld");
                // 被保护的业务逻辑
                /*您的业务逻辑 - 开始*/
                System.out.println("hello world");
                /*您的业务逻辑 - 结束*/
            } catch (BlockException e1) {
                // 资源访问组织,被限流或被降级
                /*限流控逻辑处理 - 开始*/
                System.out.println("block!");
                /*限流控逻辑处理 - 结束*/
            } finally {
                if (entry != null) {
                    entry.exit();
                }
            }
        }
    }

2.源码分析

2.1 Sph

通过上面代码可以看到创建Entry是通过如下方式,SphU可以理解为一个帮我们创建Entry资源的工具类,只是一个调用的入口SphU.entry("HelloWorld");。但是真正创建Entry资源的其实是我们的CtSph类,Sph接口提供了同步和异步创建Entry的方式以及之前讲解ResourceWrapper时有Method和String两种。

public interface Sph extends SphResourceTypeSupport {

    Entry entry(String name) throws BlockException;
    
    AsyncEntry asyncEntry(String name, EntryType trafficType, int batchCount, Object... args) throws BlockException;
    
    Entry entry(Method method) throws BlockException;
}

对于上面代码的创建部分

entry = SphU.entry("HelloWorld");

我们知道Entry内部还有一个ResrouceWrapper资源类,这里我们只传了一个name字符串进去,该name肯定就是资源名称并且代码内部会帮我们创建一个ResourceWrapper资源类

StringResourceWrapper resource = new StringResourceWrapper(name, type);

2.2 Context

之前分析CtEntry元数据时,其内部还有一个上下文对象Context,所以首先我们需要获取当前线程的上下文对象

Context context = ContextUtil.getContext();

这个Context因为是线程独有的,内部通过ThreadLocal进行存储,Sentinel对其有数量大小的限制,其大小一般为2000

// ContextUtil 
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
// Constants:最大上下文数量
public final static int MAX_CONTEXT_NAME_SIZE = 2000;

如果当前Context的数量超过了最大限制数量,那么Sentinel默认会返回空的上下文对象,对于该对象Sentinel就不会进行任何规则校验

// ContextUtil
private static final Context NULL_CONTEXT = new NullContext();

// CtSph:忽略规则链,直接创建对象
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。可以看到生成Context时传了一个固定值sentinel_default_context,使用过sentinel控制台的应该都了解,我们的请求资源一般都会挂在一个默认的父节点下,这就是Sentinel会默认为我们生成父节点。

if (context == null) {
            // Using default context.
            context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
 }

// Constants
public final static String CONTEXT_DEFAULT_NAME = "sentinel_default_context";

2.3 ProcessorSlot

有了上下文,回顾一下CtEntry资源类中还有个调用链路ProcessorSlot的字段,接下来则是构造该链路

ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

在来回顾一下下图,在这里就是构造这样一条调用链

CommonsMultipartReslover 源码 sentry 源码_spring cloud

Sentinel是通过Spi机制首先构建出SlotChainBuilder的builder对象,该配置文件存在com.alibaba.csp:sentinel-core包下的META-INF/services目录中。如果找不到的话就会默认使用DefaultSlotChainBuilder构造类。
SlotChainProvider.class

public static ProcessorSlotChain newSlotChain() {
        if (slotChainBuilder != null) {
            return slotChainBuilder.build();
        }

        // Resolve the slot chain builder SPI.
        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();
    }

创建构造类后通过build方法得到链路,这个链路Sentinel已经事先准备好在配置文件中了,也是通过Spi进行获取的,如果我们想扩展自己的插槽则可以在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

加载类之后还需要做一件事就是对他们进行排序,在这里Sentinel是自定义了一个@Spi注解,通过注解上的order字段链路的排序得到一个排序后的列表

// SpiLoader.class
Collections.sort(sortedClassList, new Comparator<Class<? extends S>>() {
            @Override
            public int compare(Class<? extends S> o1, Class<? extends S> o2) {
                Spi spi1 = o1.getAnnotation(Spi.class);
                int order1 = spi1 == null ? 0 : spi1.order();

                Spi spi2 = o2.getAnnotation(Spi.class);
                int order2 = spi2 == null ? 0 : spi2.order();

                return Integer.compare(order1, order2);
            }
        });

之前分析过ProcessorSlotChain既然是个链路,那么肯定存在着前后节点的引用,这时我们拿到排序后的链表,只需要循环调用加入到下一个节点即可

@Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();

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

            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
        }

        return chain;
}

简单画下ProcessorSlot得到的流程

CommonsMultipartReslover 源码 sentry 源码_业务逻辑_02

可以看到构造链路如此繁琐,所以得到链路后首先要缓存到本地中,在CtSph类中通过一个Map映射缓存该链路

private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
        = new HashMap<ResourceWrapper, ProcessorSlotChain>();

当然这里链路的数量并不时无限的,Sentinel设置默认的大小为6000,如果超过了则直接返回空链路

if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                        return null;
}

如果返回空的链路,那就不存在什么规则校验这类东西,因为Sentinel对于流量控制和规则校验都是在链路中的节点做的,所以链路为空则直接返回对象。

ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
if (chain == null) {
            return new CtEntry(resourceWrapper, null, context);
}

2.4 Entry

在得到了链路ProcessorSlot、上下文Context、资源ResourceWrapper后这下就能构造我们的资源类Entry对象了。

Entry e = new CtEntry(resourceWrapper, chain, context);

3.总结

可以发现Entry的构造流程其实并不复杂,有三个核心东西值得我们关注

  1. ResourceWrapper:一般都是用StringResourceWrapper
  2. Context:线程独有的上下文对象
  3. ProcessorSlot:通过Spi机制加载调用的链路

对于本次流程中出现的一些类进行解释说明

    • SphU:构造Entry的工具类
    • CtSph:提供了构造Entry的接口,SphU内部本质上是调用的Sph提供的接口
    • ContextUtil:管理Context上下文
    • InternalContextUtil:构造Context上下文
    • SlotChainProvider:创建SlotChainBuilder对象
    • SlotChainBuilder:创建ProcessorSlotChain调用链