Sentinel 调用链分析(一)

·  阅读 622

[TOC]

Sentinel 调用链路分析(一)--- 调用链的创建和数据结构解析

预设调用链大图一张

image-20201020173330199

1. Sentinel 工作流程简述

在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如:

  • NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
  • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
  • StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
  • FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
  • AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
  • DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
  • SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;

总体的框架如下

每个资源的调用都会执行一次如下图所示的相对应的调用链(slot chain)

image-20201015151738046

2. Sentinel 调用链源码分析

一个最简单的sentinel demo

  1. 创建一个调用链的上下文
  2. 尝试获取一个资源的entry,如果获取到就执行
  3. 然后退出当前资源,清除掉上下文
// 创建一个名称为entrance1,来源为appA 的上下文Context
ContextUtil.enter("entrance1", "appA");
// 创建一个资源名称nodeA的Entry
 Entry nodeA = SphU.entry("nodeA");
 if (nodeA != null) {
    nodeA.exit();
 }
 // 清除上下文
 ContextUtil.exit();
2.1 获取调用链上下文及初始化 - ContextUtil.enter("entrance1", "appA");
  1. 根据ContextName生成entranceNode,并加入缓存,每个ContextName对应一个入口节点entranceNode
  2. 根据ContextName和entranceNode初始化上下文对象,并将上下文对象设置到当前线程中
  3. 入口节点数量不能大于2000,大于会直接抛异常
  4. 每个ContextName对应一个入口节点entranceNode
  5. 每个entranceNode都有共同的父节点。也就是根节点
  6. 如果没有主动的通过 ContextUtil.enter 去创建一个上下文,SphU.entry也会创建一个默认的上下文
		
		/**
		 * 1. enter方法标志着一次调用上下文的入口,上下文是ThreadLocal类型的,意味着每个线程都有单独的上下文,如果当前线程没有上下文的话则会创建一个上下文
		 */
		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,大于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 {
                                // 根据上下文名称生成入口节点(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.2 调用链的创建和触发 - Entry nodeA = SphU.entry("nodeA");
public class SphU {

    private static final Object[] OBJECTS0 = new Object[0];

    private SphU() {}

    /**
     * Record statistics and perform rule checking for the given resource.
     * 记录统计并执行给定的资源规则检查。
     *
     * @param name the unique name of the protected resource
     * @return the {@link Entry} of this invocation (used for mark the invocation complete and get context data)
     * @throws BlockException if the block criteria is met (e.g. metric exceeded the threshold of any rules)
     */
    public static Entry entry(String name) throws BlockException {
        return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
    }
  	
  	...
}
// 一步一步跟进后

一步一步跟进后, 到 com.alibaba.csp.sentinel.CtSph#entryWithPriority()

public class CtSph implements Sph {
	
   	/**
     * Do all {@link Rule}s checking about the resource.
     * 对请求的资源做所有规则的价差
     *
     * <p>Each distinct resource will use a {@link ProcessorSlot} to do rules checking. Same resource will use
     * same {@link ProcessorSlot} globally. </p>
     * 每种不同的资源将使用不同的处理槽进行检查,全局相同的资源则使用相同的处理槽进行检查
     *
     * <p>Note that total {@link ProcessorSlot} count must not exceed {@link Constants#MAX_SLOT_CHAIN_SIZE},
     * otherwise no rules checking will do. In this condition, all requests will pass directly, with no checking
     * or exception.</p>
     * 需要注意总的 ProcessorSlot 数不得超过 MAX_SLOT_CHAIN_SIZE 个,否则任何检查都不会执行,在这种情况下所有的请求都将直接通过,不会有检查或者异常抛出
     *
     * @param resourceWrapper resource name
     * @param count           tokens needed
     * @param args            arguments of user method call
     * @return {@link Entry} represents this call
     * @throws BlockException if any rule's threshold is exceeded
     */
    public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
        return entryWithPriority(resourceWrapper, count, false, 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);
        }

        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<Object> chain = lookProcessChain(resourceWrapper);

        /*
         * Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
         * so no rule checking will be done.
         * 只会在 ProcessorSlot 超过 Constants.MAX_SLOT_CHAIN_SIZE的情况下才会触发
         */
        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) {
            // This should not happen, unless there are errors existing in Sentinel internal.
            RecordLog.info("Sentinel unexpected exception", e1);
        }
        return e;
    }

}
总结:
 1. 每种不同的资源将使用不同的 ProcessorSlotChain 进行处理,全局相同的资源则使用相同的 ProcessorSlotChain 进行检查
 2. 需要注意总的 ProcessorSlotChain 数不得超过 MAX_SLOT_CHAIN_SIZE 个,否则任何检查都不会执行,在这种情况下所有的请求都将直接通过,不会有检查或者异常抛出
 3. ProcessorSlotChain 和上下文无关,只和资源有关
2.2.1 调用链的创建 -- (通过SPI机制初始化 SlotChainBuilder 和 ProcessorSlotChain)

调用链可以类比为spring mvc中的拦截器链,一个拦截器一个拦截器进行拦截处理,每个拦截器都有不同的功能职责,任何一个条件不通过都可以禁止访问,

ProcessorSlot也是同样的道理

image-20201020143135113

  ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
        ProcessorSlotChain chain = chainMap.get(resourceWrapper);//1 查询当前的执行链缓存中是不是有指定资源的执行链
        if (chain == null) {// 2 如果不存在缓存的指定资源的执行链,则开始创建逻辑
            synchronized (LOCK) {// 3 使用加锁和double检查,因为要保持多线程情况下的同一个资源的单例执行链,因为不是volidate,可能还是有问题?
                chain = chainMap.get(resourceWrapper);// 4
                if (chain == null) {//5
                    if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {// 6
                        return null;
                    }
                  	// 通过 SlotChainProvider 创建执行链
                    chain = SlotChainProvider.newSlotChain();// 7
                    Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
                        chainMap.size() + 1);
                    newMap.putAll(chainMap);
                    // 该资源和执行链的关系
                    newMap.put(resourceWrapper, chain);
                    chainMap = newMap;
                }
            }
        }
        return chain;
    }



    /**
     * A provider for creating slot chains via resolved slot chain builder SPI.
     * 一个通过SPI机制创建SlotChain的生产者类
     *
     * @author Eric Zhao
     * @since 0.2.0
     */
    public final class SlotChainProvider {

        private static volatile SlotChainBuilder slotChainBuilder = null;

        /**
         * The load and pick process is not thread-safe, but it's okay since the method should be only invoked
         * via {@code lookProcessChain} in {@link com.alibaba.csp.sentinel.CtSph} under lock.
         * 该方法不是线程安全的,不过没关系,只能通过 com.alibaba.csp.sentinel.CtSph.lookProcessChain 来调用,该方法保证了线程安全
         * @return new created slot chain
         */
        public static ProcessorSlotChain newSlotChain() {
            if (slotChainBuilder != null) {
                // 如果 slotChainBuilder 不为空,证明已经初始化过,直接build创建执行链即可
                return slotChainBuilder.build();
            }

            // Resolve the slot chain builder SPI.
            // 通过SPI机制初始化 slotChainBuilder, 通过SPI机制就可以自定义扩展点扩展或者使用默认的,默认的扩展点就是如下图所示的Slots
            slotChainBuilder = SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);

            if (slotChainBuilder == null) {
                // Should not go through here.
              	// 一般是不应该走到这里的,正常情况下通过SPI,就可以初始化完成,不过为了容错,这里面再创建一个默认的 DefaultSlotChainBuilder
                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();
        }
      
        private SlotChainProvider() {}
    }




image-20201020143523868

构建 ProcessorSlotChain:

NodeSelectorSlot -> ClusterBuilderSlot -> LogSlot -> StatisticSlot -> AuthoritySlot -> SystemSlot -> FlowSlot -> DegradeSlot

public class DefaultSlotChainBuilder implements SlotChainBuilder {

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

        // 根据SPI获取配置中的slot列表按顺序初始化 ProcessorSlotChain
        List<ProcessorSlot> 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;
    }
}
2.2.2 ProcessorSlot 链式调用原理
  • DefaultProcessorSlotChain

image-20201020150650611

DefaultProcessorSlotChain 的本质就是一个链表,该类比较简单,下面总结下该类的特点,不做源码分析

1. DefaultProcessorSlotChain是一个链表
2. DefaultProcessorSlotChain 有两个类型为 AbstractLinkedProcessorSlot<?> 的 first;(链表表头节点)和  end (链表表尾节点)
3. 根据 DefaultProcessorSlotChain 的UML图可以看到
   1. DefaultProcessorSlotChain 实现了 ProcessorSlotChain的往表头和表尾添加元素的方法
   2. DefaultProcessorSlotChain 实现了 AbstractLinkedProcessorSlot getNext()获取下一个元素和setNext()设置下一个元素的方法
   3. DefaultProcessorSlotChain 实现了 ProcessorSlot 的进入调用链entry()方法 和 退出 调用链 exit() 方法
  • AbstractLinkedProcessorSlot 类比较重要链式调用的关键
public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {

    private AbstractLinkedProcessorSlot<?> next = null;

  	/**
  	 * fireEntry方法中用调用链的下一个元素 next 调用 transformEntry,再进入下一个元素的 entry方法,entry方法的最后一行代码就是fireEntry ()然后再次
  	 * 进入自己的 fireEntry()方法,再去进行下一个元素的相同的调用逻辑,完成链式调用;
  	 *
  	 * 如 LogSlot 所示
  	 * 
  	 */ 
    @Override
    public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
        throws Throwable {
        if (next != null) {
            next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
        }
    }

    @SuppressWarnings("unchecked")
    void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args)
        throws Throwable {
        T t = (T)o;
        entry(context, resourceWrapper, t, count, prioritized, args);
    }

    @Override
    public void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        if (next != null) {
            next.exit(context, resourceWrapper, count, args);
        }
    }

    public AbstractLinkedProcessorSlot<?> getNext() {
        return next;
    }

    public void setNext(AbstractLinkedProcessorSlot<?> next) {
        this.next = next;
    }

}

@SpiOrder(-8000)
public class LogSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode obj, int count, boolean prioritized, Object... args)
        throws Throwable {
        try {
            fireEntry(context, resourceWrapper, obj, count, prioritized, args);
        } catch (BlockException e) {
            EagleEyeLogUtil.log(resourceWrapper.getName(), e.getClass().getSimpleName(), e.getRuleLimitApp(),
                context.getOrigin(), count);
            throw e;
        } catch (Throwable e) {
            RecordLog.warn("Unexpected entry exception", e);
        }

    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        try {
            fireExit(context, resourceWrapper, count, args);
        } catch (Throwable e) {
            RecordLog.warn("Unexpected entry exit exception", e);
        }
    }
}
  • ProcessorSlot UML图

    每个SLot都继承了AbstractLinkedProcessorSlot 类并实现了 ProcessorSlot 的 entry()进入方法 和 exit()退出 方法

image-20201020152518072

分类:
阅读
标签:
分类:
阅读
标签:
收藏成功!
已添加到「」, 点击更改