Spring Cloud 程序员

Sentinel学习笔记(3)-- 上下文统计Node建立分析

2018-11-19  本文已影响25人  ro9er

前言

在完成了流量统计限流逻辑两部分的分析之后,降级其实也就非常简单了,所以我的注意力也回到了Sentinel入口调用部分,这一部分构建了Sentinel的上下文,创建了调用链并且在NodeSelectorSlot和ClusterBuilderSlot两个单元中构建了整个Sentinel资源调用的上下文和统计数据存储单元,为后面的限流熔断逻辑提供了功能基础,也是我花时间分析最多的地方。这里要感谢逅弈的这篇文章,对我帮助不少。

概念解释

在介绍具体逻辑之前,我们还是先明确几点基本的概念(下列描述有的出自官方代码注释):

每当我们调用SphU.entry() 或者 SphO.entry()获取访问资源许可的时候都需要当前线程处在某个context中,如果我们没有显式调用ContextUtil.enter(),默认会使用Default context。
如果我们在一个上下文中多次调用SphU.entry()来获取多个资源,一个调用树就会被创建出来

这一堆说完可能大家比较懵。我们通过一些结构图来换个角度看看: 继承结构图

上图大致说明了这几个概念本身的继承关系以及各自的包含关系。接下来我们通过代码来解释一下。

代码解析

Context初体验

我们按照demo,首先要看的是ContextUtil.enter这个函数调用

    static public Context enter(String name, String origin) {
        // 判断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);
    }

    /**
     * 调用创建上下文
     * @param name 上下文名称
     * @param origin 调用方
     * @return
     */
    protected static Context trueEnter(String name, String origin) {
        // 查看threadLocal中有没有context
        Context context = contextHolder.get();
        // 如果没有
        if (context == null) {
            // 用local变量来访问violatile,避免并发访问空指针隐患的同时提升效率
            // 讲究
            Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
            // 查看是否有context name对应的entranceNode
            DefaultNode node = localCacheNameMap.get(name);
            // 如果没有entranceNode 则需要创建
            if (node == null) {
                //超最大数量
                if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                    setNullContext();
                    return NULL_CONTEXT;
                } else {
                    try {
                        //锁定
                        LOCK.lock();
                        //double check 重新检查一次
                        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.
                                // context之间没有层级结构,只有root -> entrance
                                Constants.ROOT.addChild(node);
                                // 重新构建context entranceNode Map
                                Map<String, DefaultNode> newMap = new HashMap<String, DefaultNode>(
                                    contextNameNodeMap.size() + 1);
                                newMap.putAll(contextNameNodeMap);
                                newMap.put(name, node);
                                contextNameNodeMap = newMap;
                            }
                        }
                    } finally {
                        LOCK.unlock();
                    }
                }
            }
            // 根据 entranceNode创建context
            // 这里有个特性需要关注: entranceNode是线程共享的,
            // 而context是线程独有的,所以对于同一个name的上下文
            // 可能多个线程有多个context实例,虽然entranceNode是只有一个
            context = new Context(node, name);
            // 设置origin, 并设置到线程上下文中
            context.setOrigin(origin);
            contextHolder.set(context);
        }
        return context;
    }

上面代码有注释解释,也比较简单,需要注意的是最后注释中提到的,Context是线程独有的,对于同一个名称的上下文,entranceNode只会有一个,但是Context可能有多个线程使用的多个示例,另外,对Context本身的操作不需要考虑线程协同,因为是线程独有的。

SphU走起

在创建了一个上下文之后,我们看SphU.entry又在做什么

    /**
     * Checking all {@link Rule}s about the resource.
     *
     * @param name the unique name of the protected resource
     * @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded.
     */
    public static Entry entry(String name) throws BlockException {
        return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
    }

这里的Env.sph使用的是一个标准实现CtSph:

    @Override
    public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
        StringResourceWrapper resource = new StringResourceWrapper(name, type);
        return entry(resource, count, args);
    }

    public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
        // 获取线程上下文中的context
        Context context = ContextUtil.getContext();
        //如果是NullContext 说明已经超了内存阈值无法创建新的context
        // 直接返回一个ctEntry并且chain为null,表示不做任何限流熔断操作直接放过
        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);
        }
        // 如果为null 说明当前线程上下文中没有context,使用default context
        if (context == null) {
            // Using default context.
            context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
        }

        // Global switch is close, no rule checking will do.
        if (!Constants.ON) {
            return new CtEntry(resourceWrapper, null, context);
        }

        // 获取资源所对应的chain
        // 这里代码就不展开了,需要注意的是,同一个resource
        // 共享同一个处理Slot链条,并使用这个链条来完成限流熔断逻辑 切记
        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
        Entry e = new CtEntry(resourceWrapper, chain, context);
        try {
            // 开始链条处理
            chain.entry(context, resourceWrapper, null, count, 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;
    }

这里代码也基本比较简单,需要注意的是同一个资源会使用同一条Slot Chain,也就是说,如果同一个资源在不同的Context下都有调用,它们使用的也会是同一个处理链条。
另外还有一个需要注意的是,创建CtEntry的地方:

    CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
        super(resourceWrapper);
        this.chain = chain;
        this.context = context;

        setUpEntryFor(context);
    }

    /**
     * 整理当前上下文中的调用链路关系
     * @param context
     */
    private void setUpEntryFor(Context context) {
        // The entry should not be associated to NullContext.
        if (context instanceof NullContext) {
            return;
        }
        // 获取当前的entry 并且赋给 parent,即表示当前entry的上游资源调用
        this.parent = context.getCurEntry();
        if (parent != null) {
            // 如果当前parent有值,说明在此之前有SphU.entry调用并且没有exit
            // 则把自己赋给parent的儿子,完成调用链条
            ((CtEntry)parent).child = this;
        }
        //将线程context的当前资源调用指向自己
        context.setCurEntry(this);
    }

上面这段代码比较简单,但是可能大家没有一个比较直观的感觉,我这里也稍加说明一下:

对于一段代码 Untitled Diagram (7).png
ContextUtil.enter("context-test", "");
Entry ea = SphU.entry("resouceA");
Entry eb = SphU.entry("resouceB");
eb.exit();
ea.exit();

当执行到

ContextUtil.enter("context-test", "");

时,context的内容为: 新创建的context

当执行到

Entry ea = SphU.entry("resouceA");

时,context内容为: 获取资源A权限后

当执行到

Entry eb = SphU.entry("resouceB");

时,context内容为: 获取资源B权限后

这就构建了一个调用关系,即我们在上下文context-test中,先获取了resourceA的权限,再获取了resourceB的权限,这两个资源在调用关系上存在一个先后关系。当我们在后面调用exit的时候,也是先退curEntry指向的entry,并且把curEntry指向parent,后面再退他的parent,代码这里就不过多扩展了。接下来我们就来到了Slot链

NodeSelectorSlot

作为责任链的第一个Slot,我们先来看看NodeSelectorSlot的entry

   @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args)
        throws Throwable {
        // 按照上下文名称获取统计上下文对应的DefaultNode
        // 因为之前说过同一个resource会共享同一个chain
        // 所以这个map中的所有元素都属于同一个resource
        // 只是按照context name来区分了不同的上下文环境
        DefaultNode node = map.get(context.getName());
        if (node == null) {
            synchronized (this) {
                // double check
                node = map.get(context.getName());
                if (node == null) {
                    // 创建一个DefaultNode
                    node = Env.nodeBuilder.buildTreeNode(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);
            }
        }
        
        // 将这个node放入到context 的curNode
        // 实际上是context.curEntry.setCurNode(node)
        context.setCurNode(node);
        fireEntry(context, resourceWrapper, node, count, args);
    }

上面代码也比较简单,注释也写的比较细了,但是有一个地方很神奇:

((DefaultNode)context.getLastNode()).addChild(node);

这里是在做甚哪?
我们跟进去看看

    //context.getLastNode()
    public Node getLastNode() {
        if (curEntry != null && curEntry.getLastNode() != null) {
            return curEntry.getLastNode();
        } else {
            return entranceNode;
        }
    }
    //CtEntry.getLastNode()
    @Override
    public Node getLastNode() {
        return parent == null ? null : parent.getCurNode();
    }

这两段代码看起来很简单,但是绕的弯也比较多,我们还是通过图形来分析,依然是之前那个例子:

ContextUtil.enter("context-test", "");
Entry ea = SphU.entry("resouceA");
Entry eb = SphU.entry("resouceB");
eb.exit();
ea.exit();

当执行到

Entry ea = SphU.entry("resouceA");

时, 这里执行的结果为:


获取完resouceA权限之后

当执行到

Entry eb = SphU.entry("resouceB");

时, 这里执行的结果为:


获取完资源B的权限之后

根据两张图我们可以知道,通过这个slot,我们构建了一个完整的调用树。
看完这个Slot之后,我们有个疑问,我们针对一个资源在某个context创建了统计的node,那如果我们要针对context无关来做统计呢?这就是我们要看的下一个Slot的职责了。

ClusterBuiilderSlot

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args)
        throws Throwable {
        // 因为slot chain 是对于某个resource特定的
        // 因此这个slot中的private 变量clusterNode也是对于某个resource全局共享的
        if (clusterNode == null) {
            synchronized (lock) {
                // double check 创建clusterNode 无需多言
                if (clusterNode == null) {
                    // Create the cluster node.
                    clusterNode = Env.nodeBuilder.buildClusterNode();
                    HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<ResourceWrapper, ClusterNode>(16);
                    newMap.putAll(clusterNodeMap);
                    newMap.put(node.getId(), clusterNode);

                    clusterNodeMap = newMap;
                }
            }
        }
        // 设置clusterNode 到context相关的defaultNode中
        node.setClusterNode(clusterNode);

        /*
         * if context origin is set, we should get or create a new {@link Node} of
         * the specific origin.
         */
        if (!"".equals(context.getOrigin())) {
            // 如果origin调用方不为空,则创建一个对应的统计Node
            // PS origin和context并没有交叉。是平行的统计空间
            // 这里也是double check创建,无需展开
            Node originNode = node.getClusterNode().getOriginNode(context.getOrigin());
            context.getCurEntry().setOriginNode(originNode);
        }
        
        // 调用下一步
        fireEntry(context, resourceWrapper, node, count, args);
    }

这里代码就比较简单了,其实就是创建了一个对于resource全局共享的ClusterNode,并完成了信息的绑定。

后面的slot就是我在系列文章最开始所讲的StatisticNode,这里就不在叙述了。

结语

这篇文章作为Sentinel解析系列的最后一篇文章,回到了一切开始的地方,对于入口的组织进行了分析。可能文章写得有点复杂,这里也总结几个关键点给大家:

上一篇 下一篇

猜你喜欢

热点阅读