Sentinel核心概念与核心类详解:一文搞懂所有关键组件

图片[1]-Sentinel核心概念与核心类详解:一文搞懂所有关键组件 - 速优课-速优课

Sentinel核心概念与核心类详解:一文搞懂所有关键组件

前言

上一篇我们讲了Sentinel的滑动窗口原理,了解了它是怎么统计QPS的。但要真正理解Sentinel的工作流程,光知道滑动窗口还不够。

Sentinel里有很多核心概念和类,比如Resource、Node、Context、Entry、ProcessorSlot……刚开始接触的时候,很容易被这些概念绕晕。

这篇文章,我就带大家把Sentinel的核心概念和核心类一次性梳理清楚。搞懂了这些,再看Sentinel的源码就会轻松很多。

一、三个基础概念

先从最基础的三个概念说起:资源、规则、降级

概念说明举例
资源Sentinel保护的对象,可以是一个方法、一段代码、一个接口比如GET:/user/info接口
规则围绕资源设定的规则,包括限流、熔断、系统保护等比如QPS不超过100
降级流量过大时,根据规则有策略地拒绝部分请求超过100 QPS的请求直接拒绝

可以这样理解:资源是保护对象,规则是保护策略,降级是执行结果。

二、资源指标统计相关的类

Sentinel的指标统计是以资源为维度的。先看几个核心类的继承关系:

图片[2]-Sentinel核心概念与核心类详解:一文搞懂所有关键组件 - 速优课-速优课

2.1 ResourceWrapper:资源的抽象表示

ResourceWrapper是对资源的封装,我们可以把它理解为”资源ID”。如果资源是一个接口,那资源名称通常就是接口URL,比如GET:/v1/demo

public abstract class ResourceWrapper {
    protected final String name;          // 资源名称
    protected final EntryType entryType;  // 流量类型
    protected final int resourceType;     // 资源类型
}

三个字段的含义:

字段含义说明
name资源名称比如”GET:/v1/demo”
entryType流量类型IN=流入(接收请求),OUT=流出(发起请求)
resourceType资源类型Web、RPC、网关、数据库等

EntryType:流入还是流出?

EntryType是个枚举,只有两个值:

  • IN:流入流量,也就是接收处理请求
  • OUT:流出流量,也就是向外发起请求

怎么区分?举个例子:

  • 你的服务收到前端的请求 → IN
  • 你的服务调用其他服务的接口 → OUT

为什么要区分?因为熔断降级通常是在消费端统计OUT类型的流量——比如调用第三方接口失败率太高,就熔断不再调用。

资源类型有哪些?

Sentinel支持的资源类型:

public final class ResourceTypeConstants {
    public static final int COMMON = 0;           // 默认
    public static final int COMMON_WEB = 1;       // Web应用接口
    public static final int COMMON_RPC = 2;       // Dubbo等RPC接口
    public static final int COMMON_API_GATEWAY = 3; // API网关
    public static final int COMMON_DB_SQL = 4;    // 数据库SQL
}

2.2 Node:统计节点接口

Node是Sentinel中非常核心的接口,它负责持有实时统计的指标数据,对外屏蔽滑动窗口的存在。

简单说就是:Node封装了所有指标统计的读写方法。

Node接口的方法很多,大致可以分为几类:

方法分类代表方法说明
查询总数totalRequest()、totalPass()、totalSuccess()总请求数、通过数、成功数
查询QPSpassQps()、blockQps()、successQps()各种QPS指标
查询耗时avgRt()、minRt()平均耗时、最小耗时
查询并发curThreadNum()当前占用线程数
记录数据addPassRequest()、addRtAndSuccess()记录通过、成功、耗时
记录异常/拒绝increaseBlockQps()、increaseExceptionQps()记录拒绝和异常
线程计数increaseThreadNum()、decreaseThreadNum()并发线程增减

Node有几个重要的实现类:StatisticNode、DefaultNode、ClusterNode、EntranceNode。我们一个个来讲。

2.3 StatisticNode:最基础的统计节点

StatisticNode,顾名思义就是”统计节点”,是Node接口的基础实现类,真正实现了实时指标数据统计。

public class StatisticNode implements Node {
    // 秒级滑动窗口:2个Bucket,每个500毫秒,总共1秒
    private transient volatile Metric rollingCounterInSecond = new ArrayMetric(2, 1000);
    // 分钟级滑动窗口:60个Bucket,每个1秒,总共1分钟
    private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
    // 当前并发线程数
    private LongAdder curThreadNum = new LongAdder();
}

一个StatisticNode里包含了:

组件作用
秒级滑动窗口统计实时QPS(默认2个500ms的窗口)
分钟级滑动窗口保存最近1分钟的历史数据(60个1秒的窗口)
并发线程计数器统计当前占用的线程数

为什么要两个滑动窗口?因为用途不一样:

  • 秒级:用于实时限流判断,对精度要求高
  • 分钟级:用于Dashboard展示、熔断判断等,需要更长时间的历史数据

比如:

  • 获取当前秒的拒绝QPS → 用秒级窗口
  • 获取前一秒的拒绝QPS → 用分钟级窗口
  • 获取最小耗时 → 用秒级窗口

记录数据的时候,两个窗口都会同时更新:

@Override
public void addRtAndSuccess(long rt, int successCount) {
    // 秒级滑动窗口
    rollingCounterInSecond.addSuccess(successCount);
    rollingCounterInSecond.addRT(rt);
    // 分钟级滑动窗口
    rollingCounterInMinute.addSuccess(successCount);
    rollingCounterInMinute.addRT(rt);
}

并发线程数有什么用?

StatisticNode里的curThreadNum用于统计当前并发占用的线程数,用来实现信号量隔离

  • 接收到请求 → curThreadNum自增1
  • 请求处理完 → curThreadNum自减1

比如Tomcat线程池有200个线程,如果不控制,一个慢接口可能把200个线程都占满,导致其他接口没法处理。通过控制并发线程数(比如最多20个线程处理这个接口),就能避免一个接口拖垮整个应用。

2.4 DefaultNode:资源维度的统计节点

DefaultNode是StatisticNode的子类,它的作用是:将资源ID和StatisticNode绑定在一起,实现以资源为维度的统计。

public class DefaultNode extends StatisticNode {
    private ResourceWrapper id;           // 资源ID
    private volatile Set<Node> childList = new HashSet<>(); // 子节点集合
    private ClusterNode clusterNode;      // 对应的ClusterNode
}

三个字段的含义:

字段含义说明
id资源ID这个Node统计的是哪个资源
childList子节点集合用于构建调用链树
clusterNode集群节点该资源对应的全局统计节点

DefaultNode继承了StatisticNode,所以它本身就具备统计能力。加上资源ID,就变成了”某个资源的统计节点”。

一个资源理论上可能有多个DefaultNode,这取决于是否有多个不同的Context(调用上下文)。不过通常情况下,一个资源在同一个Context下只有一个DefaultNode。

2.5 ClusterNode:全局维度的统计节点

ClusterNode用于统计每个资源全局的指标数据——不区分调用链路,一个资源ID只对应一个ClusterNode。

public class ClusterNode extends StatisticNode {
    private final String name;              // 资源名称
    private final int resourceType;         // 资源类型
    // 按调用来源区分的统计数据
    private Map<String, StatisticNode> originCountMap = new HashMap<>();
    private final ReentrantLock lock = new ReentrantLock();
}

重点是originCountMap这个字段:它维护了每个调用来源的统计数据。

什么是调用来源?比如服务A调用服务B,那服务A就是来源。通过originCountMap,Sentinel可以实现按调用来源限流——比如只限制服务A的调用QPS,不限制服务C的。

2.6 EntranceNode:入口节点

EntranceNode是一个特殊的Node,它继承自DefaultNode,用于维护调用树

public class EntranceNode extends DefaultNode {
    public EntranceNode(ResourceWrapper id, ClusterNode clusterNode) {
        super(id, clusterNode);
    }
}

什么是调用树?从根节点到每个叶子节点,代表不同的请求调用链路。调用链路上经过的每个节点,对应该链路上被Sentinel保护的资源。

举个例子:在Web MVC应用中,Sentinel会统一创建一个名为sentinel_spring_web_context的Context,对应的EntranceNode就是入口。每个接口对应一个DefaultNode,都挂在这个EntranceNode的childList下面。

EntranceNode有个很实用的功能:统计整个应用的总QPS

@Override
public double totalQps() {
    double r = 0;
    // 遍历所有子节点,把QPS加起来
    for (Node node : getChildList()) {
        r += node.totalQps();
    }
    return r;
}

2.7 各类Node的关系总结

用一张图来梳理一下:

图片[3]-Sentinel核心概念与核心类详解:一文搞懂所有关键组件 - 速优课-速优课
  • StatisticNode:基础统计能力,包含秒级和分钟级滑动窗口
  • DefaultNode:资源维度的统计,绑定了资源ID,有子节点列表
  • ClusterNode:全局维度的统计,还支持按来源统计
  • EntranceNode:入口节点,维护调用树,可以统计总QPS

三、Context与Entry:调用链路的上下文

理解了各种Node,接下来要理解Context和Entry。这两个是理解Sentinel工作流程的关键。

3.1 Context:调用链路上下文

Context代表调用链路的上下文,贯穿一次调用链路中的所有Entry。

Context里存了什么?

public class Context {
    private final String name;        // Context名称(入口名称)
    private DefaultNode entranceNode; // 入口节点
    private Entry curEntry;           // 当前Entry
    private String origin = "";       // 调用来源
}
字段含义说明
nameContext名称也就是调用链路入口名称
entranceNode入口节点当前调用树的入口节点(EntranceNode)
curEntry当前Entry当前正在处理的资源对应的Entry
origin调用来源服务消费者的名称或IP

Context是通过ThreadLocal传递的,只在调用链路的入口处创建。也就是说,同一个线程里的多次Sentinel调用,共用同一个Context。

举个实际的例子:

  • 服务B是一个MVC应用,同时用了Sentinel的MVC适配和OpenFeign适配
  • 服务B收到服务A的请求时,MVC拦截器创建了名为sentinel_spring_web_context的Context
  • 服务B调用服务C时,OpenFeign适配器发现当前线程已经有Context了,就直接复用
  • 整个调用链路用的是同一个Context

再打个比方:

  • 路径一:A.a() → B.b() → C.c(),入口是A.a(),Context名称是”a_context”
  • 路径二:D.d() → B.b() → C.c(),入口是D.d(),Context名称是”d_context”

虽然都调用了B.b()和C.c(),但因为入口不同,Context不同,对应的调用链树也是分开的。

3.2 Entry:资源调用的表示

Entry代表一次资源调用。每次调用SphU.entry()都会创建一个Entry。

Entry的抽象类定义:

public abstract class Entry implements AutoCloseable {
    private long createTime;          // 创建时间
    private Node curNode;             // 当前节点(DefaultNode)
    private Node originNode;          // 来源节点
    private Throwable error;          // 异常
    protected ResourceWrapper resourceWrapper; // 资源
}

Entry的具体实现类是CtEntry

class CtEntry extends Entry {
    protected Entry parent = null;          // 父Entry
    protected Entry child = null;           // 子Entry
    protected ProcessorSlot<Object> chain;  // 当前资源的处理器链
    protected Context context;              // 当前上下文
}

这里有个很重要的设计:父子Entry构成双向链表

什么意思?比如在一次请求中,多次调用SphU.entry(),每次都会创建一个CtEntry,这些CtEntry通过parent和child指针连起来。

举个例子:

  1. 服务B收到服务A的请求,创建ctEntry1(parent为空)
  2. 服务B调用服务C,创建ctEntry2(parent=ctEntry1,ctEntry1.child=ctEntry2)
图片[4]-Sentinel核心概念与核心类详解:一文搞懂所有关键组件 - 速优课-速优课

为什么要搞双向链表?因为调用entry.exit()的时候,需要把Context的curEntry还原为上一个Entry。就像栈一样,后进先出。

3.3 ROOT与调用树

Sentinel里有一个全局的根节点:Constants.ROOT,类型是EntranceNode。

整个调用树的结构是这样的:

  • 最顶层是ROOT(全局唯一)
  • ROOT的子节点是各个入口的EntranceNode
  • 每个EntranceNode的子节点是该入口下各个资源的DefaultNode
  • 以此类推,形成一棵树

用一张图来表示:

图片[5]-Sentinel核心概念与核心类详解:一文搞懂所有关键组件 - 速优课-速优课

三者的关系:

  • Constants.ROOT:全局根节点,所有EntranceNode都挂在它下面
  • Context.entranceNode:当前调用链路的入口节点
  • Entry.curNode:当前访问的资源节点

四、ProcessorSlot:功能插槽

ProcessorSlot直译是”处理器插槽”,是Sentinel实现各种降级功能的切入点。

Sentinel用了责任链模式,每个ProcessorSlot负责一项功能,多个Slot串成一条链,请求依次经过每个Slot的处理。

ProcessorSlot大致可以分为两类:

4.1 辅助统计的ProcessorSlot

这类Slot主要负责构建节点、统计数据:

Slot名称作用
NodeSelectorSlot为当前资源创建DefaultNode,构建调用链树
ClusterBuilderSlot创建ClusterNode,以及按调用来源的StatisticNode
StatisticSlot最重要的统计Slot,调用后续Slot后根据结果更新统计数据

4.2 实现降级功能的ProcessorSlot

这类Slot是真正实现降级功能的:

Slot名称功能
AuthoritySlot黑白名单限流
SystemSlot系统自适应限流
FlowSlot限流降级
DegradeSlot熔断降级

关于每个ProcessorSlot的具体实现,后面的文章会详细分析。

总结与思考

这篇文章内容比较多,我们来梳理一下整体脉络:

资源相关:

  • ResourceWrapper:资源的抽象表示(名称、类型、流入流出)

统计节点相关(Node体系):

  • StatisticNode:基础统计,秒级+分钟级滑动窗口
  • DefaultNode:资源维度的统计,有子节点
  • ClusterNode:全局统计,支持按来源统计
  • EntranceNode:入口节点,可以统计总QPS

调用上下文相关:

  • Context:调用链路上下文,ThreadLocal传递
  • Entry/CtEntry:资源调用表示,父子Entry构成双向链表
  • ROOT:全局根节点,维护调用树

功能插槽相关:

  • NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot:辅助统计
  • AuthoritySlot、SystemSlot、FlowSlot、DegradeSlot:实现降级功能

几个关键理解点:

  1. 为什么需要这么多种Node? 因为统计的维度不一样。有资源维度、全局维度、调用链路维度、按来源维度。不同维度满足不同的需求。
  2. Context和Entry的关系? Context是整个调用链路的上下文,Entry是链路上每个资源的表示。一个Context里可以有多个Entry,形成双向链表。
  3. 调用树有什么用? 调用树清晰地展示了资源之间的调用关系,可以实现按链路限流、统计入口总QPS等功能。

理解了这些核心概念,下一篇我们就可以来看Sentinel的责任链模式和整体工作流程了。

© 版权声明
THE END
喜欢就支持一下吧
点赞5
相关推荐
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!