Sentinel 全系列之五 —— 责任链源码


1. 限流从何处开始

  在Sentinel系列三的文章中已经讲解了Sentinel的限流的入口是SphU.entry()这个方法,本篇就会带大家深入方法源码去探寻Sentinel的限流流程。

  entry()源码:

public static Entry entry(String name, EntryType type, int count, Object... args) throws  BlockException { 
    return Env.sph.entry(name, type, count, args); 
}

2.初始化

  Env.sph.entry(name, type, count, args)这个方法里面会调用Env的sph静态方法,我们首先进入到Env中:

public class Env {
    public static final Sph sph = new CtSph();
    static {
        // If init fails, the process will exit.
        InitExecutor.doInit();
    }
}

  这个方法初始化的时候会调用InitExecutor.doInit()

/**
* 加载已经注册的功能模块,并按照注册的顺序执行
*/
public final class InitExecutor {
  private static AtomicBoolean initialized = new AtomicBoolean(false);
  /**
   *如果在加载某一个InitFunc出现异常,初始化进程将立即中断,应用将退出.
   * 初始化动作只执行一次.
   */
  public static void doInit() {
  	//判断是否是第一次初始化,不是则直接返回
      if (!initialized.compareAndSet(false, true)) {
          return;
      }
      try {
        //通过spi加载InitFunc子类
        List<InitFunc> initFuncs = SpiLoader.of(InitFunc.class).loadInstanceListSorted();
          List<OrderWrapper> initList = new ArrayList<OrderWrapper>();
          for (InitFunc initFunc : loader) {
              RecordLog.info("[InitExecutor] Found init func: " + initFunc.getClass().getCanonicalName());
             //给所有的initFunc排序,按@InitOrder从小到大进行排序
            //然后封装成OrderWrapper对象
              insertSorted(initList, initFunc);
          }
          for (OrderWrapper w : initList) {
          //挨个执行每个InitFunc实现类的init()方法,init()方法又会去加载其它所需资源
              w.func.init();
              RecordLog.info(String.format("[InitExecutor] Initialized: %s with order %d",
                  w.func.getClass().getCanonicalName(), w.order));
          }
      } catch (Exception ex) {
          RecordLog.warn("[InitExecutor] Init failed", ex);
          ex.printStackTrace();
      } catch (Error error) {
          RecordLog.warn("[InitExecutor] Init failed with fatal error", error);
          error.printStackTrace();
          throw error;
      }
  }
}

  这个InitExecutor.doInit方法会去加载初始化组件,这个初始化组件就是通过SPI机制加载的。它会去加载com.alibaba.csp.sentinel.init.InitFunc不同的实现类,并且按照这些实现类上的@InitOrder注解进行排序。

image.png

  • CommandCenterInitFunc:用于初始化所有CommandCenter,对外提供客户端的数据处理功能;
  • DefaultClusterClientInitFunc:用于初始化集群限流客户端的所需资源;
  • DefaultClusterServerInitFunc:用于初始化集群限流服务端的所需资源;
  • HeartbeatSenderInitFunc:用于初始化心跳发送者,在系列文章四中有详细介绍
  • ParamFlowStatisticSlotCallbackInit:用于初始化参数流控回调.

3.责任链

3.1. Tree、Node、Context

  在走插槽链流程之前,我们先总结一下调用树的三种节点以及Context。

  首先是这个Tree,这个Tree是由NodeSelectorSlot这个插槽来创建的,每创建一个树都有一个Root节点,这个Root节点就代表的是一个应用,一个应用只会创建一个Root节点。就比如说一个一个dubbo项目中会有provider和consumer,这一个consumer就是一个应用。

image.png

  一个应用中可以定义很多资源可以是代码块、方法等等:

image.png

  当属于同一个应用下的请求过来后,会首先判断是否有Root,没有的话就会新建一个Root节点。 再往下就会出现EntranceNode、DefaultNode、CLusterNode三种node,那这三种节点又是什么关系呢?我们首先看一下这三个节点类的关系图:

image.png

  其中StatsticNode是用作数据统计的,那根据继承关系其三个子类Node也是进行数据统计,那他们分别的作用又是什么呢? 在讲这三个Node前,大家要明白Sentinel的一个核心概念Context:

  Context是对资源操作的上下文,每个资源操作必须属于一个Context。它会保存一次资源访问链路元数据和该资源所对应的实时信息,链路的各个节点都能通过获取链路绑定的context来获取一些信息进行相应的处理。如果代码中没有指定Context,则会创建一个name为sentinel_default_context的默认Context。一个Context生命周期中可以包含多个资源操作。Context生命周期中的最后一个资源在exit()时会清理该Conetxt,这也就意味着这个Context生命周期结束了。

  接下来就开始介绍三个节点:

image.png

  • Node:用于完成数据统计的接口
  • StatisticNode:统计节点,是Node接口的实现类,用于完成数据统计
  • EntranceNode:入口节点,一个Context会有一个入口节点,用于统计当前Context的总体流量数据
  • DefaultNode:默认节点,用于统计一个资源在当前Context中的流量数据
  • ClusterNode:集群节点,用于统计一个资源在所有Context中的总体流量数据   而Sentinel也是通过滑动窗口算法高效的实现数据统计,这个算法请参考系列文章。

2.2 构建责任链

  初始化结束后,我们继续走代码流程,发现最后调用到CtSph的entry方法

/**
* @param resourceWrapper 资源名字
* @param count           限流数
* @param args            arguments of user method call
*/
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
    return entryWithPriority(resourceWrapper, count, false, args);
}

  entryWithPriority()源码:

   private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
       throws BlockException {
       // 从ThreadLocal中获取context
       // 一个请求会占用一个线程,一个线程会绑定一个context
       Context context = ContextUtil.getContext();
       // 若context是NullContext类型,则表示当前系统中的context数量已经超出的阈值
       // 即访问请求的数量已经超出了阈值。此时直接返回一个无需做规则检测的资源操作对象
       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并将其放入到ThreadLocal
       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);
       }

       // 查找SlotChain,详情见下文
       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.
        */
       // 若没有找到chain,则意味着chain数量超出了阈值,则直接返回一个无需做规则检测的资源操作对象
       if (chain == null) {
           return new CtEntry(resourceWrapper, null, context);
       }

       // 创建一个资源操作对象
       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. 获取上下文context
  2. 使用责任链这种设计模式创建功能插槽
  3. 走责任链流程对资源对象进行限流等操作

  接下来将详细介绍这三件事儿:

3.2.1 ContextUtil.getContext()源码:

  首先来看ContextUtil的静态代码:

    private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>(); 
    static {
        // Cache the entrance node for default context.
        initDefaultContext();
    }

    private static void initDefaultContext() {
    String defaultContextName = Constants.CONTEXT_DEFAULT_NAME;
    //初始化一个sentinel_default_context,type为in的队形
    EntranceNode node = new EntranceNode(new StringResourceWrapper(defaultContextName, EntryType.IN), null);
    //Constants.ROOT会初始化一个name是machine-root,type=IN的对象
    Constants.ROOT.addChild(node);
    //所以现在map里面有一个key=CONTEXT_DEFAULT_NAME的对象
    contextNameNodeMap.put(defaultContextName, node);
    }

  通过这段源码可以看出ContextUtil在初始化的时候会先调用initDefaultContext方法。通过Constants.ROOT创建一个root节点,然后将创建的node节点加入到root的子节点中,并存入contextNameNodeMap中。

  然后我们再回到entryWithPriority()方法中发现,其中调用了InternalContextUtil的internalEnter(Constants.CONTEXT_DEFAULT_NAME)这个方法,InternalContextUtil是继承了ContextUtil,源码如下:

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

  trueEnter方法源码如下:

protected static Context trueEnter(String name, String origin) {
        // 从ThreadLocal中获取Context
        Context context = contextHolder.get();
        // 若ThreadLocal中没有context,则尝试着从缓存map中获取
        if (context == null) {
            // 本地缓存map的key为context名称,value为EntranceNode
            Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
            // 获取EntranceNode——双重检测锁DCL——为了防止并发创建
            DefaultNode node = localCacheNameMap.get(name);
            if (node == null) {
                // 若缓存map的size 大于 context数量的最大阈值,则直接返回NULL_CONTEXT
                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);
                                // Add entrance node.将新建的node添加到ROOT
                                Constants.ROOT.addChild(node);

                                // 将新建node存入到缓存map
                                // 为了防止“迭代稳定性问题”——iterate stable——对于共享集合的写操作
                                Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
                                newMap.putAll(contextNameNodeMap);
                                newMap.put(name, node);
                                contextNameNodeMap = newMap;
                            }
                        }
                    } finally {
                        LOCK.unlock();
                    }
                }
            }
            // 将context的name与entranceNode封装为context
            context = new Context(node, name);
            // 初始化context的来源
            context.setOrigin(origin);
            // 将context写入到ThreadLocal
            contextHolder.set(context);
        }

        return context;
    }

  在获取上下文时,首先会从线程中获取到Context,若获取到则会直接返回context;若为线程首次获取则为空,然后会根据context的名称去缓存中找表示入口的节点ErtanceNode,如果Entrancenode为空则会通过一个双重检测锁的机制创建一个EntranceNode,然后会将Context对象中的入口节点设置为新创建的 EntranceNode。最后将context对象存入到ThreadLocal中。

3.2.2. lookProcessChain(resourceWrapper)源码:

private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
    = new HashMap<ResourceWrapper, ProcessorSlotChain>();
    
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
        // 从缓存map中获取当前资源的SlotChain
        // 缓存map的key为资源,value为其相关的SlotChain
        ProcessorSlotChain chain = chainMap.get(resourceWrapper);
        // DCL
        // 若缓存中没有相关的SlotChain,则创建一个并放入到缓存
        if (chain == null) {
            synchronized (LOCK) {
                chain = chainMap.get(resourceWrapper);
                if (chain == null) {
                    // Entry size limit.
                    // 缓存map的size >= chain数量最大阈值,则直接返回null,不再创建新的chain
                    if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                        return null;
                    }

                    // 创建新的chain
                    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;
    }

  我们可以发现当执行该方法时会首先在内存中查找是否已经有chain,若没有就创建一个新的chain,创建源码如下:

public static ProcessorSlotChain newSlotChain() {
        // 若builder不为null,则直接使用builder构建一个chain,否则先创建一个builder
        if (slotChainBuilder != null) {
            return slotChainBuilder.build();
        }

        // Resolve the slot chain builder SPI.
        // 通过SPI方式创建一个builder
        slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();

        // 若通过SPI方式未能创建builder,则手工new一个DefaultSlotChainBuilder
        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());
        }
        // 构建一个chain
        return slotChainBuilder.build();
    }

默认调用DefaultSlotChainBuilder的build方法根据sentinel-core/src/main/resources/META-INF/services下的com.alibaba.csp.sentinel.slotchain.ProcessorSlot文件内容进行初始化,初始化顺序也是按照这些slot类的@Order注解顺序排列的:

@Spi(isDefault = true)
public class DefaultSlotChainBuilder implements SlotChainBuilder {

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

        // 通过SPI方式构建Slot
        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;
            }
            //将每一个功能插槽放入到责任链中,这里的责任链是一个单向链表,默认包含一个节点,且有两个指针first与end同时指向这个节点
            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
        }

        return chain;
    }
}

com.alibaba.csp.sentinel.slotchain.ProcessorSlot:

image.png

debug时构造的chain链表:

ProcessorSlot<Object> chain = this.lookProcessChain(resourceWrapper);

image.png

  此时责任链已经构造完毕,接下来就该开始走责任链的工作流程了。

3.2.3. chain.entry(context, resourceWrapper, null, count, prioritized, args)源码:

  从debug中,我们看到代码首先走的是NodeSelectorSlot,然后依次执行后边的slot。这里我们只介绍StatisticSlot统计信息slot和FlowSlot限流处理slot。

1. StatisticSlot

  StatisticSlot应用了滑动窗口算法来统计信息,统计信息详情请看本系列文章之滑动窗口。

@Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        try {
            // Do some checking.
            // 调用SlotChain中后续的所有Slot,完成所有规则检测
            // 其在执行过程中可能会抛出异常,例如,规则检测未通过,抛出BlockException
            fireEntry(context, resourceWrapper, node, count, prioritized, args);

            // Request passed, add thread count and pass count.
            // 代码能走到这里,说明前面所有规则检测全部通过,此时就可以将该请求统计到相应数据中了
            // 增加线程数据
            node.increaseThreadNum();
            // 增加通过的请求数量
            node.addPassRequest(count);
            ...
    } catch (PriorityWaitException ex) {
        node.increaseThreadNum(); 
            ...
    } catch (BlockException e) {
        //设置错误信息
        // Blocked, set block exception to current entry.
        context.getCurEntry().setError(e);
            ...
        //设置被阻塞的次数
        // Add block count.
        node.increaseBlockQps(count); 
            ...
        throw e;
    } catch (Throwable e) {
        // Unexpected error, set error to current entry.
        context.getCurEntry().setError(e);

        //设置异常的次数
        // This should not happen.
        node.increaseExceptionQps(count); 
            ...
        throw e;
    }
}

2. FlowSlot

  用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制,对应流控规则,源码如下:

@Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        // 检测并应用流控规则
        checkFlow(resourceWrapper, context, node, count, prioritized);
        // 触发下一个Slot
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
        throws BlockException {
        checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
    }

checkFlow()源码:

  这里就是去判断指定资源的流控规则是否能够正常通过,若不能通过则抛出异常,被限流了,且后续规则不再应用。其中canPassCheck就是进行详细判断的方法。方法具体的获取实时流量数据并进行判断的相关源码请参考后续集群流控源码分析。

FlowRuleChecker#checkFlow()

public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
                          Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
        if (ruleProvider == null || resource == null) {
            return;
        }
         // 获取到指定资源的所有流控规则
        Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
        if (rules != null) {
             // 逐个应用流控规则。若无法通过则抛出异常,后续规则不再应用for (FlowRule rule : rules) {
                if (!canPassCheck(rule, context, node, count, prioritized)) {
                    throw new FlowException(rule.getLimitApp(), rule);
                }
            }
        }
    }

到这里Sentinel的主流程就分析完毕了。

3.小结

  sentinel限流处理流程如下: sentinel限流流程.png