likes
comments
collection
share

Sentinel源码4-NodeSelectorSlot和ClusterBuilderSlot

作者站长头像
站长
· 阅读数 26

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

我们在Sentinel源码3-入口类和SlotChain构建过程 一文中介绍了slot链的构建过程(创建),并分析了其源码,需要注意的是,在sentinel中,一个资源对应着一个slot链,毫不夸张的说sentinel的核心其实就是这个slot链,一看到这个链,我们更应该想到的是责任链设计模式,sentinel其实就是个拦截器链,比如一个请求过来,经过层层slot拦截,全部满足要求就继续执行,不通过就抛出异常,从本篇开始我们就按照调用顺序深入剖析下它的各个slot,看看每个slot都是干什么的。

1. NodeSelectorSlot

第一个slot 是 NodeSelectorSlot开始,这个NodeSelectorSlot从命名上就能猜出来,做node选择的slot。看slot的源码,我们要时刻记着,一个资源对应一个slot链。这里我们先看下~NodeSelectorSlot·它的成员变量

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

就是个map,存储着资源与node的对应关系

1.1 entry方法

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
    throws Throwable {
    
    // 一个context name 对应一个defaultNode 对象
    // 从缓存中获取DefaultNode
    DefaultNode node = map.get(context.getName());
    // DCL
    if (node == null) {
        synchronized (this) {
            node = map.get(context.getName());
            if (node == null) {
                // 创建一个DefaultNode,并放入缓存map
                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
                // 将新建的node添加到调用树中
                ((DefaultNode) context.getLastNode()).addChild(node);
            }

        }
    }

    // 设置当前node
    context.setCurNode(node);
    // todo 触发下一个节点
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

其实不关心业务含义的话,看这段代码很简单,无非就是从map中获取DefaultNode对象,如果不存在的话就创建。 但是我们不知道这个context是什么东西,它的name属性又是什么东西,有经验的程序员同学,一看到这个context就知道是个上下文,而且一般配合着ThreadLocal来使用,代码中倒数第二句把node设置到了这个context中,这个node就会跟着这个context一直往下传,但是我们知道这些是远远不够的,这时候我们就要结合之前的一些代码来看了:查看中4.3案例:

ContextUtil.enter(target, origin);
entry = SphU.entry(target, EntryType.IN);

这里解释下targetorigintarget的话就是当前资源,这里也就是你请求路径,origin,这个是上游的请求路径,在微服务中,上游服务调用下游服务,sentinel会通过一些手段将上游的资源信息带到下游服务中,这个不用太关心。 先看下这个ContextUtil.enter(target, origin);这段代码

protected static Context trueEnter(String name, String origin) {
    // 尝试从ThreadLocal中获取context
    Context context = contextHolder.get();
    // 若ThreadLocal中没有,则尝试从缓存map中获取
    if (context == null) {
        // 缓存map的key为context名称,value为EntranceNode
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        // DCL 双重检测锁,防止并发创建对象
        DefaultNode node = localCacheNameMap.get(name);
        if (node == null) {
            // 若缓存map的size 大于 context数量的最大阈值,则直接返回NULL_CONTEXT
            if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                setNullContext();
                return NULL_CONTEXT;
            } else {
                LOCK.lock();
                try {

                    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.
                            // 将新建的node添加到Root
                            Constants.ROOT.addChild(node);

                            // 将新建的node写入到缓存map
                            // 为了防止"迭代稳定性问题"-iterate stable 对于共享集合的写操作
                            Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
                            newMap.putAll(contextNameNodeMap);
                            newMap.put(name, node);
                            contextNameNodeMap = newMap;
                        }
                    }
                } finally {
                    LOCK.unlock();
                }
            }
        }
        // 将context的name与entranceNode 封装成context
        context = new Context(node, name);
        // 初始化context的来源
        context.setOrigin(origin);
        // 将context写入到ThreadLocal
        contextHolder.set(context);
    }

别看很长,难度不大这段代码,首先是从contextHolder获取这个context,这个contextHolder就是个ThreadLocal ThreadLocal<Context> contextHolder = new ThreadLocal<>(); 这里想想,如果是项目刚启动,还没有请求过来,肯定是没有的,这个时候就会继续往下走,从一个缓存map中获取DefaultNode ,这个也是没有的,接着就是判断map的大小,不能超过2000,超过了就返回一个null context,不超过的话就会创建EntranceNode 对象,将这个资源传入构造,同时将新创建的EntranceNode 对象挂在ROOT 节点上,缓存到这个map中。这个ROOT节点其实也是个EntranceNode,不过它的资源名字是machine-root。接着往下看就是创建context了,然后将创建的context放到ThreadLocal中。

好了,到这里ContextUtil.enter(target, origin);这句代码就分析完成了。

接着分析下entry = SphU.entry(target, EntryType.IN);

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
        throws BlockException {
        // 获取某个线程对应的context
        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);
        }
        // 如果是null的话
        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);
        }
        ///创建 processor 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
            chain.entry(context, resourceWrapper, null, count, prioritized, 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;
    }
  • 先获取context,上面我们创建了context,这里直接获取就OK了。

  • 接着构建slot链,这里我们就不看了,之前都介绍过。

  • 接着创建CtEntry对象,这里没啥好看的

  • 最后就是进入slot链执行了。

到这里我们的context来龙去脉都能解释清楚了,但是node之间还是很乱,我们来画下关系 Sentinel源码4-NodeSelectorSlot和ClusterBuilderSlot

这个正好就是NodeSelectorSlot 类上面注释画的那个关系。

  • ROOT(machine-root) 是顶级的节点,也就是根节点
  • EntranceNodeOne是一个线程节点,属于context,同理EntranceNodeTwo
  • 而NodeSelectorSlot 里面维护的那个map,是相同资源不同context DefaultNode

1.2 exit

@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
    // 没干啥操作,接着往后调用
    fireExit(context, resourceWrapper, count, args);
}

1.3 总结

本文是第一个slot NodeSelectorSlot的源码解析,也是稍微简单的,代码不难,但是这里面Node的关系还是需要慢慢的体会

2. ClusterBuilderSlot

在第1小节中我们分析了第一个slot NodeSelectorSlot的源码,这个slot主要是为相同资源不同的线程创建node,缓存没有创建,然后塞到当前线程中。本小节分析的ClusterBuilderSlot 从名字上看也能知道是与集群有关的,它主要是为某个资源创建集群node的,也就是每个资源就一个集群node,代码也很简单,我们看下。

首先看下ClusterBuilderSlot的成员

private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();//资源 ---》clusterNode

private static final Object lock = new Object();

private volatile ClusterNode clusterNode = null;// clusterNode

可以看到这个clusterNodeMap 其实就是个缓存 资源与 ClusterNode 对应关系的,一个资源对应着一个ClusterNode 对象。lock就是个锁,这个下面会用到,一个资源一把锁。clusterNode这个是对象成员,一个资源对应一个clusterNode。

2.1 entry方法

  @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.

                    // 创建 cluster node
                    clusterNode = new ClusterNode();
                    HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
                    newMap.putAll(clusterNodeMap);

                    ///node.getId() 这个其实是resource
                    newMap.put(node.getId(), clusterNode);

                    clusterNodeMap = newMap;
                }
            }
        }

        /// 设置clusterNode
        node.setClusterNode(clusterNode);

        /*
         * if context origin is set, we should get or create a new {@link Node} of
         * the specific origin.
         * 此上下文的起源(通常表示不同的调用者,例如服务使用者名称或起源IP)。
         */
        if (!"".equals(context.getOrigin())) {

            //创建originNode  然后在当前entry 甚至originNode
            Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
            context.getCurEntry().setOriginNode(originNode);
        }

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

整体步骤如下:

  • 就是这个资源对应的clusterNode不存在的话,就创建一个ClusterNode实例,然后放到map中缓存
  • 再就是设置到往当前node中,这个node是上个NodeSelectorSlot 创建的那个。
  • 再就是从context取出origin,这个origin得解释下,其实就是上层资源,比如说微服务中,上层服务调用下层服务,sentinel会通过一些手段将上层服务资源传到下层服务,然后就能获取到这个origin,如果我们这个服务不是上层服务的话,这个origin一般会有的,这里就是获取origin的node,他也有个map专门缓存的,如果没有就创建。- 最后将这个origin node塞到当前调用entry中。
  • 最后就是调用下一个slot。

2.2 exit方法

这个与NodeSelectorSlot 一样,啥事没干

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

2.3 总结

这个ClusterBuilderSlot 还是很简单,就是为资源创建一个clusternode ,然后塞到当前调用node中,同时将origin node 塞到当前调用entry中。其实从ClusterBuilderSlotNodeSelectorSlot 中我们会发现,开始它就是准备各个维度统计使用的node,这些维度还是需要我们自己品的。

参考文章

Sentinel1.8.5源码github地址(注释) Sentinel源码解析 Sentinel官网

转载自:https://juejin.cn/post/7148980285764272135
评论
请登录