流控神器-Sentinel-NodeSelectorSlot

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_33330687/article/details/86567955

目录

 

1. 概述

2. 一些需要知道的前提

2.1. Resource

2.2. Context

2.3. Entry

2.4. Node

3. 深入分析

3.1. demo启动

3.2. 创建Context

3.3. 创建Entry

3.4. 执行NodeSelectorSlot.entry

3.4.1. 相同资源名情况下

3.4.2. 不同资源名情况下


1. 概述

之前分析过Sentinel是基于责任链的模式,其逻辑处理部分是一个有一个的Slot

这里大概的介绍下每种Slot的功能职责:

  • NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;

  • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;

  • StatisticsSlot 则用于记录,统计不同维度的 runtime 信息;

  • SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;

  • AuthoritySlot 则根据黑白名单,来做黑白名单控制;

  • FlowSlot 则用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流;

  • DegradeSlot 则通过统计信息,以及预设的规则,来做熔断降级;

每个Slot执行完业务逻辑处理后,会调用fireEntry()方法,该方法将会触发下一个节点的entry方法,下一个节点又会调用他的fireEntry,以此类推直到最后一个Slot,由此就形成了sentinel的责任链。

2. 一些需要知道的前提

2.1. Resource

资源,是Sentinel世界中的抽象,任何东西都能被定义成资源,自己提供的服务,调用的服务,甚至一段代码,有了资源才能在资源上定义规则,去进行限流降级之类的操作

在Sentinel中提供了两个默认是Resource分别是StringResourceWrapper和MethodResourceWrapper

2.2. Context

Context是上下文的意思,一个线程对应一个Context

其中有三个属性

  • name:名字
  • entranceNode:调用链入口
  • curEntry:当前entry
  • origin:调用者来源
  • async:异步

2.3. Entry

每次调用 SphU.entry() 都会生成一个Entry入口,该入口中会保存了以下数据:入口的创建时间,当前入口所关联的节点(Node),当前入口所关联的调用源对应的节点。Entry是一个抽象类,他只有一个实现类,在CtSph中的一个静态类:CtEntry

其中有这些属性

  • createtime
  • curNode
  • originNode
  • error
  • resourceWrapper
  • parent
  • child
  • chain
  • context

红色的是抽象类Entry的,黑色的是CtEntry中的

一个Entry相当于一个token只有正常生成了一个entry才能算pass,不然报异常BlockException肯定是限流了

2.4. Node

节点是用来保存某个资源的各种实时统计信息的,他是一个接口,通过访问节点,就可以获取到对应资源的实时状态,以此为依据进行限流和降级操作。

有几种节点类型

  • StatisticNode:统计节点
  • DefaultNode:默认节点,NodeSelectorSlot中创建的就是这个节点
  • ClusterNode:集群节点
  • EntranceNode:该节点表示一棵调用链树的入口节点,通过他可以获取调用链树中所有的子节点

3. 深入分析

3.1. demo启动

Entry entry = null;

try {

    entry = SphU.entry("abc");

    entry = SphU.entry("abc");

} catch (BlockException e1) {

     

} finally {

    if (entry != null) {

        entry.exit();

    }

}

CtSph.entryWithPriority

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)

    throws BlockException {

    //先从ThreadLocal获取,第一次肯定是null

    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) {

        //生成Context的部分

        context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());

    }

    //省略

     

}

3.2. 创建Context

ContextUtil.trueEnter

protected static Context trueEnter(String name, String origin) {

    //从ThreadLocal中获取,第一次肯定是null

    Context context = contextHolder.get();

    if (context == null) {

        //这里是根据Context的名字获取Node

        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;

        DefaultNode node = localCacheNameMap.get(name);

        if (node == null) {

            if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {

                setNullContext();

                return NULL_CONTEXT;

            } else {

                try {

                    LOCK.lock();

                    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中

                            Map<String, DefaultNode> newMap = new HashMap<String, DefaultNode>(

                                contextNameNodeMap.size() + 1);

                            newMap.putAll(contextNameNodeMap);

                            newMap.put(name, node);

                            contextNameNodeMap = newMap;

                        }

                    }

                } finally {

                    LOCK.unlock();

                }

            }

        }

        context = new Context(node, name);

        context.setOrigin(origin);

        //放入ThreadLocal中

        contextHolder.set(context);

    }



    return context;

}

这里的逻辑还是比较简单的

  1. 首先在ThreadLocal获取,获取不到就创建,不然就返回
  2. 然后再Map中根据ContextName找一个Node
  3. 没有找到Node就加锁的方式,创建一个EntranceNode,然后放入Map中
  4. 创建Context,设置node,name,origin,再放入ThreadLocal中

到此Context就创建完成

目前Context对象的状态如下图

 

3.3. 创建Entry

新建Entry的过程

Entry e = new CtEntry(resourceWrapper, chain, context);
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {

    super(resourceWrapper);

    this.chain = chain;

    this.context = context;



    setUpEntryFor(context);

}

private void setUpEntryFor(Context context) {

    // The entry should not be associated to NullContext.

    if (context instanceof NullContext) {

        return;

    }

    this.parent = context.getCurEntry();

    if (parent != null) {

        ((CtEntry)parent).child = this;

    }

    context.setCurEntry(this);

}

当第一次Entry生成的时候,context.getCurEntry必定是NULL,那么直接执行Context.setCurEntry方法

然后这个Context的状态如下图

再执行一次新的Sphu.entry后会再次新建一个Entry,这个时候curEntry不是null,那么执行((CtEntry)parent).child = this;

结果如下图

可以看出,原来的CtEntry被移出Context,新建的CtEntry和旧CtEntry通过内部的parent和child引用相连

3.4. 执行NodeSelectorSlot.entry

之前分析过,Sentinel是基于责任链模式,责任链第一个slot是NodeSelectorSlot

public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)

    throws Throwable {

    //这里有个缓存,根据context的名字缓存node

    DefaultNode node = map.get(context.getName());

    //双重检测,线程安全

    if (node == null) {

        synchronized (this) {

            node = map.get(context.getName());

            if (node == null) {

                //这里生成的是DefaultNode节点

                node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);

                //下面这些逻辑是放入map的逻辑,因为后期map比较大,所以这样放入,性能会高一些

                HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());

                cacheMap.putAll(map);

                cacheMap.put(context.getName(), node);

                map = cacheMap;

            }

            // 关键在这,这是修改调用链树的地方

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

        }

    }

    //替换context中的curEntry中的curNode

    context.setCurNode(node);

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

}

// Context.java

public Context setCurNode(Node node) {

    this.curEntry.setCurNode(node);

    return this;

}

public Node getLastNode() {

    if (curEntry != null && curEntry.getLastNode() != null) {

        return curEntry.getLastNode();

    } else {

        return entranceNode;

    }

}

//CteEntry.java

public Node getLastNode() {

    return parent == null ? null : parent.getCurNode();

}

//DefaultNode.java

public void addChild(Node node) {

    if (node == null) {

        RecordLog.warn("Trying to add null child to node <{0}>, ignored", id.getName());

        return;

    }

    if (!childList.contains(node)) {

        synchronized (this) {

            if (!childList.contains(node)) {

                Set<Node> newSet = new HashSet<Node>(childList.size() + 1);

                newSet.addAll(childList);

                newSet.add(node);

                childList = newSet;

            }

        }

        RecordLog.info("Add child <{0}> to node <{1}>", ((DefaultNode)node).id.getName(), id.getName());

    }

}


查询缓存中是否有这个node这里的逻辑也很简单

  • 根据ContextName查询缓存是否有这个Node
  • 没有就生成这个DefaultNode,放入缓存,然后构造调用树链
  • Context的curEntry中的curnode设置为这个node

但是这样有个情况是第二步不是经常出现的,第二步出现的前提是node取不到,而node在缓存中获取不到的条件是contextName不同,除非不同线程才有可能不同contextName,不仅如此,还有非NodeSelectorSlot同对象,那么其中的map是不同的

3.4.1. 相同资源名情况下

假设我们先回到现有一个Entry生成的的情况下

然后执行NodeSelectorSlot中的entry方法

这个时候curEntry是等于CtEntry,但是CtEntry中的parent是null,所以getLastNode还是返回entranceNode

然后再执行下面方法setCurNode

结果如下图

注:这两个Node是相同的Node,是一个对象

然后再执行一次生成Entry和一次NodeSelectorSlot中的entry方法

这一次Context还是相同的Context,因为再一个线程中,那么就不会再生成新的Node,只执行上述过程的第一步和第三步

结果如下图

注:这三个Node都是相同的Node,因为根据ContextName从缓存中获取的

3.4.2. 不同资源名情况下

Entry entry = null;

try {

    entry = SphU.entry("abc");  //资源名abc

    System.out.println("pass");

    entry = SphU.entry("abcd");  //资源名abcd

    System.out.println("pass");

} catch (BlockException e1) {

    System.out.println("block");

} finally {

    if (entry != null) {

        entry.exit();

    }

}

为什么不同资源名会不同?

本质在于ChainSlot的生成问题

ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);





ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {

    //可以看出,chain链根据资源来作为key,不同的资源肯定是不同chain链

    ProcessorSlotChain chain = chainMap.get(resourceWrapper);

    if (chain == null) {

        synchronized (LOCK) {

            chain = chainMap.get(resourceWrapper);

            if (chain == null) {

                // Entry size limit.

                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;

}

然后在NodeSelectorSlot中

private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

这是个私有属性,不同资源情况下,默认的map中是没有缓存的

下面开始分析

先回到第一个Entry生成并执行了NodeSelectorSlot的entry方法情况

然后在执行一次Entry生成并执行了NodeSelectorSlot的entry方法情况

结果如下图

猜你喜欢

转载自blog.csdn.net/qq_33330687/article/details/86567955
今日推荐