作积极的人,越努力越幸运!
本节将详细介绍 Sentienl 的上下文环境管理机制。java
咱们从 sentinel-apache-dubbo-adapter 模块的 SentinelDubboProviderFilter 的实现中不难看出,在其入口处会首先调用 ContextUtil.enter(resourceName, application) 。那咱们就从该方法开始来探究上下文环境管理机制。node
说到 Sentinel 的调用上下文环境,那调用上下文环境中会保存哪些信息呢?咱们先来看看 Context。面试
Context
其核心属性与核心方法以下:redis
CtEntry
同步调用调用信息封装对象。数据库
对应的核心方法将在下文具体用到时再详细介绍。apache
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:首先咱们来看一下其参数:网络
在进入 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<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 中,会定义一个固定根节点,其定义以下:
其资源名称为:machine-root。addChild 方法就是将节点添加到以下数据结构中:
public static void exit() { Context context = contextHolder.get(); if (context != null && context.getCurEntry() == null) { contextHolder.set(null); } }
退出当前上下文环境,这里有一个条件就是当前的上下文环境的当前调用节点已经退出,不然没法移除,故使用建议:ContextUtil . exit 必定要在持有的 Entry 退出以后再调用。
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 处理链的第一个节点。
从该类的注释能够得出以下的结论:该类的做用是构建一颗虚拟调用树,咱们接下来以一个Dubbo调用示例来讲明。
正如上图所示:应用 A 向应用 order-servie 服务发起一个 RPC 服务,下订单,order-service 应用引入了 sentinel-apache-dubbo-adapter 相关依懒,会执行 SentinelDubboProviderFilter 过滤器,调用 Sentinel 相关的方法,对资源进行保护,而后下单服务中,首先会操做数据库,将本次数据库操做定义为资源:insertOrderSQL,而后再操做 redis,redis 的操做命名为资源 setRedisOp。其对应在内存中会生成以下调用链的结构图。
那上面这个调用链保存在线程上下文环境中,即 ThreadLocal 中。在 Sentinel 中使用 Node 来表示一个一个调用节点,其中 EntranceNode 表示调用链的入口,DefaultNode 表示普通节点,ClusterNode 表示集群节点,即同一个资源会统计整个集群中的信息。
从该类的注释咱们能够得出上述的结论,接下来咱们从源码的角度对其进行分析与理解。
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:咱们先来看看其参数:
代码@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 的 名称呢?这样设计的目的是什么,能有什么好处?
欢迎加入个人知识星球,一块儿交流源码,探讨架构,打造高质量的技术交流圈,长按以下二维码
中间件兴趣圈 知识星球 正在对以下话题展开如火如荼的讨论:
一、【让天下没有难学的Netty-网络通道篇】一、Netty4 Channel概述(已发表)二、Netty4 ChannelHandler概述(已发表)三、Netty4事件处理传播机制(已发表)四、Netty4服务端启动流程(已发表)五、Netty4 NIO 客户端启动流程六、Netty4 NIO线程模型分析七、Netty4编码器、解码器实现原理八、Netty4 读事件处理流程九、Netty4 写事件处理流程十、Netty4 NIO Channel其余方法详解二、Java 并发框架(JUC) 探讨【面试神器】三、源码分析Alibaba Sentienl 专栏背后的写做与学习技巧。