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

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

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


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


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


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


1.1 Context 详解


Context 类图如下:

a7ef01ead8f7becc1ebfacf9f5cbb812.jpg

  • 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 中两个最核心的属性:

ba132c1a83d5e4f0f2b32cbf1e09e21d.png

首先使用 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<String, DefaultNode> 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<String, DefaultNode> 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 中,会定义一个固定根节点,其定义如下:

2a2b9bfeac2f82d5106b4062c39f16c3.png

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

24d72373dd351fa52902065da5ebfbf7.png


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调用示例来说明。

48386f1a0cbe2ce301405e27ba894cb5.jpg

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

b8a92abf36f77250c6d3cf031c9fcf69.jpg

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


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


2.2 源码分析 NodeSelectorSlot


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

private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(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<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);   // @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 调用上下文环境实现原理就介绍到这里了。



相关文章
|
2月前
|
负载均衡 算法 Java
蚂蚁面试:Nacos、Sentinel了解吗?Springcloud 核心底层原理,你知道多少?
40岁老架构师尼恩分享了关于SpringCloud核心组件的底层原理,特别是针对蚂蚁集团面试中常见的面试题进行了详细解析。内容涵盖了Nacos注册中心的AP/CP模式、Distro和Raft分布式协议、Sentinel的高可用组件、负载均衡组件的实现原理等。尼恩强调了系统化学习的重要性,推荐了《尼恩Java面试宝典PDF》等资料,帮助读者更好地准备面试,提高技术实力,最终实现“offer自由”。更多技术资料和指导,可关注公众号【技术自由圈】获取。
蚂蚁面试:Nacos、Sentinel了解吗?Springcloud 核心底层原理,你知道多少?
|
2月前
|
运维 监控 算法
聊一聊Sentinel背后的原理
本文介绍了Sentinel的核心原理,包括流量控制、熔断降级、系统负载保护、实时监控和统计、与多种微服务框架的集成能力以及扩展性,强调了Sentinel在保障分布式系统稳定性方面的重要性。
147 0
|
4月前
|
运维 监控 NoSQL
【Redis】哨兵(Sentinel)原理与实战全解~炒鸡简单啊
Redis 的哨兵模式(Sentinel)是一种用于实现高可用性的机制。它通过监控主节点和从节点,并在主节点故障时自动进行切换,确保集群持续提供服务。哨兵模式包括主节点、从节点和哨兵实例,具备监控、通知、自动故障转移等功能,能显著提高系统的稳定性和可靠性。本文详细介绍了哨兵模式的组成、功能、工作机制以及其优势和局限性,并提供了单实例的安装和配置步骤,包括系统优化、安装、配置、启停管理和性能监控等。此外,还介绍了如何配置主从复制和哨兵,确保在故障时能够自动切换并恢复服务。
|
6月前
|
监控 Java 应用服务中间件
Sentinel原理及实践
Sentinel原理及实践
125 1
|
7月前
|
监控 NoSQL 程序员
Redis 高可用篇:你管这叫 Sentinel 哨兵集群原理
Redis 高可用篇:你管这叫 Sentinel 哨兵集群原理
136 5
|
7月前
|
监控 算法 Java
sentinel 服务限流工作原理
sentinel 服务限流工作原理
|
7月前
|
监控 BI Sentinel
深入理解Sentinel系列-2.Sentinel原理及核心源码分析(下)
深入理解Sentinel系列-2.Sentinel原理及核心源码分析
131 0
|
7月前
|
存储 监控 测试技术
深入理解Sentinel系列-2.Sentinel原理及核心源码分析(上)
深入理解Sentinel系列-2.Sentinel原理及核心源码分析
515 0
|
算法 Java BI
Sentinel为什么这么强,我忍不住扒了扒背后的实现原理
大家好,我是三友~~ 最近我在整理代码仓库的时候突然发现了被尘封了接近两年之久的Sentinel源码库 两年前我出于好奇心扒了一下Sentinel的源码,但是由于Sentinel本身源码并不复杂,在简单扒了扒之后几乎就再没扒过了 那么既然现在又让我看到了,所以我准备再来好好地扒一扒,然后顺带写篇文章来总结一下。
Sentinel为什么这么强,我忍不住扒了扒背后的实现原理
|
算法 Java Sentinel
sentinel架构底层原理剖析详解
sentinel架构底层原理剖析详解
201 0