# my-sentinel
**Repository Path**: travelerly/my-sentinel
## Basic Information
- **Project Name**: my-sentinel
- **Description**: sentinel 源码注释
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 2
- **Forks**: 2
- **Created**: 2022-02-24
- **Last Updated**: 2025-03-10
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Sentinel: 分布式系统的流量防卫兵
[官方文档](https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D)
[Awesome Sentinel](./awesome-sentinel.md)
## Sentinel 原理流程图
---
## Sentinel 是什么?
Sentinel 是分布式系统的防御系统。以流量为切入点,通过动态设置的流量控制、服务熔断降级、系统负载保护等多个维度保护服务的稳定性,通过服务降级增强服务被拒后用户的体验。
Sentinel 其实就是一个 AOP,通过 AspectJ 切入要进行限流的接口,为其添加 @Around 环绕通知,并使用 try-catch-finally 包裹起来,具体源码参考上图。
每一个需要限流接口的请求,都要经过 AOP 的增强,先执行一系列流控、熔断规则组成的责任链,然后才执行真正的接口逻辑。责任链的组装使用了原生 SPI 机制,流控规则可以在 Sentinel 控制台配置,配置完毕后会填入 Sentinel 服务端,也就是每一个服务,请求流控接口时,就会触发流控逻辑。
若通过流控规则的校验,则直接执行目标方法;若未通过流控规则的校验,则会抛出异常,然后由指定的回调方法进行处理,给出提示。
### Sentinel 具有以下特征:
- **丰富的应用场景**:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- **完备的实时监控**:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- **广泛的开源生态**:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
- **完善的 SPI 扩展机制**:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
### Sentinel 的主要特性:
### Sentinel 分为两个部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo/Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
---
## Sentinel 基本概念
Sentinel 实现限流、隔离、降级、熔断功能,本质要做的就是两件事:
- 统计数据:统计某个资源的访问数据,例如:QPS、RT 等信息;
- 规则判断:判断限流规则、隔离规则、降级规则、熔断规则是否满足。
### 资源
- 资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。
- 只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来表示资源。
- **资源就是希望被 Sentinel 保护的业务,例如项目中定义的 Controller 方法,就是默认被 Sentinel 保护的资源**
### 规则
围绕资源的实时状态设定的规则,可以包括**流量控制规则**、**熔断降级规则**以及**系统保护规则**。所有规则可以动态实时调整。
---
## Sentinel 功能和设计理念
### 1.流量控制
流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:
流量控制有以下几个角度:
- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
- 运行指标,例如 QPS、线程池、系统负载等;
- 控制的效果,例如直接限流、冷启动、排队等。
> Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
### 2.熔断降级
#### 什么是熔断降级
除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。
Sentinel 和 Hystrix 的原则是一致的: 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。
#### 熔断降级设计理念
在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。
Hystrix 通过[线程池](https://github.com/Netflix/Hystrix/wiki/How-it-Works#benefits-of-thread-pools)的方式,来对依赖(在我们的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本,还需要预先给各个资源做线程池大小的分配。
Sentinel 对这个问题采取了两种手段:
- 通过并发线程数进行限制
> 和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。
- 通过响应时间对资源进行降级
> 除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。
### 3.系统负载保护
Sentinel 同时提供[系统维度的自适应保护能力](https://sentinelguard.io/zh-cn/docs/system-adaptive-protection.html)。防止雪崩,是系统防护中重要的一环。当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。
针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
---
## Sentinel 工作主流程
在 Sentinel 里面,所有的资源都对应一个资源名称以及一个 Entry。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 API 显式创建;每一个 Entry 创建的时候,同时也会创建一系列功能插槽(slot chain)。这些插槽有不同的职责,例如:
- `NodeSelectorSlot` 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
- `ClusterBuilderSlot` 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
- `StatisticSlot` 则用于记录、统计不同纬度的 runtime 指标监控信息;☆☆☆☆☆
- `FlowSlot` 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;☆☆☆☆☆
- `AuthoritySlot` 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
- `DegradeSlot` 则通过统计信息以及预设的规则,来做熔断降级;☆☆☆☆☆
- `SystemSlot` 则通过系统的状态,例如 load1 等,来控制总的入口流量;
Sentinel 将 `ProcessorSlot` 作为 SPI 接口进行扩展(1.7.2 版本以前 `SlotChainBuilder` 作为 SPI),使得 Slot Chain 具备了扩展的能力。您可以自行加入自定义的 slot 并编排 slot 间的顺序,从而可以给 Sentinel 添加自定义的功能。
## Sentinel 的基本使用
### 熔断与限流
1. #### 定义资源
> 资源:可以是任何东西,如引用程序提供的服务、RPC 接口方法、甚至是一段代码。只要通过 Sentinel API 定义的代码,就是资源,就能够被 Sentinel 保护起来。
>
> 大部分情况下,可以使用方法签名、URL、甚至服务名称作为资源名来标识资源。
>
> 资源注解 @SentinelResource,用于定义资源,并提供可选的异常处理和 fallback 配置项。@SentinelResource 注解包含以下属性:
>
> - value:资源名称(不能为空)
> - entryType:entry 类型,可选项(默认为 `EntryType.OUT`)
> - blockHandler/blocakHandlerClass:blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回值类型需要与原方法相匹配,参数类型需要和原方法向匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在用一个类中,若希望使用其它类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必须为 static 函数,否则无法解析。
> - fallback/fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionToIgnore 里面排除掉的异常类型)进行处理。
> - defaultFallback:(1.6.0 版本) 默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑,即可以用于很多服务或方法。默认 fallback 函数可以针对所有类型的异常(除了 exceptionToIgnore 里面排除掉的异常类型)进行处理,若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。
>
> fallback/defaultFallback 函数签名和位置要求:
>
> - 返回值类型必须与原函数返回值类型一致;
> - 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数,用于接收对应的异常;
> - fallback/defaultFallback 函数默认需要和原方法在同一个类中,若希望使用其它类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必须为 static 函数,否则无法解析;
> - exceptionToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。
2. #### 定义规则
> 规则:Sentinel 支持使用以下几种规则:
>
> - 流量控制规则:`FlowRuleManager.loadRules(List rules)`
> - 熔断降级规则:`DegradeRuleManager.loadRules(List rules)`
> - 系统保护规则:`SystemRuleManager.loadRules(List rules)`
> - 来源访问控制规则:`AuthorityRuleManager.loadRules(List rules)`
> - 热点参数规则:`ParamFlowRuleManager.loadRules(List rules)`
3. #### 检验规则是否生效
> Sentinel 的所有规则都可以在内存中动态的查询及修改,修改后立即生效。先把可能需要保护的资源定义好,之后再匹配规则。
>
> 只要有了资源,就可以在任何时候,灵活地定义各种流量控制规则,在编码的时候,只需要考虑这个代码是否需要被保护,如果需要被保护,就将之称为一个资源。
### 流控规则
#### 系统保护规则 SystemRule:
- 系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数,这四个维度监控应用数据,让系统尽可能跑在最大的吞吐量的同时保证系统整体的稳定性
- 系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(`EntryType.IN`),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统保护的目的:
- 保证系统不被拖垮
- 在系统稳定的前提下,保持系统的吞吐量
提供保护的思路是根据硬指标,即系统的负载(load1)来做系统过载保护。当系统负载高于某个阈值,就禁止或减少流量的进入;当 load 开始好转,则恢复流量的进入。
系统保护的目标是在系统不被拖垮的情况下,提高系统的吞吐率,而不是 load 一定要定于某个阈值。
系统保护规则支持四种阈值类型:
- Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超出阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 `maxQps * minRT `计算得出,设定参考值一般是 CPU 核心数的 2.5 倍;
- CPU 使用率:当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0~1.0),比较灵敏;
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值时,即触发系统保护,单位是毫秒;
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值时触发系统保护
- 入口 QPS:当单台机器上所有入口流量的QPS 达到阈值时触发系统保护
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、并发线程和入口 QPS等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时,保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则的参数说明:
- highestSyetemLoad:最大的 load1, -1(不生效)
- avgRt:所有入口流量的平均响应时间, -1(不生效)
- maxThread:入口流量的最大并发数,-1(不生效)
- qps:所有入口资源的 QPS,-1(不生效)
```java
List srules = new ArrayList<>();
SystemRule srule = new SystemRule<>();
srule.setAvgRt(3000);
srules.add(srule);
SystemRuleManager.loadRules(srules);
```
#### 来源访问控制规则 AuthorityRule:
- 很多时候,我们需要根据调用方来限制资源是否通过,可以使用 Sentinel 的访问控制(黑白名单)的功能。黑白名单根据资源的请求来源(origin)限制资源是否通过。若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则只有来源位于黑名单时不通过,其余的通过
黑白名单规则配置项:
- resource:资源名,即限流规则的作用对象
- limitApp:对应的黑名单/白名单,不同 origin 使用逗号分割,例如:appA,appB
- strategy:限制模式,AUTHORITY_WHITE 为白名单模式,默认为白名单模式;AUTHORITY_BLACK 为黑名单模式。
熔断降级规则 :
- 熔断降级对调用链路中不稳定的资源进行熔断降级是保障高可用的重要措施之一
- 由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对资源的调用进行限制,让请求快速失败,避免影响其它的资源而导致级联错误。当前资源被降级后,在接下来的降级时间窗口内,对该资源的调用都自动熔断,默认行为是抛出 DegradeException。
#### 熔断降级规则 DegradeRule:
规则属性:
- resource:资源名,即规则的作用对象;
- grade:熔断策略,支持慢调用比例、异常比例、异常数等策略,模式为慢调用比例;
- cout:慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值;
- timeWindow:熔断时长,单位为秒;
- minRequestAmount:熔断触发的最小强求数,请求数小于该值时,即使异常比例超出阈值也不会熔断;
- slowRatioThreshold:慢调用比例阈值,仅慢调用比例模式有效;
- statIntervalMs:统计时长,单位为毫秒 ms,例如:60 * 1000 代表分钟级
降级策略之平均响应时间 DEGRADE_GREAD_RT:
- 当资源的平均响应时间超出阈值(DegradeRule 中的 count,以毫秒 ms 为单位)之后,资源进入准降级状态,如果接下来的 1s 内持续进入 5 个请求(即 QPS >= 5),它们的 RT 都持续超过了这个阈值,那么在接下来的时间窗口(DegradeRule 中的 timeWindow,以秒 s 为单位)之内,对这个方法的调用都会自动地熔断,即抛出 DegradeException。
- 注意 Sentinel 默认的统计 RT 上限是 4900ms,超出此阈值的都会算作 4900ms,但可以通过启动配置项`-Dcsp.sentinel.statistic.mac.rt=xxx`来自定义配置。
降级策略之异常比例 DEGRADE_GREAD_EXCEPTION_RATIO:
- 当资源的每秒异常数占总通过量的比例超出阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下来的时间窗口(DegradeRule 中的 timeWindow,以秒 s 为单位)之内,对这个方法的调用都会自动地返回。
- 异常比例的阈值范围是 0.0 ~1.0,即 0% ~ 100%
降级策略之异常数 DEGRADE_GREAD_EXCEPTION_COUNT:
- 当资源近 1 分钟的异常数超过阈值之后,会进行熔断;
- 注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后,任可能再进入熔断状态。
#### 流量控制规则 FlowRule:
流量控制,原理是监控引用流量的 QPS 或并发线程数等指标,当达到指定阈值时对流量进行控制,避免系统被瞬间的流量高峰冲垮,保障高可用性。
一条限流规则主要由下面几个因素组成,可以组合这些元素来实现不同的限流效果:
- resource:资源名,即限流规则的作用对象
- count:限流阈值
- grade:限流阈值类型(QPS 或并发线程数)
- limitApp:流控针对的调用来源,若为 default,则不区分调用来源
- strategy:调用关系限流策略,判断的依据是资源本身(模型形式),还是根据其它关联资源,还是根据链路入口。
- controlBehavior:流量控制效果(直接拒绝、Warm Up、匀速排队)
流控的几种 strategy 策略(模式):
- 直接:当达到限流条件时,直接限流
- 关联:当关联资源达到阈值时,就限流自己
- 链路:只记录指定链路上的流量,指定资源从入口资源进来的流量,如果达到阈值,就进行限流,api 级别的限流。
##### 流控策略之关联模式:
- 调用关系包括调用方和被调用方,一个方法可能会调用其它方法,形成一个调用链路的层级关系。Sentinel 通过 NodeSelectorSlot 建立不同资源间的调用关系,并且通过 ClusterBuilderSlot 记录每个资源的实时统计信息了;
- 当两个资源之间具有资源争抢或依赖关系的时候,这两个资源便具有了关联。例如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写的速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量,可使用关联限流来避免具有关联关系的资源之间过渡的争抢。
- 举例来说,read_db 和 write_db 这两个资源分别代表数据库读和写,可以给 read_db 设置限流规则来达到写优先的目的:设置 strategy 为 `RuleConstant.STRATEGY_RELATE`,同时设置 refResource 为 write_db,这样当写库操作过于频繁时,读数据的请求就会被限流。
##### 流控效果之预热(Wram up):
当流量突然增大的时候,通常希望系统从空闲状态到忙碌状态的切换时间稍长一些。即如果系统在此之前长期处于空闲的状态,希望处理请求的数量是缓步增多的,经过预期的时间之后,到达系统处理请求个数的最大值。Warm up(冷启动、预热)模式就是为了实现这个目的的。
默认 coldFactor = 3,即请求 QPS 从 threshold / 3 开始,经过预热时长逐渐升至设定的 QPS 阈值。先在单机阈值 10/3,3 的时候,预热 10s 后,慢慢将阈值升至 20。刚开始请求资源,会出现默认错误,预热时间到了之后,阈值增加,没超过阈值刷新,请求正常
通常冷启动的过程系统允许通过的 QPS 曲线如下所示:
如秒杀系统在开启瞬间,会有很多流量上来,很可能把系统打死,预热方式就是为了保护系统,可慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
##### 流控效果之匀速排队:
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的时间间隔,也即是让请求以均匀的速度通过,对应的算法是**漏桶算法**,阈值类型必须设置为 QPS。
这种方式主要用于处理间隔性突发的流量,例如消息队列。若某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,则希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒就直接拒绝多余的请求。
某瞬时来了大量请求,而如果此时要处理所有请求,则可能会导致系统负载过高,影响稳定性。但其实可能后面几秒之内没有消息投递,若直接把多余的消息丢掉则没有充分利用系统处理消息的能力。Sentinel 的 Rate Limiter 模式能在某一段时间间隔内以匀速的方式处理这样的请求,充分利用系统的处理能力,也就是削峰填谷,保证资源的稳定性。
Sentinel 会以固定的间隔时间让请求通过,以访问资源。当请求到来的时候,如果当前请求距离上一个通过的请求的时间间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果请求的预期通过时间小于规则预设的 timeout 时间,则该请求回等待直到预设时间到来再通过,反之,则马上抛出阻塞异常。
使用 Sentinel 的这种策略,就是使用一个时间段(比如 20s 的时间内)处理某一瞬间产生的大量请求,起到一个削峰填谷的作用,从而充分利用系统的处理能力,例如下图展示的场景,X 轴代表时间,Y 轴代表系统处理的请求:
#### 热点参数规则 ParamFlowRule
热点,即经常访问的数据,例如:
- 商品 ID 为参数,统计一段时间内最长购买的商品 ID,并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID,进行限制,热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
#### 访问控制规则 AuthorityRule
访问控制规则,也即是黑白名单规则
黑白名单根据资源的请求来源(origin)来限制资源是否通过,
- 若配置白名单规则,则只有请求来源位于白名单内时,才可能通过;
- 若配置黑名单规则,则请求来源位于黑名单时,不可通过,其它请求可以通过
> 调用方信息通过 `ContextUtil.enter(resourceName,origin)` 方法中的 origin 参数传入
黑白名单控制规则的主要配置项:
- resource:资源名,即限流规则的作用对象;
- limitApp:对应的黑名单/白名单,不同 origin 用逗号 “,” 分割,例如:appA,appB
- strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式
例如希望控制对资源 test 的访问设置白名单,只有来源为 appA 和 appB 的请求才可通过,则可以配置如下白名单规则
```java
AuthorityRule rule = new AuthorityRule();
rule.setResource("test");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));
```
## Sentinel 工作原理
### Sentinel 的架构图
### Sentinel 核心组件
#### Resource
Resource 是 Sentinel 中重要的一个概念,Sentinel 通过资源来保护具体的业务代码或其它后方服务,Sentinel 把复杂的逻辑给屏蔽掉了,用户只需要为受保护的代码或服务定义一个资源,然后定义规则就可以了,其余功能都交给 Sentinel 处理。并且资源和规则是解耦的,规则甚至可以在运行时动态修改。定义完资源后,就可以通过在程序中的埋点来保护自己的服务了,埋点的方式有两种:
- try - catch 方式:通过 SphU.entry(……),当 catch 到 BlockException 时执行异常处理或 fallback;
- if - else 方式:通过 SphO.entry(……),当返回 false 时执行异常处理或 fallback;
以上这两种方式都是通过硬编码的形式定义资源后进行资源埋点的,对业务代码的侵入太大,从 0.1.1 版本开始,Sentinel 加入了注解的支持,可以通过注解来定义资源,具体的注解为:@SentinelResource,通过注解除了可以定义资源外,还可以指定 blockHandler 和 fallback 方法;
在 Sentinel 中具体表示资源的类是 ResourceWrapper,是一个抽象的包装类,包装了资源的 Name 和 EntryType,其具有两个实现类
- StringResourceWrapper:对字符串的包装
- MethodResourceWrapper:对方法调用的包装
#### ProcessorSlotChain
Sentinel 的核心骨架是 ProcessorSlotChain,这个类基于责任链模式来设计,将不同的功能(限流、降级、系统保护等)封装为一个一个的 Slot,请求进入后逐个执行即可。系统会为每个资源创建一套 SlotChain。SlotChain 其实可以分为两部分:**统计数据构建部分**(statistic)和**判断部分**(rule checking)
责任链中的 SlotChain 分为两大类:
1. **统计数据构建部分**(statistic)
- NodeSelectorSlot:负责构建簇点链路中的节点(DefaultNode),将这些节点形成链路树
- ClusterBuilderSlot:负责构建某个资源的 ClusterNode,ClusterNode 可以保存资源的运行信息以及来源信息(origin 名称),例如响应时间、QPS、block 数量、线程数、异常数等
- StatisticSlot:负责统计实时调用数据,包括运行信息、来源信息等
2. **规则判断部分**(rule checking)
- AuthoritySlot:负责授权规则(来源控制)
- SystemSlot:负责系统保护规则
- ParamFlowSlot:负责热点参数限流规则
- FlowSlot:负责限流规则
- DegradeSlot:负责降级规则
各 Slot 在责任链中的顺序图
#### Context
Context 是对资源操作的上下文,每个资源操作(针对 Resource 进行的 entry / exit)必须属于一个 Context。如果代码中没有指定 Context,则会创建一个 name 为 `sentinel_default_context`的默认 Context。一个 Context 生命周期中可以包含多个资源操作。Context 生命周期中最后一个资源在 exit() 时会清理该 Context,也就意味着这个 Context 生命周期结束了。
- Context 代表调用链路上下文,贯穿一次调用链路中的所有资源(Entry)。Context 维持着入口节点(entranceNode)、本次调用链路的 curNode、调用来源(origin)等信息;
- Context 维持的方式:通过 ThreadLocal 传递,只有在入口 enter 的时候生效。由于 Context 是通过 ThreadLocal 传递的,因此对于异步调用链路,线程切换的时候会丢掉 Context,因此需要手动通过 `ContextUtil.runOnContext(context, f)` 来变换 context;
- 后续的 Slot 都可以通过 Context 拿到 DefaultNode 或者 ClusterNode,从而获取统计数据,完成规则判断;
- Context 初始化的过程中,会创建 EntranceNode、contextName 就是 EntranceNode 的名称
Context 的主要属性如下:
```java
public class Context{
// context 名字,默认名字:“sentinel_default_context”
private final String name;
// context 入口节点,每个 context 必须有一个 entranceNode
private DefaultNode entranceNode;
// context 当前 entry,context 生命周期中可能有多个 Entry,所有 curEntry 会有变化
private Entry curEntry;
private String origin = "";
private final boolean async;
}
```
注意:一个 Context 生命周期内只能初始化一次,因为是存到 ThreadLoacl 中,并且只有在非 null 时才会进行初始化;
如果想要在调用 `SphU.entry()` 或 `SphO.entry()` 前,自定义一个 Context,则通过 `ContextUtil.enter()` 方法来创建。Context 是保存在 ThreadLoacl 中的,每次执行的时候会优先到 ThreadLocal 中获取,为 null 时,才会调用 `MyContextUtil.myEnter(Constants.CONTEXT_DEFUALT_NAME,"",resourceWrapper.getType())` 来创建一个 Context。当 Entry 执行 exit() 方法时,如果 entry 的 parent 节点为 null,表示是当前 Context 中最外层的 Entry 了,此时将 ThreadLoacl 中的 Context 清空。
##### Context 的创建与销毁
每次执行 entry 方法时,就是试图冲破一个资源时,都会生成一个上下文,这个上下文中会保存着调用链的根节点和当前的入口。Context 是通过 ContextUtil 创建的,具体的方法时 trueEnter,代码如下
```java
protected static Context trueEnter(String name, String origin) {
// 尝试着从 ThreadLocal 中获取 context
Context context = contextHolder.get();
// 若 ThreadLocal 中没有 context,则尝试着从缓存 map(contextNameNodeMap)中获取
if (context == null) {
// 缓存 map 的 key 为 context 名称;value 为 EntranceNode
Map localCacheNameMap = contextNameNodeMap;
// 从缓存中获取 EntranceNode
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
// 若缓存 map 的大小超出了context 的阈值,则直接返回 NULL_CONTEXT
return NULL_CONTEXT;
} else {
LOCK.lock();
try {
// 双端检索,为了方式并发创建,再次从缓存中获取 EntranceNode
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
// 创建一个 EntranceNode
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// 将 EntranceNode 添加到 ROOT 中。Add entrance node.
Constants.ROOT.addChild(node);
/**
* 缓存 EntranceNode
* 为了防止"迭代稳定性问题",采用此种方式进行缓存,应用的场景是对共享集合的写操作
*/
Map newMap = new HashMap<>(contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
// 将 context 的 name 与 EntranceNode 封装为 context
context = new Context(node, name);
// 初始化 context 的来源
context.setOrigin(origin);
// 将 context 保存到 ThreadLocal 中
contextHolder.set(context);
}
return context;
}
```
Context 的创建过程:
1. 先从 ThreadLocal 中获取,如果能获取到,则直接返回,如果获取不到则继续第二步;
2. 从一个静态的 map 中根据 Context 的名称获取 DefaultNode,如果能获取到则直接返回,否则继续第三步;
3. 加锁后进行一次 double_check,如果还是没能从 map 中获取到 DefaultNode,则创建一个 EntranceNode,并把该 EntranceNode 添加到一个全局的 ROOT 节点中,然后将该节点添加到 map 中去;
4. 根据 EntranceNode 创建一个 Context,并将该 Context 保存到 ThreadLocal 中,以便下一个请求可以直接获取
ThreadLocal 中保存的 Context 会在 `ContextUtil.exit()` 方法调用时清除,这个方法的调用时机有两种情况:
- 主动调用
- 当一个入口 Entry 要退出,执行该 Entry 的 trueExit() 方法时,会触发 `ContextUtil.exit()` 方法的调用,但前提条件是,当前 Entry 的父 Entry 为 null,说明该 Entry 是对顶层的根节点,此时才可以清除 Context。
#### Entry
默认情况下,Sentinel 会将 Controller 中的方法作为被保护资源,Entry 表示一次资源操作,内部会保存当前调用信息。在一个 Context 生命周期中多次资源操作,也就是对应多个 Entry,这些 Entry 形成 parent / child 结构保存在 Entry 实例中
- 每一次资源调用都会创建一个 Entry。Entry 包含了资源名、curNode(当前统计节点)、originNode(来源统计节点)等信息。
- CtEntry 为普通的 Entry,在调用 `SphU.entry(xxx)` 的时候创建。特性:Linked entry within current context(内部维护着 parent 和 child)
- **需要注意的一点**:CtEntry 构造函数中会做**调用链的变换**,即将当前 Entry 接到传入 Context 的调用链路上(setUpEntryFor),即若 Context 中的 curEntry 为空,则将当前 CtEntry 设置为 Context 的 curEntry;若 Context 中的 curEntry 不为空,则将当前 ctEntry 设置为 Context 的 curEntry 的 child 属性;
- 资源调用结束时需要 `entry.exit()`。exit 操作会过一遍 slot chain exit,恢复调用栈,exit context 然后清空 entry 中的 context 防止重复调用。
```java
public abstract class Entry implements AutoCloseable {
private static final Object[] OBJECTS0 = new Object[0];
private final long createTimestamp;
private long completeTimestamp;
private Node curNode;
private Node originNode;
……
}
class CtEntry extends Entry {
protected Entry parent = null;
protected Entry child = null;
protected ProcessorSlot