Sentinel源码分析(第二篇):NodeSelectorSlot和ClusterBuilderSlot分析

2,071 阅读8分钟

1. 前言

上一篇介绍了Sentinel的Context、Entry、Node相关的信息。在创建Node时,涉及到了NodeSelectorSlot和ClusterBuilderSlot,创建Entry的时候,会创建一个chain。本文会通过源码分析这些对象的作用。

2. 功能插槽

2.1. 功能插槽的作用

当使用SphU.entry()获取资源创建Entry对象的时候,会创建一系列的功能插槽,这些插槽以责任链的方式组合在一起,每个插槽分别处理不同的功能。

在CtSph#entryWithPriority()方法中会尝试去获取chain,如果没有,则创建:

 ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

具体的获取流程:

源码位置: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. 处理链的数量不能超过6000,
                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;
}

可以看到,chain会先保存到一个静态的全局map中,以资源名称为key,chain为value,所以同一个resource只有一个chain。

实际创建chain的地方是SlotChainProvider.newSlotChain():

#源码位置:SlotChainProvider.java

 private static volatile SlotChainBuilder slotChainBuilder = null;

 public static ProcessorSlotChain newSlotChain() {

    //如果builder不为空,创建chain,否则先创建builder,再创建chain
    if (slotChainBuilder != null) {
        return slotChainBuilder.build();
    }

    resolveSlotChainBuilder();

    if (slotChainBuilder == null) {
        RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
        slotChainBuilder = new DefaultSlotChainBuilder();
    }
    return slotChainBuilder.build();
}

2.2. SlotChainBuilder

SlotChainBuilder负责创建功能插槽链,SlotChainBuilder是一个接口,里面只有一个build方法,定义功能插槽如何创建。

可以看到首先看一下SlotChainBuilder有三个实现,不同的实现增加不同的处理能力。

  • DefaultSlotChainBuilder:默认的功能插槽构造器
  • GatewaySlotChainBuilder:网关插槽构造器,里面新增了网关限流功能
  • HotParamSlotChainBuilder:热点参数插槽构造器,里面新增了热点参数功能

这里看一下默认的功能插槽构造器,其他的两个只是在适当的位置新增一个处理插槽。

public class DefaultSlotChainBuilder implements SlotChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
        chain.addLast(new NodeSelectorSlot());
        chain.addLast(new ClusterBuilderSlot());
        chain.addLast(new LogSlot());
        chain.addLast(new StatisticSlot());
        chain.addLast(new SystemSlot());
        chain.addLast(new AuthoritySlot());
        chain.addLast(new FlowSlot());
        chain.addLast(new DegradeSlot());

        return chain;
    }
}

可以看到build方法主要是创建一个DefaultProcessorSlotChain,然后将不同功能的插槽添加进去。

需要注意的是,Sentinel是通过SPI的形式选择使用哪个SlotChainBuilder来创建功能插槽的。

private static final ServiceLoader<SlotChainBuilder> LOADER = ServiceLoader.load(SlotChainBuilder.class);

2.3. ProcessorSlot

ProcessorSlot代表功能插槽对象,定义了进入功能插槽和退出功能插槽的接口,具体的功能插槽都实现了该接口。

ProcessorSlot继承结构如下:

通过继承可以发现,ProcessorSlot主要是两类,一类是具有特定功能的功能插槽,比如限流、统计、降级等,还有一类是主要是形成插槽链,主要就是DefaultProcessorSlotChain。

2.3.1. DefaultProcessorSlotChain

DefaultProcessorSlotChain代表功能处理链的开始,里面定义了两个节点,第一个节点和最后一个节点,并提供新增节点的方法。


public class DefaultProcessorSlotChain extends ProcessorSlotChain {

    //默认先创建一个头结点插槽
    AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {

        @Override
        public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
            throws Throwable {
            super.fireEntry(context, resourceWrapper, t, count, prioritized, args);
        }

        @Override
        public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
            super.fireExit(context, resourceWrapper, count, args);
        }

    };
    AbstractLinkedProcessorSlot<?> end = first;

    @Override
    public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        protocolProcessor.setNext(first.getNext());
        first.setNext(protocolProcessor);
        if (end == first) {
            end = protocolProcessor;
        }
    }

    @Override
    public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        end.setNext(protocolProcessor);
        end = protocolProcessor;
    }
}
2.3.2. AbstractLinkedProcessorSlot

AbstractLinkedProcessorSlot非常的重要,不仅实现了ProcessorSlot,还实现功能插槽链之间的如何连接的。

    private AbstractLinkedProcessorSlot<?> next = null;

AbstractLinkedProcessorSlot里面通过新增一个next指针标记下一个功能处理槽是哪一个,这样,在一个功能槽处理完流程后,通过next可以找到下一个功能槽,退出的时候也是一样的逻辑。

2.4. 功能插槽小结

上面讲了功能插槽创建的流程,Sentinel是如何将所有的插槽组织工作的。但具体每个功能插槽是怎么工作的,在这个地方先不讲。最后,通过一个图看一下DefaultSlotChainBuilder创建的功能插槽链结构:

3. NodeSelectorSlot

官方文档是这样描述NodeSelectorSlot的:这个 slot 主要负责收集资源的路径,并将这些资源的调用路径以树状结构存储起来,用于根据调用路径进行流量控制。

先看源码:

public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {

    /**
     * {@link DefaultNode}s of the same resource in different context.
     */
    private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
        throws Throwable {
        DefaultNode node = map.get(context.getName());
        if (node == null) {
            synchronized (this) {
                node = map.get(context.getName());
                if (node == null) {
                    node = new DefaultNode(resourceWrapper, null);
                    HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
                    cacheMap.putAll(map);
                    cacheMap.put(context.getName(), node);
                    map = cacheMap;
                    // Build invocation tree
                    ((DefaultNode) context.getLastNode()).addChild(node);
                }

            }
        }

        context.setCurNode(node);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

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

NodeSelectorSlot会创建一个DefaultNode,然后将该节点设置到Context下的最新一个节点子节点列表中,然后将上下文的当前节点设置为本次创建的节点。

public Node getLastNode() {
    if (curEntry != null && curEntry.getLastNode() != null) {
        return curEntry.getLastNode();
    } else {
        return entranceNode;
    }
}

上面是获取最新节点的逻辑。

NodeSelectorSlot主要作用就是收集资源的路径。在这个Slot中,创建了一个DefaultNode,在上一篇中讲到DefaultNode是以Context和Resource为维度保存统计指标的,但是这个地方的map是根据Context来保存的,这个是为什么呢?需要注意的是功能插槽处理链的创建是根据Resource来的,一个Resource只会创建一个chain,不同的Resource创建不同的chain,所以这地方虽然是使用Context作为key,但其实是Context和Resource为维度的。

4. ClusterBuilderSlot

上面的NodeSelector是创建DefaultNode节点,CLusterBuilderSlot主要是负责创建ClusterNode和来源节点。至于这两个节点的作用,在上一篇已经讲过。

public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();

    private static final Object lock = new Object();

    private volatile ClusterNode clusterNode = null;

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args)
        throws Throwable {
        if (clusterNode == null) {
            synchronized (lock) {
                if (clusterNode == null) {
                    // Create the cluster node.
                    clusterNode = new ClusterNode();
                    HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
                    newMap.putAll(clusterNodeMap);
                    newMap.put(node.getId(), clusterNode);

                    clusterNodeMap = newMap;
                }
            }
        }
        node.setClusterNode(clusterNode);

        /*
         * if context origin is set, we should get or create a new {@link Node} of
         * the specific origin.
         */
        //如果context定义了来源名称,需要给当前的entry创建一个originNode
        if (!"".equals(context.getOrigin())) {
            Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
            context.getCurEntry().setOriginNode(originNode);
        }

        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }
}

可以看到,ClusterBuilderSlot中有一个ClusterNode类型的变量,因为之前说过,同一个Resource只有一个Chain,所以这个地方创建的ClusterNode是统计某个Resource的数据,所以ClusterNode的统计维度为Resource。

如果定义了入口的来源,这里还会创建一个来源节点,作为统计来源节点相关数据的node,这个地方的统计维度是同一个Resource和orgin为维度。

5. 创建Node、Context、Entry整体流程

上面讲NodeSelectorSlot和ClusterBuilderSlot的时候,只是把大致的流程说一下,至于创建了Node后整个结构是什么样的,这个需要再说一下,如果在看上一篇和这篇中的内容时比较乱,可以看了这一小节后,再去看一下之前的内容。因为到此,整个Sentinel保存统计数据的流程才讲完。

5.1. 再次回忆Context、Entry、Node的创建时机

  • Context:在使用ContextUtil.entry()或者SphU.entry()的时候会创建Context。
  • EntranceNode:在创建Context的时候创建EntranceNode,并赋值给Context。
  • ProcessorSlot::在使用SphU.entry()创建,同一个Resource共享一个。
  • Entry:在使用SphU.entry()创建,并通过parent和child将Entry链连接,同时将该Entry对象赋值给Context的curEntry.
  • DefaultNode:在NodeSelectorSlot中创建,以Resource和Context为维度
  • ClusterNode:在ClusterBuilderSlot中创建,以Resource为维度。
  • 来源源节点:在ClusterBuilderSlot中创建,以Resource和orgin为维度。

5.2. 流程图

首先先看一下代码:

 ContextUtil.enter("context1","origin1");
 Entry entry = SphU.entry("resource1");

当创建一个Entry的时候,整个关系图如下:

这个时候在获取同一个资源一次:

 Entry entry2 = SphU.entry("resource1");

关系图如下:

接下来在创建不同资源的:

Entry entry3 = SphU.entry("resource2");
Entry entry4 = SphU.entry("resource3");

关系图如下:

上面是在同一个Context下,如果是在不同的Context下,由于chain是以Resource为维度的,所以ClusterNode对同一个Resource只会存在一个,所以多个Context下,关系图就是把上图复制一份,只是ClusterNode指向同一个。

根据关系图,可以很清晰的看到ClusterNode、DefaultNode、OrginNode是按什么样的维度来进行统计的。

6. 小结

本篇主要讲Sentinel通过责任链的方式将各种插槽组合起来实现限流降级等功能,并分析了前两个Slot:NodeSelectorSlot和ClusterNodeSlot的作用。最后结合第一篇的内容画出不同场景下Context、Entry、Node的关系,更加体现ClusterNode、DefaultNode、源节点是按照什么维度进行统计的。在下一篇中,将继续分析Sentinel中的统计功能StatisticSlot,以及各种Node节点是怎么保存数据的。

7. 参考资料

Sentinel工作主流程