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

前言

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

概念解释

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

  • Resource
    资源是整个Sentinel最基本的一个概念。可以是一段代码,一个http请求,一个微服务,总而言之,他是Sentinel需要保证的实体。大部分情况下,我们可以使用方法签名,URL或者是服务名称来作为资源的名称。它在Sentinel中的体现是:ResourceWrapper,他有两个子类:
    1. StringResourceWrapper 使用string来标识一个资源
    2. MethodResouceWrapper 使用一个函数签名来标识一个资源
  • Node
    节点是用来存储统计数据的基本数据单元,Node本身只是一个接口,它有多个实现:
    1. StatisticNode 唯一的直接实现类,实现了流量统计的基本方法,在StatisticSlot中使用(具体参见前文)
    2. ClusterNode 继承自StatisticNode,对于某一个资源的全局统计
    3. DefaultNode 继承自StatisticNode, 对于某一个资源在相应上下文中的实现,保存了一个指向ClusterNode的引用。另外还保存了子节点列表,当在同一个context下多次调用SphU.entry时会创建子节点
    4. EntranceNode 继承自DefaultNode,代表一个调用的根节点,一个Context会对应到一个EntranceNode
  • Context
    上下文是用来保存当前调用的元数据,它包含了几个信息:
    1. EntranceNode 整个调用树的根节点,即入口
    2. Entry 当前的调用点
    3. Node 关联到当前调用点的统计信息
    4. Origin 通常用来标识调用方,这在我们需要按照调用方来区分流控策略的时候会非常有用

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

  • Entry
    每次SphU.entry()调用都会返回一个Entry,Entry保持了所有关于当前资源调用的信息:

    1. createTime 这个资源调用的创建时间
    2. currentNode SphU.entry请求进入的资源在当前上下文中的统计数据Node
    3. originNode SphU.entry请求进入的资源对于特定origin调用方的统计数据node

    Entry的实现类为CtEntry,它其中除了上述信息之外,还保存了额外的信息:

    1. parent 调用树链条中上一个entry
    2. child 调用树链条中的下一个entry
    3. chain 当前调用资源所使用的限流工作责任链,包括各个Slot
    4. context 当前调用点所属的上下文

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

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

代码解析

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

  • entranceNode作为上下文的入口,每个context name对应全局一个entranceNode
  • context是线程相关的,就算context name一样,但是不同线程的context是不同的(虽然他们关联的entranceNode是一样)
  • defaultNode对于同一个resource的不同context name有不同的统计实例,但非线程相关
  • clusterNode是一个resource的全局统计
  • 同一个resource全局共享同一个slot chain
    希望大家喜欢
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,249评论 4 56
  • 小凤和小峰既不是兄妹,也不是姐弟,而是一个女生的两个不同的名字。一般作业本上女孩写的是小峰,而学籍注册上我看到的却...
    wsx书女阅读 256评论 0 1
  • 我是雷小花,是个护士,平日在医院里见到无数的悲欢离合,人情冷暖,今天的故事,主角是一位因贪恋美味而亡的姑娘…… 清...
    丛铭阅读 7,590评论 22 19
  • 许多年前朋友对我说的那句,她就好比是干柴,而你那一刻刚好是烈火,所以你们互相点燃,但是你要好好想一想,公司那么多人...
    芬芬vstar阅读 325评论 6 3
  • 今天,阳光时有时无。 是2016年最后待在学校里的一天了。心情也是阴晴不定,最终,三天假期里如国庆般的作业量,给这...
    盐不够吃阅读 154评论 0 1