目录

 

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方法情况

结果如下图