Sentinel 调用上下文环境实现原理(含原理图)

点击上方 “中间件兴趣圈” 选择 “设为星标”

做积极的人,越努力越幸运!

本节将详细介绍 Sentienl 的上下文环境管理机制。

1、Sentinel Context 调用上下文环境管理

我们从  sentinel-apache-dubbo-adapter 模块的 SentinelDubboProviderFilter 的实现中不难看出,在其入口处会首先调用 ContextUtil.enter(resourceName, application) 。那我们就从该方法开始来探究上下文环境管理机制。

说到 Sentinel 的调用上下文环境,那调用上下文环境中会保存哪些信息呢?我们先来看看 Context。

1.1 Context 详解

Context 类图如下:

  • Context

    其核心属性与核心方法如下:

    • String name

      Sentinel 调用上下文环境的名称。

    • DefaultNode entranceNode

      调用链的入口节点信息。

    • Entry curEntry

      调用链中当前节点的信息。

    • boolean async

      是否是异步调用上下文环境。

    • Entry

      保存当前的调用信息,其主要核心属性:

    • private long createTime

      资源调用的时间戳。

    • private Node curNode

      该资源所对应的实时采集信息。

    • protected ResourceWrapper resourceWrapper

      资源对象。

  • CtEntry

    同步调用调用信息封装对象。

  • AsyncEntry

    异步调用调用信息的封装对象。

对应的核心方法将在下文具体用到时再详细介绍。

1.2 创建调用上下文环境

ContextUtil#enter

public static Context enter(String name, String origin) {  // @1
    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);   // @2
}

代码@1:首先我们来看一下其参数:

  • String name

    上下文环境 Context 的名称。

  • String origin

    该参数的含义在介绍集群限流时会详细介绍,从 dubbo 模块的适配来看,通常该值会传入当前应用的 application 名称。

代码@2:通过调用内部的 trueEnter 方法。

在进入 trueEnter 方法之前,我们先来看一下 ContextUtil 中两个最核心的属性:

首先使用 ThreadLocal 对象来存储线程上下文环境对象 Context。Map contextNameNodeMap ,其键为 context 的名称,用来缓存其对应的 EntranceNode 。

ContextUtil#trueEnter

protected static Context trueEnter(String name, String origin) {
    Context context = contextHolder.get();   // @1 
    if (context == null) {
    Map localCacheNameMap = contextNameNodeMap;
        DefaultNode node = localCacheNameMap.get(name);   // @2
        if (node == null) {
        if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {   // @3
                     setNullContext();
                       return NULL_CONTEXT;
                } else {
                    try {
                            LOCK.lock();
                            node = contextNameNodeMap.get(name);   // @4
                            if (node == null) {
                                if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {  
                                        setNullContext();
                                        return NULL_CONTEXT;
                                } else {
                                        node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);  // @5
                                        // Add entrance node.
                                        Constant.ROOT.addChild(node);                                                                                     // @6
                        Map newMap = new HashMap(contextNameNodeMap.size() + 1);
                                        newMap.putAll(contextNameNodeMap);
                                        newMap.put(name, node);
                                        contextNameNodeMap = newMap;
                                }
                            }
                    } finally {
                            LOCK.unlock();
                       }
            }
        }
        context = new Context(node, name);    // @7
        context.setOrigin(origin);
        contextHolder.set(context);    // @8
   }
  return context;
}

代码@1:从 threadLocal 中获取 Context 对象,线程首次获取时为空。

代码@2:根据 context 的名称尝试从缓存中去找对应的 Node,通常是 EntranceNode。即用来表示入口的节点Node 为 EntranceNode。

代码@3:如果 localCacheNameMap 已缓存的对象容量默认超过2000,则不纳入 Sentinel 限流,熔断等机制中来,即一个应用,默认不能定义 2000个 资源统计入口,以 一个 Dubbo 服务为例,一个 Dubbo 服务应用,如果超过2000个服务,则超过的部分不会应用 Sentinel 限流与熔断机制。

代码@4:锁应用的经典场景,dubbo check。

代码@5:为该 context name 创建一个对应的 EntranceNode。

代码@6:将创建的 EntranceNode 加入到根节点的子节点中,稍后重点讨论一下。

代码@7:创建 Context 对象,将 Context 对象中的入口节点设置为 新创建的 EntranceNode。

代码@8:将新创建的 Context 对象存入当前线程本地环境变量中(ThreadLocal)。

接下来先来探讨代码@6 Constants.ROOT.addChild(node)。

在 Sentinel 中,会定义一个固定根节点,其定义如下:

其资源名称为:machine-root。addChild 方法就是将节点添加到如下数据结构中:

1.3 移除调用上下文环境

public static void exit() {
    Context context = contextHolder.get();
    if (context != null && context.getCurEntry() == null) {
        contextHolder.set(null);
    }
}

退出当前上下文环境,这里有一个条件就是当前的上下文环境的当前调用节点已经退出,否则无法移除,故使用建议:ContextUtil . exit 一定要在持有的 Entry 退出之后再调用。

1.4 异步上下文环境切换

public static void runOnContext(Context context, Runnable f) {
    Context curContext = replaceContext(context);  // @1
    try {
        f.run();  // @2
    } finally {
        replaceContext(curContext);  // @3
    }
}

这里是异步调用上下文环境切换的实现原理,我们知道存在 ThreadLocal 中的数据是无法跨线程访问的,故一个线程中启动另外一个线程,上下文环境是无法直接被传递的,Sentinel 的思想是为先创建的线程再创建一个 Context,在运行子线程时,调用 runOnContext 来切换上下文环境。

Context 就介绍到这里了,我们接下来再来看一个与上下文环境管理密切相关的 Sentinel Slot 处理器:NodeSelectorSlot,通常也是 Sentinel Slot 处理链的第一个节点。

2、NodeSelectorSlot

2.1 NodeSelectorSlot 调用链概述

从该类的注释可以得出如下的结论:该类的作用是构建一颗虚拟调用树,我们接下来以一个Dubbo调用示例来说明。

正如上图所示:应用 A 向应用 order-servie 服务发起一个 RPC 服务,下订单,order-service 应用引入了 sentinel-apache-dubbo-adapter 相关依懒,会执行 SentinelDubboProviderFilter 过滤器,调用 Sentinel 相关的方法,对资源进行保护,然后下单服务中,首先会操作数据库,将本次数据库操作定义为资源:insertOrderSQL,然后再操作 redis,redis 的操作命名为资源 setRedisOp。其对应在内存中会生成如下调用链的结构图。

那上面这个调用链保存在线程上下文环境中,即 ThreadLocal 中。在 Sentinel 中使用 Node 来表示一个一个调用节点,其中 EntranceNode  表示调用链的入口,DefaultNode 表示普通节点,ClusterNode 表示集群节点,即同一个资源会统计整个集群中的信息。

从该类的注释我们可以得出上述的结论,接下来我们从源码的角度对其进行分析与理解。

2.2 源码分析 NodeSelectorSlot

NodeSelectorSlot 中只声明了一个唯一的成员变量,其声明如下:

private volatile Map map = new HashMap(10);

定义一个 Map,其键为上下文环境 Context 的名称,通常是进入节点的名称,例如上面提到的 EntranceNode( dubbo:provider:com.a.b.OrderService:saveOrder(java.lang.String))。

注意:一个 NodeSelectorSlot 对象会被多个线程使用,其共享的维度为资源,即多个线程进入同一个资源保护的代码时,执行的是同一个 NodeSelectorSlot 对象。详细实现请参考上文中 CtSph # lookProcessChain 部分详解。

接下来重点看一下 NodeSelectorSlot 的核心方法 entry。

NodeSelectorSlot#entry

public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) // @1
        throws Throwable {
    DefaultNode node = map.get(context.getName());   // @2
    if (node == null) {                                                       // @3
        synchronized (this) {                                          // @4
            node = map.get(context.getName());
                if (node == null) {
            node = new DefaultNode(resourceWrapper, null);    // @5
                          HashMap cacheMap = new HashMap(map.size());
                    cacheMap.putAll(map);
                    cacheMap.put(context.getName(), node);
                    map = cacheMap;
                       // Build invocation tree
                    ((DefaultNode) context.getLastNode()).addChild(node);   // @6
              }
            }
    }
    context.setCurNode(node);                                                                  // @7
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

代码@1:我们先来看看其参数:

  • Context context

    调用上下文环境,该对象存储在 ThreadLocal,其名称在调用链的入口处设置。

  • ResourceWrapper resourceWrapper

    资源的包装类,注意留意其 equals 与 hashCode 方法,判断两个对象是否相等的依据是资源名

    称是否相同。

  • Object obj

    参数。

  • int count

    本次需要消耗的令牌数量。

  • boolean prioritized

    请求是否按优先级排列。

  • Object… args

    额外参数。

代码@2:如果缓存中存在对应该上下文环境的节点,则直接使用,并将其节点设置当前调用上下文的当前节点中(Context)。

代码@3:如果节点为空,则进入到节点创建流程,此过程需要加锁,见代码@4。

代码@5:创建一个新的 DefaultNode 。

代码@6:构建调用链,由于 NodeSelectorSlot 是第一个进入的处理器,故此时 Context 的 curEntry 为 null ,故这里就是创建与的上下文环境名称对应的节点会被添加到 ContextUtil 的 entry 创建的调用链入口节点(EntranceNode),然后顺便更新 Context 中的 Entry curEntry 属性,即再次验证了上面的图。

我们来总结一下 NodeSelectorSlot 作用:从官方的注释来看:构建一条调用链,更直接一点就是设置 Context 的 curEntry 属性。

关于 Sentinel 调用上下文环境实现原理就介绍到这里了。

如果您喜欢这篇文章,点【在看】与转发是一种美德,期待您的认可与鼓励,越努力越幸运。

思考题:首先在这里先“剧透”一下,Node 在 Sentinel 中的作用是持有资源的实时统计信息,将在下一篇文章介绍 StatisticSlot 时详细介绍。NodeSelectorSlot 中的  Map 中的键为什么是 Context 的 名称呢?这样设计的目的是什么,能有什么好处?

欢迎加入我的知识星球,一起交流源码,探讨架构 ,打造高质量的技术交流圈, 长按如下二维码

中间件兴趣圈 知识星球 正在对如下话题展开如火如荼的讨论:

1、【让天下没有难学的Netty-网络通道篇】

1、Netty4 Channel概述( 已发表

2、Netty4 ChannelHandler概述( 已发表

3、Netty4事件处理传播机制( 已发表

4、Netty4服务端启动流程( 已发表

5、Netty4 NIO 客户端启动流程

6、Netty4 NIO线程模型分析

7、Netty4编码器、解码器实现原理

8、Netty4 读事件处理流程

9、Netty4 写事件处理流程

10、Netty4 NIO Channel其他方法详解

2、Java 并发框架(JUC) 探讨【 面试神器

3、源码分析Alibaba Sentienl 专栏背后的

写作与学习技巧