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

Stella981
• 阅读 443

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

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

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

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

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


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

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

1.1 Context 详解

Context 类图如下:

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

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

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

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

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

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

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

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

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

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

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

那上面这个调用链保存在线程上下文环境中,即 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 调用上下文环境实现原理就介绍到这里了。

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

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


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

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

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

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 专栏背后的写作与学习技巧。

本文分享自微信公众号 - 中间件兴趣圈(dingwpmz_zjj)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Docker 部署SpringBoot项目不香吗?
  公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。  !(http://dingyue.ws.126.net/2020/0920/b00fbfc7j00qgy5xy002kd200qo00hsg00it00cj.jpg)  2
Stella981 Stella981
2年前
Sentinel 集群限流设计原理
点击上方“中间件兴趣圈”,选择“设为星标”做积极的人,越努力越幸运!!(https://oscimg.oschina.net/oscnet/7366d60332fba1cdd61fffdbfe50fbb2f75.png)(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fmp
Stella981 Stella981
2年前
Sentinel 动态数据源架构设计理念与改造实践
点击上方“中间件兴趣圈”,选择“设为星标”做积极的人,越努力越幸运!!(https://oscimg.oschina.net/oscnet/d4efa6c24ed1a56d0edc2e015106825c9ed.png)(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fmp
Stella981 Stella981
2年前
Sentinel 系统自适应限流原理剖析与实战指导
点击上方“中间件兴趣圈”,选择“设为星标”做积极的人,越努力越幸运!!(https://oscimg.oschina.net/oscnet/71eb753c0197b1ddde74497a579296873ec.png)(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fmp
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这