【Spring Cloud Alibaba】Sentinel运行原理

news2024/12/27 17:42:18

文章目录

  • 前言
  • 1、基本原理
  • 2、SphU.entry()
    • 2.1、StringResourceWrapper
    • 2.2、Entry
  • 3、entry.exit()
  • 4、Context

前言

本文基于sentinel-1.8.0版本

Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。

sentinel整体设计的很精巧,只需要一个sentinel-core便可以运行,它提供了诸如服务降级、黑白名单校验、QPS、线程数、系统负载、CPU负载、流控等功能,可谓是功能非常的强大。

大家都知道sentinel使用SphU或者SphO标示一个被保护的资源,比如:

Entry entry = SphU.entry(“HelloWorld”, EntryType.IN);

上述代码标示了一个名为HelloWorld的被保护资源,并且检查入口流量(SystemSlot只对入口流量生效)。在这行代码之后,便可以访问被保护的资源了,那么SphU.entry()内部究竟做了什么?访问资源结束后,还要执行entry.exit(),那么entry.exit()又做了什么?本文接下来详细分析SphU.entry()和entry.exit()方法的执行原理。

1、基本原理

sentinel在内部创建了一个责任链,责任链是由一系列ProcessorSlot对象组成的,每个ProcessorSlot对象负责不同的功能,外部请求是否允许访问资源,需要通过责任链的校验,只有校验通过的,才可以访问资源,如果被校验失败,会抛出BlockException异常。

sentinel提供了8个ProcessorSlot的实现类,下面实现类功能介绍:

  1. DegradeSlot:用于服务降级,如果发现服务超时次数或者报错次数超过限制,DegradeSlot将禁止再次访问服务,等待一段时间后,DegradeSlot试探性的放过一个请求,然后根据该请求的处理情况,决定是否再次降级。
  2. AuthoritySlot:黑白名单校验,按照字符串匹配,如果在黑名单,则禁止访问。
  3. ClusterBuilderSlot:构建ClusterNode对象,该对象用于统计访问资源的QPS、线程数、异常、响应时间等,每个资源对应一个ClusterNode对象。
  4. SystemSlot:校验QPS、并发线程数、系统负载、CPU使用率、平均响应时间是否超过限制,使用滑动窗口算法统计上述这些数据。
  5. StatisticSlot:用于从多个维度(入口流量、调用者、当前被访问资源)统计响应时间、并发线程数、处理失败个数、处理成功个数等。
  6. FlowSlot:用于流控,可以根据QPS或者每秒并发线程数控制,当QPS或者并发线程数超过设定值,便会抛出FlowException异常。FlowSlot依赖于StatisticSlot的统计数据。
  7. NodeSelectorSlot:负责收集资源路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级、数据统计。
  8. LogSlot:打印日志。

比如本文开头的例子,当请求要访问HelloWorld资源时,该请求需要顺次经过上述这些slot的检查,同时当访问结束时StatisticSlot里面也记录下HelloWorld资源被访问的统计数据,当后面的请求再次访问该资源时,FlowSlot、DegradeSlot可以使用这些统计数据做检查。
sentinel使用SPI加载这些slot,并根据注解@SpiOrder的属性value对它们排序,value越小优先级越高。在sentinel中,这些slot的顺序是:
在这里插入图片描述

我们也可以添加自定义的slot,只需要实现ProcessorSlot接口,在com.alibaba.csp.sentinel.slotchain.ProcessorSlot文件中添加自定义类的全限定名,然后使用注解@SpiOrder指定顺序即可。
对于每个slot的实现原理在后面的文章做介绍。下面通过代码介绍一下SphU.entry()和entry.exit()内部都做了什么。

2、SphU.entry()

在介绍代码前先介绍两个对象。

2.1、StringResourceWrapper

entry()方法内部首先创建一个StringResourceWrapper对象,该对象表示被保护的资源,资源使用字符串命名,StringResourceWrapper对象有三个参数:

//资源名,也就是entry()方法的第一个入参
protected final String name;
//表示是入口流量(IN)还是出口流量(OUT),
//两个参数的区别在于是否被SystemSlot检查,IN会被检查,OUT不会,默认是OUT
protected final EntryType entryType;
//表示资源类型,sentinel提供了common、web、sql、api等类型,资源类型用于统计使用
protected final int resourceType;

任何一个被保护的资源都被封装成StringResourceWrapper对象,sentinel也是使用该对象识别被保护资源。

2.2、Entry

有了表示资源的对象后,接下来创建Entry对象,这个对象也是SphU.entry()方法的返回值,Entry对象持有资源对象,ProcessorSlot链,sentinel上下文对象Context,通过Entry对象应用程序可以窥探sentinel内部情况。

SphU.entry()通过一系列的调用最终调用到CtSph的entryWithPriority()方法上:

//resourceWrapper:是StringResourceWrapper对象,表示资源
//count:表示令牌数,默认是1,一般一个请求对应一个令牌,也可以指定一个请求对应多个令牌,如果令牌不够,则禁止访问
//prioritized:在FlowSlot里面使用,没找到具体的使用含义,有看懂的小伙伴可以告知一下
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
    throws BlockException {
    //构建上下文对象,上下文对象存储在ThreadLocal中
    Context context = ContextUtil.getContext();
    if (context instanceof NullContext) {
        return new CtEntry(resourceWrapper, null, context);
    }
    //一般的线程第一次访问资源,context都是null,我们也可以在应用程序中使用ContextUtil自己创建Context对象
    if (context == null) {
    	//下面创建了一个名字为sentinel_default_context的Context对象
        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }
	//全局开关,可以使用它来关闭sentinel
    if (!Constants.ON) {
        return new CtEntry(resourceWrapper, null, context);
    }
	//使用SPI构建slot链,每个slot对象都有一个next属性,可以使用该属性指定下一个slot对象
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }
	//创建Entry对象
    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
    	//对该请求,遍历每个slot对象
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
    } catch (BlockException e1) {
        e.exit(count, args);
        throw e1;
    } catch (Throwable e1) {
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}

entryWithPriority()方法首先创建一个Context对象,这个对象将会贯穿整个请求的过程,一些共享数据可以放在这里面,既可以使用上面的代码创建名字为sentinel_default_context的Context对象,也可以在应用程序中创建Context对象,如果在应用程序中创建的话,上面代码就不会再次创建了:

//第一个参数表示Context名字,
//第二个参数表示请求方或者调用方的名字,当需要根据调用方进行控制的时候,第二个参数就会起作用
ContextUtil.enter("HelloWorld", "app");
Entry entry = SphU.entry("HelloWorld", EntryType.IN);

创建完Context对象后,使用SPI构建slot链,之后是创建Entry对象,之后就是遍历slot链以决定是否允许该请求访问资源。

3、entry.exit()

访问完资源后,需要调用entry.exit()以告知sentinel结束访问,sentinel会做一些资源的清理和数据统计工作。
entry.exit()方法最后调用到CtEntry.exitForContext()方法上:

 protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException {
        if (context != null) {
            if (context instanceof NullContext) {
                return;
            }
			//如果Context对象记录的Entry对象不是当前对象,
			//意味着entry.exit()与SphU.entry()不是成对出现的,
			//sentinel要求两者必须成对出现,而且要一一对应,否则抛出异常
			//Context有父子关系,这个在文章后面介绍
            if (context.getCurEntry() != this) {
                String curEntryNameInContext = context.getCurEntry() == null ? null
                    : context.getCurEntry().getResourceWrapper().getName();
                // Clean previous call stack.
                CtEntry e = (CtEntry) context.getCurEntry();
                while (e != null) {
                    e.exit(count, args);
                    e = (CtEntry) e.parent;
                }
                String errorMessage = String.format("The order of entry exit can't be paired with the order of entry"
                        + ", current entry in context: <%s>, but expected: <%s>", curEntryNameInContext,
                    resourceWrapper.getName());
                throw new ErrorEntryFreeException(errorMessage);
            } else {
                //在遍历每个slot的exit方法,每个slot清理和统计数据
                if (chain != null) {
                    chain.exit(context, resourceWrapper, count, args);
                }
                //遍历exitHandlers,相当于回调,一般的DegradeSlot有回调,
                //DegradeSlot根据服务访问状态,决定是否将降级状态由HALF_OPEN变为OPEN
                callExitHandlersAndCleanUp(context);

                //设置为上一级Context对象
                context.setCurEntry(parent);
                if (parent != null) {
                    ((CtEntry) parent).child = null;
                }
                if (parent == null) {
                    // Default context (auto entered) will be exited automatically.
                    if (ContextUtil.isDefaultContext(context)) {
                        ContextUtil.exit();
                    }
                }
				//设置当前对象的this.context = null
                clearEntryContext();
            }
        }
    }

entry.exit()相对比较简单,它按照顺序再次遍历访问每个slot的exit()方法。

4、Context

Context是sentinel中的上下文对象,Context贯穿整个资源的访问过程。Context保存在ThreadLocal中。
创建Context有多种方式,可以像第二小节里面一样,创建一个默认的Context对象,也可以在访问资源前使用ContextUtil创建Context对象:

//name表示Context的名称或者链路入口的名称,origin表示调用来源的名称,默认为空字符串
public static Context enter(String name, String origin);
public static Context enter(String name);

无论是上面两种创建方式还是第二小节里面的创建方式,最终都是调用ContextUtil.trueEnter()方法:

protected static Context trueEnter(String name, String origin) {
   	//contextHolder是ThreadLocal<Context>类型
       Context context = contextHolder.get();
       if (context == null) {
       	//contextNameNodeMap持有系统所有的入口节点
           Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
           DefaultNode node = localCacheNameMap.get(name);
           if (node == null) {
           	//sentinel最大只能支撑2000个入口节点,如果超过2000个,sentinel无法提供对资源的保护
               if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                   setNullContext();
                   return NULL_CONTEXT;
               } else {
                   LOCK.lock();
                   try {
                       node = contextNameNodeMap.get(name);
                       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);
                               //入口节点作为虚拟根节点的子节点
                               Constants.ROOT.addChild(node);
                               Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
                               newMap.putAll(contextNameNodeMap);
                               newMap.put(name, node);
                               contextNameNodeMap = newMap;
                           }
                       }
                   } finally {
                       LOCK.unlock();
                   }
               }
           }
           //创建Context对象,可以看到Context对象与入口节点一一对应
           context = new Context(node, name);
           //设置调用来源
           context.setOrigin(origin);
           contextHolder.set(context);
       }
       return context;
   }

Context对象持有名称和一个入口节点对象,入口节点与对应了线程访问的第一个资源,Context对象对应了线程对资源的一次访问,一个线程对应一个Context对象。而且每个入口节点对象都是虚拟根对象ROOT的子节点,虚拟根对象的定义如下:

//ROOT_ID=machine-root
public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN),
    new ClusterNode(ROOT_ID, ResourceTypeConstants.COMMON));

虚拟根对象的名字为machine-root。总的来说,Context是为了在访问资源的过程中保存共享数据使用的。
下面详细介绍一下sentinel中的访问链路树。
假如使用如下代码访问资源(来源官网):

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

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

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

               machine-root
               /         \
              /           \
      entrance1         entrance2    -------表示入口节点对象EntranceNode
            /               \
           /                 \
   DefaultNode(nodeA)   DefaultNode(nodeB)   ---------内部创建DefaultNode节点
              |               |          
              |               |
    ClusterNode(nodeA)   ClusterNode(nodeB)    ------------记录资源的访问数据

再看下面这个访问方式:

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

上面这个代码创建的访问链路树如下:

              machine-root
               /          
              /            
        entrance1      -------表示入口节点对象EntranceNode
            /               
           /                 
   DefaultNode(nodeA)  ---------内部创建DefaultNode节点,持有一个ClusterNode对象
          /               
         /            
 DefaultNode(nodeB)  ------------记录资源的访问数据,持有一个ClusterNode对象    

每调用一次SphU.entry()方法都会在访问链路树上增加一个子节点,通过这个树可以还原出资源的访问路径。

每访问一个资源,Context对象都使用curEntry属性记录下正在访问资源对应的Entry对象,Entry对象有一个parent属性记录下父Entry,比如上面代码中,nodeB的父Entry是nodeA,Entry还有一个curNode属性,该属性记录了对应的DefaultNode对象。每个DefaultNode对象还有一个ClusterNode类的属性clusterNode,clusterNode的作用是记录被访问的资源的统计数据,比如平均响应时间、总请求数、QPS等,FlowSlot便是依据这些数据来判断是否允许访问资源。

Context可以通过上述这些属性构建出一个完整的资源访问树,并将资源访问数据更新到对应的ClusterNode对象中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/799105.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

fast segment anything

[论文解读]比sam快50倍的通用视觉模型fastsam(Fast Segment Anything) - 知乎MetaAI提出的能够“分割一切”的视觉基础大模型SAM提供了很好的分割效果&#xff0c;为探索视觉大模型提供了一个新的方向。 虽然sam的效果很好&#xff0c;但由于sam的backbone使用了vit&#xff0c…

Nexus上传jar包到私有Maven仓库的方法: 页面Upload 和命令

Upload和mvn deploy命令两种上传方式的应用场景不同&#xff0c;手动上传适用于工具类的jar包&#xff0c;比如项目需要引入的lib文件&#xff0c;这种可以选择手动上传。复杂的、有较多依赖的jar包则需要连同pom文件一起上传&#xff0c;此时需要使用mvn deploy。 登录Nexus&a…

ESP32 官方AT固件编译(从零开始环境搭建到编译完成全过程)

1、下载VMware免费版 https://download3.vmware.com/software/WKST-PLAYER-1702/VMware-player-full-17.0.2-21581411.exe 2、下载Ubuntu &#xff08;ubuntu-22.04.2-desktop-amd64.iso&#xff09;** https://releases.ubuntu.com/jammy/ubuntu-22.04.2-desktop-amd64.iso…

Redis学习2--使用java操作Redis

1、java操作Redis库的比较 Redis有各种语言的客户端可以来操作redis数据库&#xff0c;其中java语言主要有Jedis与lettuce &#xff0c;Spring Data Redis封装了上边两个客户端&#xff0c;优缺点如下&#xff1a; 2、使用Jedis操作Redis Jedis使用的基本步骤&#xff1a; 引…

python_PyQt5开发工具结构基础

写在前面&#xff1a; 考虑已经陆陆续续在平台写了几篇PyQt5开发的小工具&#xff0c;后续还会继续发布新的新工具&#xff0c;这些工具都基于一个基础结构往上构建&#xff0c;这个基础结构是本人自己开发的习惯&#xff0c;在这里把工具的基础结构代码抽取出来&#xff0c;后…

ARM汇编中预定义的寄存器和协处理器名称

一、是什么? 预定义的寄存器和协处理器名称,汇编代码中直接使用就可以. # 二、使用步骤 1.引入库 代码如下(示例): .global _start _start:mov r0,#0x18LDR R3,=0x55555555mov r1,#0x18LDR R1,=0x55555555mov r2,#

HTML:常用标签归纳(用法 + 代码 + 显示效果)

目录 标签span 文本标签div 竖着布局的标签h1~h6 标题标签p 段落标签a 超链接标签href 资源位置target 打开位置 a 锚点标签固定位置 br 换行标签img 图片标签src 属性altwidth 宽度 height 高度 列表标签ul 无序列表ol 有序标签设置表示顺序 table 表格属性border 边框width 宽…

Unity Shader - if 和 keyword 的指令比较

文章目录 环境TestingIf4Sampleunity shaderlab 中的 TestingIf4Sample.shadergraphics analyzer 中的 TestingIf4Sample.glsl TestingKW4Sampleunity shaderlab 中的 TestingKW4Sample.shadergraphics analyzer 中的 TestingKW4Sample.glsl 比较 环境 Unity : 2020.3.37f1 Pi…

gin框架内容(三)--中间件

gin框架内容&#xff08;三&#xff09;--中间件 Gin框架允许开发者在处理请求的过程中&#xff0c;加入用户自己的函数。这个函数就叫中间件&#xff0c;中间件适合处理一些公共的业务逻辑&#xff0c;比如登录认证、权限校验、数据分页、记录日志、耗时统计等 即比如&#x…

虚拟个家用服务器集群(2):PVE 系统安装启动

风无痕 July 26,2023 前言 很多人想建个人博客类的网站&#xff0c;这就需要网站服务器&#xff1b;需要管理手机、电脑中积累的照片&#xff0c;每张照片可都是人生一个片段的记录&#xff0c;需要管理微信中收发的各种文档等等&#xff0c;这就需要一台 NAS 即 Network Att…

【JMeter】JMeter添加插件

目录 一、前言 二、插件管理器 三、推荐插件 1.Custom Thread Groups &#xff08;1&#xff09;Ultmate Thread Group &#xff08;2&#xff09;Stepping Thread Group 2.3 Basic Graph 资料获取方法 一、前言 ​ 在我们的工作中&#xff0c;我们可以利用一些插件来帮…

【机器学习】西瓜书学习心得及课后习题参考答案—第3章线性模型

过了一遍第三章&#xff0c;大致理解了内容&#xff0c;认识了线性回归模型&#xff0c;对数几率回归模型&#xff0c;线性判别分析方法&#xff0c;以及多分类学习&#xff0c;其中有很多数学推理过程以参考他人现有思想为主&#xff0c;没有亲手去推。 术语学习 线性模型 l…

网站构建说明

一、网站中的web构建 HTML 4.01CSS 的使用 &#xff08;样式表&#xff09;XHTMLXML 和 XSLT客户端脚本服务器端脚本通过 SQL 管理数据Web 的未来 HTML 4.01 通过 HTML 4.01&#xff0c;所有的格式化信息可以被移出 HTML 文档&#xff0c;转而放入一个独立的样式表中。 层叠样…

linux实现运行java分包部署

1.打好包之后 找到bin文件夹下的 startup.sh文件 2.cd 进入bin文件夹下 3.执行 sh startup.sh 运行命令 4.如果出现此错误 是Windows和Linux的.sh脚本文件格式不同&#xff0c;如果在脚本中有空行&#xff0c;脚本是在Windows下进行编辑之后上传到linux上去执行的话&#xff0c…

httpclient 小案例(未完待续)

为了调用微信小程序接口&#xff0c;我们可以使用httpclient 导入依赖&#xff1a; java通过编码方式里发http请求&#xff0c;步骤&#xff1a; 创建http client 对象创建http请求对象&#xff08;get或post&#xff09;调用http client excute方法 httpGet请求方法 Test…

Pandas时序数据分析实践—概述

时序数据&#xff0c;作为一种时间上有序的数据形式&#xff0c;无疑是我们日常生活中最常见的数据类型之一。它记录了事件、现象或者过程随时间的变化&#xff0c;是对于许多实际场景的忠实反映。而在众多时序数据的应用领域中&#xff0c;跑步训练记录莫过于是一项令人着迷的…

美容店预约小程序制作教程详解

现在&#xff0c;制作一个专属于美容店的预约小程序不再需要编程经验&#xff0c;通过乔拓云网提供的后台管理系统&#xff0c;你可以轻松地完成整个制作过程。下面&#xff0c;我将为你详细介绍如何DIY一个美容店预约小程序。 首先&#xff0c;登录乔拓云网的后台管理系统&…

高阶C语言|C语言文件管理--输入输出流

文件管理 一、为什么使用文件二、什么是文件2.1程序文件2.2数据文件2.3文件名 三、文件的打开和关闭3.1文件指针3.2文件的打开和关闭 四、文件的顺序读写4.1顺序读写函数介绍4.1.1fgetc4.1.2fputc4.1.3fgets4.1.4fputs4.1.5fscanf4.1.6fprintf4.1.7fread4.1.8fwrite 4.2对比一组…

pg三种插件验证

sr_plan 创建extension, 他会创建保留执行计划的表 创建表并插入数据 开启sr_plan.write_mode, 允许sr_plan收集SQL和执行计划 查看QUERY 1的执行计划 PostgreSQL支持merge join、GroupAggregate(通过INDEX SCAN),所以这个CASE,非常快,并不需要b对所有数据进行聚合。查看…

linux 动态库so相关操作

1. 查看库版本号 一般在文件名上有版本号&#xff0c;若文件名上没有版本号&#xff0c;使用如下命令查看&#xff1a; readelf -d libstdc.so 2. 查看库内函数 a) nm -d libstdc.so | grep 内容 b) objdump -tT libstdc.so | grep 内容 c) readelf -s libstdc.so | grep…