Sentinel之Slots插槽源码分析(一)

一、概述

前面介绍过Sentinel核心框架就是通过插槽链一层层的调用,每个插槽的功能如下:

  • NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结-构存储起来,用于根据调用路径来限流降级。
  • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据。
  • StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息。
  • LogSlot 则用于记录blockException信息的日志信息,会写入的日志文件中。
  • ParamFlowSlot 则用于根据热点参数进行限流控制的。
  • SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量。
  • AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制。
  • FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制。
  • DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级。

这边文章会先介绍NodeSelectorSlot和ClusterBuilderSlot功能

二、节点Node

我们知道在进行资源的限流降级中,需要拿到Entry,Entry就像是一个凭证,拿到这个凭证,代码才能继续走下去。Entry又被封装到上下文对象Context中。在Context中还有一个entranceNode节点,代表这个资源调用树一个入口。Entry中又有curNode、originNode这两个属性。curNode保存了这一次调用统计信息,riginNode保存了在这个上下文中,所有的调用的统计信息和。

有这么多Node,那他们之间的关系是怎样的,如图:

Node
  • Node是一个接口,有很多各种指标的方法,Sentinel就用通过这些指标进行限流降级的。
  • StatisticNode:统计实时统计指标的Node。有两个子类DefaultNode和ClusterNode。
  • EntranceNode:EntranceNode是每个上下文的入口,该节点是挂在root下的,是全局唯一的,每一个context都会对应一个entranceNode。
  • DefaultNode:DefaultNode是记录当前调用实时数据的,在同一个上下文中不同资源关联着不同的DefaultNode。在同一个上下文中,对不同的资源调用,DefaultNode会有子childNode生成。
  • ClusterNode:全局的统计数据,包括 rt, thread count, qps等,相同的资源关DefaultNode联着统一个ClusterNode,无论它在哪个上下文中。

三、NodeSelectorSlot

Sentinel之Entry构建源码解析 这篇文章介绍过,Entry可以理解Context这棵树的树干,那么NodeSelectorSlot这个插槽可以理解为正式构建entry叶子而形成的,下面具体分析。

    //注意这里的可以使context的name
    private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

  @Override
  public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
        throws Throwable {
        DefaultNode node = map.get(context.getName());
        if (node == null) {
            synchronized (this) {
                node = map.get(context.getName());
                if (node == null) {
                    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);
            }
        }

        context.setCurNode(node);
  }

分析

  • 1、从map中获取改context下的defaultNode,如果defaultNode不存在,到2,反之直接到4。
  • 2、双重锁校验defaultNode存不存,若不存在,则创建一个defaultNode,并重新构造map,并添加到map中,然后到3。
  • 3、获取context的当前调用Node的LastNode,并把该node添加到子Node中。
  • 4、重新设置Context的curEntry的curNode。

看图分析
在一个Context中第一次进入时,在第三步,调用Context的getLastNode方法。

//在Context中
public Node getLastNode() {
      //curEntry.getLastNode() 调用CtEntry的方法getLastNode
        if (curEntry != null && curEntry.getLastNode() != null) {
            return curEntry.getLastNode();
        } else {
            return entranceNode;
        }
}

在CtEntry中

 @Override
    public Node getLastNode() {
        return parent == null ? null : parent.getCurNode();
    }

上篇文章分析过Entry的构建过程,第一次调用时:

Entry

可以发现curEntry != null条件满足,但是parent是null。所以,lastNode第一次的值就是context的entranceNode,然后将node添加到entranceNode中,并设置curEntry的curNode节点。然后Context内存结构变成如下。

defaultNode

接着又一个请求进入,若资源不同,在生成一个新的Entry后(上一篇分析过)。
这个时候再次调用context.getLastNode()将会返回parent.getCurNode(),把这个节点放入到lastNode的子节点中,再把node设置给context的curEntry。最后Context内存结构变成如下。

defaultNode

这里假设的是资源名不同的情况,若是资源相同的话,则context.getLastNode()).addChild(node);逻辑就不会执行,只会把当前node设置为context的curEntry中。这时的内存结构如图。

defaultNode

上面分析了在NodeSelectorSlot中CurEntry叶子构建的过程,在一次构建中会设置entranceNode的childNode。

资源路径收集

经过上述分析可以发现,NodeSelectorSlot主要负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级。

 ContextUtil.enter("entrance1", "appA");
 Entry nodeA = SphU.entry("nodeA");
 if (nodeA != null) {
    nodeA.exit();
 }
 ContextUtil.exit();

上述代码通过 ContextUtil.enter() 创建了一个名为 entrance1 的上下文,同时指定调用发起者为 appA;接着通过 SphU.entry()请求一个 token,如果该方法顺利执行没有抛 BlockException,表明 token 请求成功。

以上代码将在内存中生成以下结构:

              machine-root
                 /     
                /
         EntranceNode1
              /
             /   
      DefaultNode(nodeA

注意:每个 DefaultNode 由资源 ID 和输入名称来标识。换句话说,一个资源 ID 可以有多个不同入口的 DefaultNode。

 ContextUtil.enter("entrance1", "appA");
  Entry nodeA = SphU.entry("nodeA");
  if (nodeA != null) {
    nodeA.exit();
  }
  ContextUtil.exit();

  ContextUtil.enter("entrance2", "appA");
  nodeA = SphU.entry("nodeA");
  if (nodeA != null) {
    nodeA.exit();
  }
  ContextUtil.exit();

以上代码将在内存中生成以下结构:

                   machine-root
                    /       \
                   /         \
             Entrance1     Entrance2
                /             \
               /               \
      DefaultNode(nodeA)   DefaultNode(nodeA)
               |                    |
      +- - - - - - - - - - +- - - - - - -> ClusterNode(nodeA);

可以发现同一个nodeA资源共用一个ClusterNode,而不管它在哪一个上线文中。接下面讲解ClusterBuilderSlot,来看ClusterNode构造。

四、ClusterBuilderSlot

ClusterBuilderSlot插槽用于构建资源的 ClusterNode 以及调用来源节点。ClusterNode 保持资源运行统计信息(响应时间、QPS、block 数目、线程数、异常数等)以及原始调用者统计信息列表。来源调用者的名字由 Context.enter(contextName,origin) 中的 origin 标记。

看源码:

public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap
        = new HashMap<ResourceWrapper, ClusterNode>();

    private static final Object lock = new Object();

    private ClusterNode clusterNode = null;

    @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.
                    clusterNode = Env.nodeBuilder.buildClusterNode();
                    HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<ResourceWrapper, ClusterNode>(16);
                    newMap.putAll(clusterNodeMap);
                    newMap.put(node.getId(), clusterNode);

                    clusterNodeMap = newMap;
                }
            }
        }
        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())) {
            Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
            context.getCurEntry().setOriginNode(originNode);
        }

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

//以下代码省略
}

细心地人可以发现ClusterBuilderSlot集成的AbstractLinkedProcessorSlot的泛型对象由Object变为DefaultNode了,这是因为DefaultNode已经在NodeSelectorSlot插槽中构建好了。

来看ClusterBuilderSlot插槽的entry方法做了哪些事。

  • 1、判断clusterNode是否为空,若为空,到2,反之到3。
  • 2、创建一个clusterNode节点,并添加到clusterNodeMap中,注意这里的map的可以使ResourceWrapper,用以表示不区分在哪个上下文中,到3。
  • 3、设置clusterNode到defaultNode中,到4。
  • 4、如果context的origin不为空,则把originNode设置到Context当前cutEntry的originNode中。这个originNode用于后续根据调用源进行限流或资源保护。

五、我的小结

1、本文是插槽分析的第一篇,介绍的NodeSelectorSlot和ClusterBuilderSlot的作用。
2、NodeSelectorSlot用来构建Context的curEntry的叶子节点,不同的资源id在不同的上下文中有不同的入口,并且对应不同的defaultNode,但同一个资源对应同一个clusterNode。
3、ClusterBuilderSlot设置了defaultNode的clusterNode,并设置了Entry的originNode。clusterNode保存clusterNodeMap中,可以发现系统运行的时间越长,这个map就越稳定。
4、NodeSelectorSlot和ClusterBuilderSlot是整个插槽链中的前两个插槽,这个两个插槽完成Context数据的封装和对资源调用链Node包装,以便对后续数据的收集和资源的保护限流。


以上内容,若有不当住处,请指正

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

推荐阅读更多精彩内容