【Spring Cloud Alibaba】Sentinel运行原理

文章目录

  • 前言
  • 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对象中。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DoIP学习笔记系列:导航篇

文章目录 1. 前言2. 导航3. 参考资料 1. 前言 DoIP学习笔记系列是一整套基于网络的诊断协议学习笔记&#xff0c;非常适合对有UDS基础但对DoIP没有实战经验的小伙伴参考&#xff0c;通过源协议讲解&#xff0c;企标讲解&#xff0c;测试需求讲解&#xff0c;测试用例讲解&…

STM32CubeMX配置STM32G031多通道ADC采集(HAL库开发)

时钟配置HSI主频配置64M 勾选打开8个通道的ADC 使能连续转换模式 配置好串口&#xff0c;选择异步模式配置好需要的开发环境并获取代码 修改main.c 串口重定向 #include "stdio.h" int fputc(int ch, FILE *f) {HAL_UART_Transmit(&huart1, (uint8_t *)&ch…

Shell脚本学习-read命令

Shell变量可以直接赋值或者脚本传参的方式&#xff0c;还可以使用echo命令从标准输入中获得&#xff0c;read为bash内置命令。 [rootvm1 ~]# type echo echo is a shell builtin常用参数&#xff1a; -p prompt&#xff1a;设置提示信息&#xff0c;我们看help内容的信息&…

开发中遇到的 cookie 问题

1. cookie 无法跨域携带问题 尽管已经登录&#xff0c;但是请求接口返回状态码&#xff1a;202&#xff0c;msg&#xff1a; 未登录&#xff0c;如下图所示&#xff1b; 1.1 XMLHttpRequest.withCredentials未设置 如果需要跨域 AJAX 请求发送 Cookie&#xff0c;需要withCre…

【C++】STL---list基本用法介绍

个人主页&#xff1a;平行线也会相交&#x1f4aa; 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 平行线也会相交 原创 收录于专栏【C之路】&#x1f48c; 本专栏旨在记录C的学习路线&#xff0c;望对大家有所帮助&#x1f647;‍ 希望我们一起努力、成长&…

基于YOLOv8开发构建蝴蝶目标检测识别系统

在前面的一篇博文中已经很详细地描述了如何基于YOLOv8开发构建自己的个性化目标检测模型&#xff0c;感兴趣的话可以看下&#xff1a; 《基于YOLOv8开发构建目标检测模型超详细教程【以焊缝质量检测数据场景为例】》 本文的主要目的就是基于YOLOv8来开发构建细粒度的蝴蝶目标…

Python深度学习“四大名著”之一【赠书活动|第二期《Python机器学习:基于PyTorch和Scikit-Learn》】

近年来&#xff0c;机器学习方法凭借其理解海量数据和自主决策的能力&#xff0c;已在医疗保健、 机器人、生物学、物理学、大众消费和互联网服务等行业得到了广泛的应用。自从AlexNet模型在2012年ImageNet大赛被提出以来&#xff0c;机器学习和深度学习迅猛发展&#xff0c;取…

Centos报错:[Errno 12] Cannot allocate memory

执行一个脚本刚开始正常&#xff0c;后面就报[Errno 12] Cannot allocate memory 如果内存不足&#xff0c;可能需要增加交换内存。或者可能根本没有启用交换。可以通过以下方式检查您的交换: sudo swapon -s如果它为空&#xff0c;则表示您没有启用任何交换。添加 1GB 交换…

客户方数据库服务器CPU负载高优化案例

客户方数据库服务器CPU负载高优化案例 背景 上周线上服务出现一个问题&#xff0c;打开某个页面&#xff0c;会导致其它接口请求响应超时&#xff0c;排查后发现数据库响应超400s&#xff0c;之前1s就可查到数据。 具体原因是有个大屏统计页面&#xff0c;会实时查看各业务服…

pve安装ikuai并设置,同时把pve的网络连接到ikuai虚拟机

目录 前因 前置条件 安装ikuai 进入ikuai的后台 配置lan口&#xff0c;以及wan口 配置lan口桥接 按实际情况来设置了 单拨&#xff08;PPOE拨号&#xff09; 多拨(内外网设置点击基于物理网卡的混合模式) 后续步骤 pve连接虚拟机ikuai的网络以及其他虚拟机连接ikuai的网…

3秒快速打开 jupyter notebook

利用 bat 脚本&#xff0c;实现一键打开 minconda 特点&#xff1a; 1、可指定 python 环境 2、可指定 jupyter 目录 一、配置环境 minconda 可以搭建不同的 python 环境&#xff0c;所以我们需要找到 minconda 安装目录&#xff0c;把对应目录添加到电脑环境 PATH 中&#…

环境搭建-Ubuntu20.04.6系统TensorFlow BenchMark的GPU测试

1. 下载Ubuntu20.04.6镜像 登录阿里云官方镜像站&#xff1a;阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区 2. 测试环境 Server OS&#xff1a;Ubuntu 20.04.6 LTS Kernel: Linux 5.4.0-155-generic x86-64 Docker Version&#xff1a;24.0.5, build ced0996 docker-com…

NFT市场泡沫破裂了吗?投资NFT是否仍然安全?

近期&#xff0c;NFT市场的价格出现了明显的下跌趋势&#xff0c;许多人开始担心NFT市场是否已经进入了泡沫破裂的阶段。但是&#xff0c;我们需要认真分析这个问题&#xff0c;并且探讨投资NFT是否仍然安全。 NFT&#xff08;Non-Fungible Token&#xff09;是一种非同质化代币…

在家构建您的迷你 ChatGPT

这篇文章分为三个部分&#xff1b;他们是&#xff1a; 什么是指令遵循模型&#xff1f;如何查找遵循模型的指令构建一个简单的聊天机器人废话不多说直接开始吧&#xff01;&#xff01;&#xff01; 什么是指令遵循模型&#xff1f; 语言模型是机器学习模型&#xff0c;可以根…

如何使用GPT作为SQL查询引擎的自然语言

​生成的AI输出并不总是可靠的&#xff0c;但是下面我会讲述如何改进你的代码和查询的方法&#xff0c;以及防止发送敏感数据的方法。与大多数生成式AI一样&#xff0c;OpenAI的API的结果仍然不完美&#xff0c;这意味着我们不能完全信任它们。幸运的是&#xff0c;现在我们可以…

Android11 相机拍照权限,以及解决resolveActivity返回null

一、配置拍照和读写权限 <uses-permission android:name"android.permission.CAMERA"/> <uses-feature android:name"android.hardware.camera" /><uses-permission android:name"android.permission.WRITE_EXTERNAL_STORAGE"/&…

CAN转EtherNet/IP网关can协议是什么意思

你是否曾经遇到过不同的总线协议难以互相通信的问题&#xff1f;远创智控的YC-EIP-CAN网关为你解决了这个烦恼&#xff01; 远创智控YC-EIP-CAN通讯网关是一款自主研发的设备&#xff0c;它能够将各种CAN总线和ETHERNET/IP网络连接起来&#xff0c;解决不同总线协议之间的通信…

【Ajax】笔记-使用fetch函数发送AJAX请求

fetch()函数说明与使用方法详解 fetch()是XMLHttpRequest的升级版,用于在JavaScript脚本里面发出 HTTP请求,本文章向大家介绍fetch()的用法,主要包括fetch()的用法使用实例、应用技巧、基本知识点总结和需要注意事项&#xff0c;具有一定的参考价值&#xff0c;需要的朋友可以参…

IDE /完整分析C4819编译错误的本质原因

文章目录 概述基本概念代码页标识符字符集和字符编码方案源字符集和执行字符集 编译器使用的字符集VS字符集配置 有何作用编译器 - 源字符集编译器 -执行字符集 Qt Creator下配置MSVC编译器参数动态库DLL字符集配置不同于可执行程序EXE总结 概述 本文将从根本原因上来分析和解…

Vuex的使用

1. 是什么&#xff1a; vuex 是一个 vue 的 状态管理工具 &#xff0c;状态就是数据。 大白话&#xff1a;vuex 是一个插件&#xff0c;可以帮我们 管理 vue 通用的数据 (多组件共享的数据) 2. 场景&#xff1a; ① 某个状态 在 很多个组件 来使用 (个人信息) ② 多个组…