Sentinel 规则的种类

引言

在前面的文章中,我们已经简单地介绍了如何在 Sentinel 中如何定义资源,本文将介绍 Sentinel 中所包含的各种类型的规则,和 Sentinel 相关的所有文章均会收录于<Sentinel系列文章>中,感兴趣的同学可以看一下。

规则的种类

Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效。同时 Sentinel 也提供相关 API,供您来定制自己的规则策略。

Sentinel 支持以下几种规则:流量控制规则、熔断降级规则、系统保护规则、来源访问控制规则和热点参数规则。

流量控制

流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

FlowSlot 会根据预设的规则,结合前面 NodeSelectorSlot、ClusterNodeBuilderSlot、StatisticSlot 统计出来的实时信息进行流量控制。

限流的直接表现是在执行 Entry nodeA = SphU.entry(resourceName) 的时候抛出 FlowException 异常。FlowException 是 BlockException 的子类,您可以捕捉 BlockException 来自定义被限流之后的处理逻辑。

同一个资源可以创建多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。

一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

Field 说明 默认值
resource 资源名,资源名是限流规则的作用对象
count 限流阈值
grade 限流阈值类型,QPS 模式(1)或并发线程数模式(0) QPS 模式
limitApp 流控针对的调用来源 default,代表不区分调用来源
strategy 调用关系限流策略:直接、链路、关联 根据资源本身(直接)
controlBehavior 流控效果(直接拒绝/WarmUp/匀速+排队等待) 直接拒绝
clusterMode 是否集群限流

理解上面规则的定义之后,我们可以通过调用 FlowRuleManager.loadRules() 方法来定义流量控制规则,比如:

1
2
3
4
5
6
7
8
9
10
private void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule(resourceName);
// set limit qps to 20
rule.setCount(20);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setLimitApp("default");
rules.add(rule);
FlowRuleManager.loadRules(rules);
}

基于QPS or 并发数

流量控制主要有两种统计类型,一种是统计并发线程数,另外一种则是统计 QPS。类型由 FlowRule 的 grade 字段来定义。其中,0 代表根据并发数量来限流,1 代表根据 QPS 来进行流量控制。其中线程数、QPS 值,都是由 StatisticSlot 实时统计获取的。

并发数控制用于保护业务线程池不被慢调用耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对太多线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。

而基于 QPS 的限流就比较简单,Sentinel 会为每个维度都保存一个 QPS 计数器,例如请求来源,调用入口,资源本身等维度,当 QPS 超过某个阈值的时候,则采取措施进行流量控制。

在 Sentinel 中 QPS 的统计和检查都是通过一套无锁化的滑动窗口来进行的,也就是说通过 QPS 进行的流控实际上并不会准确地和定义的规则相匹配,在极端情况下会出现放行流量大于设定的限制的情况,但是如果您的线程池数量小于 QPS 限制数并且没有耗时的自定义 ProcessSlot 的话就不会出现严重的问题。而线程的统计也是通过一套无锁化的计数器实现的,在极端情况下会出现实际放行的线程数小于设定的限制的情况。

基于调用关系的流量控制

在 Sentinel 中,函数的调用关系按层级进行组织,从而形成一个完整的调用链路。Sentinel 通过 NodeSelectorSlot 建立不同资源间的调用的关系,并且通过 ClusterNodeBuilderSlot 记录每个资源的实时统计信息。有了调用链路的统计信息,我们就可以衍生出多种流量控制手段。
调用方限流
ContextUtil.enter(resourceName, origin) 方法中的 origin 参数标明了调用方身份。这些信息会在 ClusterBuilderSlot 中被统计。可通过以下命令来展示不同的调用方对同一个资源的调用数据:

1
curl http://localhost:8719/origin?id=nodeA

调用数据示例:

1
2
3
4
id: nodeA
idx origin threadNum passedQps blockedQps totalQps aRt 1m-passed 1m-blocked 1m-total
1 caller1 0 0 0 0 0 0 0 0
2 caller2 0 0 0 0 0 0 0 0

上面这个命令展示了资源名为 nodeA 的资源被两个不同的调用方调用的统计。

流控规则中的 limitApp 字段用于根据调用来源进行流量控制。该字段的值有以下三种选项,分别对应不同的场景:

  • default:表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。
  • {some_origin_name}:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者caller1的规则,那么当且仅当来自 caller1 对 NodeA 的请求才会触发流量控制。
  • other:表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源NodeA配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1 对 NodeA 的调用,都不能超过 other 这条规则定义的阈值。

同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default
链路限流
NodeSelectorSlot 中记录了资源之间的调用链路,这些资源之间的调用关系本质上构成了一棵调用树。这棵树的根节点是一个名字为 machine-root 的虚拟节点,所有调用链的入口都是这个虚节点的子节点。

一棵典型的调用树如下图所示:

1
2
3
4
5
6
7
            machine-root
/ \
/ \
Entrance1 Entrance2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)

上图中来自入口 Entrance1 和 Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 strategy 为 RuleConstant.STRATEGY_CHAIN,同时设置 refResource 为 Entrance1 来表示只有从入口 Entrance1 的调用才会记录到 NodeA 的限流统计当中,而不关心经 Entrance2 到来的调用。

调用链的入口(上下文)是通过 API 方法 ContextUtil.enter(contextName) 定义的,其中 contextName 即对应调用链路入口名称, 注意这里的 contextName 也可以被理解为 resourceName。也就是说,链路限流和调用方限流的区别是:

  • 链路限流:根据 ContextUtil.enter(resourceName, origin) 的第一个参数 resourceName 作为 key 来限流,如果没有显式地调用 ContextUtil.enter, 会使用 default 作为 resourceName。
  • 调用方限流: 根据 ContextUtil.enter(resourceName, origin) 的第二个参数 origin 作为 key 来限流,如果没有显式地调用 ContextUtil.enter, 会使用空字符串作为 origin。

简单地说,链路限流和调用方限流分别是用ContextUtil.enter(resourceName, origin)的不同参数作为 key 来匹配限流规则的。
关联限流
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 strategy 为 RuleConstant.STRATEGY_RELATE 同时设置 refResource 为 write_db。这样当写库操作过于频繁时,读数据的请求会被限流。

流控效果

流量控制的效果包括以下几种:直接拒绝、Warm Up、匀速排队。对应 FlowRule 中的 controlBehavior 字段。

直接拒绝
直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。

Warm Up
Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。当使用 Warm Up 模式时,我们还需要指定启动时开放的 QPS 比例(DEFAULT_COLD_FACTOR,默认值为3,代表 30%),以及系统预热所需时长(warmUpPeriodSec, 默认值是 10 秒)。

通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:

qps-warm-up
匀速排队

匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。它的中心思想是,以固定的间隔时间让请求通过。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过(排队等待处理);若预期的通过时间超出最大排队时长,则直接拒接这个请求。

这种方式适合用于请求以突刺状来到,这个时候我们不希望一下子把所有的请求都通过,这样可能会把系统压垮;同时我们也期待系统以稳定的速度,逐步处理这些请求,以起到“削峰填谷”的效果,而不是拒绝所有请求。Sentinel 匀速排队等待策略是 Leaky Bucket 算法结合虚拟队列等待机制实现的。

该方式的作用如下图所示:
rate-limiter

注意:匀速排队模式暂时不支持 QPS > 1000 的场景,因为 Sentinel 内部通过 Thread::sleep 来实现虚拟等待队列,QPS 等于 1000 时,每个请求正好 sleep 1 ms,而当 QPS > 1000 时,没法精准的控制 sleep 小于 1 ms 的时长。

集群流控

为什么要使用集群流控呢?假设我们希望限制某个用户调用某一 API 的 QPS(比如限制到 50),但是提供这个服务的机器数可能很多(比如有 100 台),该用户的每一个请求都有可能路由到不同的机器上去,所以我们不能只使用单机上的流控规则。这时候我们很自然地就想到,找一个 server 来专门来统计总的调用量,其它的实例都与这台 server 通信来判断是否可以调用。这就是最基础的集群流控的方式。

另外集群流控还可以解决流量不均匀导致总体限流效果不佳的问题。假设集群中有 10 台机器,我们给每台机器设置单机限流阈值为 10 QPS,理想情况下整个集群的限流阈值就为 100 QPS。不过实际情况下流量到每台机器可能会不均匀,会导致总量没有到的情况下某些机器就开始限流。因此仅靠单机维度去限制的话会无法精确地限制总体流量。而集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。

集群流控中共有两种身份:

  • Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
  • Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。

cluster-flow-control
Sentinel 1.4.0 开始引入了集群流控模块,主要包含以下几部分:

  • sentinel-cluster-common-default: 公共模块,包含公共接口和实体
  • sentinel-cluster-client-default: 默认集群流控 client 模块,使用 Netty 进行通信,提供接口方便序列化协议扩展
  • sentinel-cluster-server-default: 默认集群流控 server 模块,使用 Netty 进行通信,提供接口方便序列化协议扩展;同时提供扩展接口对接规则判断的具体实现(TokenService),默认实现是复用 sentinel-core 的相关逻辑
集群流控规则

FlowRule 添加了两个字段用于集群限流相关配置:

1
2
private boolean clusterMode; // 标识是否为集群限流配置
private ClusterFlowConfig clusterConfig; // 集群限流相关配置项

其中 用一个专门的 ClusterFlowConfig 代表集群限流相关配置项,以与现有规则配置项分开:

1
2
3
4
5
6
7
8
// (必需)全局唯一的规则 ID,由集群限流管控端分配.
private Long flowId;

// 阈值模式,默认(0)为单机均摊,1 为全局阈值.
private int thresholdType = ClusterRuleConstant.FLOW_THRESHOLD_AVG_LOCAL;

// 在 client 连接失败或通信失败时,是否退化到本地的限流模式
private boolean fallbackToLocalWhenFail = true;
  • flowId: 代表全局唯一的规则 ID,Sentinel 集群限流服务端通过此 ID 来区分各个规则,因此务必保持全局唯一。一般 flowId 由统一的管控端进行分配,或写入至 DB 时生成。
  • thresholdType: 代表集群限流阈值模式。其中单机均摊模式下配置的阈值等同于单机能够承受的限额,token server 会根据客户端对应的 namespace(默认为 project.name 定义的应用名)下的连接数来计算总的阈值(比如独立模式下有 3 个 client 连接到了 token server,然后配的单机均摊阈值为 10,则计算出的集群总量就为 30);而全局模式下配置的阈值等同于整个集群的总阈值。
  • fallbackToLocalWhenFail: 在 client 和 Token Server 连接失败或通信失败时,是否退化到本地的限流模式

在集群流控的场景下,我们推荐使用动态规则源来管理规则。对于客户端,我们可以按照原有的方式来向 FlowRuleManager 和 ParamFlowRuleManager 注册动态规则源,例如:

1
2
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, parser);
FlowRuleManager.register2Property(flowRuleDataSource.getProperty());

对于集群流控 token server,还会向下划分出多个作用域(namespace),你可以认为一个 Token Server 会负责多个 namespace 的限流工作,因此我们需要设置一个自动根据 namespace 生成动态规则源的 PropertySupplier:

1
2
3
4
5
// Supplier 类型:接受 namespace,返回生成的动态规则源,类型为 SentinelProperty<List<FlowRule>>
// ClusterFlowRuleManager 针对集群限流规则,ClusterParamFlowRuleManager 针对集群热点规则,配置方式类似
ClusterFlowRuleManager.setPropertySupplier(namespace -> {
return new SomeDataSource(namespace).getProperty();
});

然后每当集群限流服务端负责的 namespace set 产生变更时,Sentinel 会自动针对新加入的 namespace 生成动态规则源并进行自动监听,此外还会删除旧的不需要的规则源。

集群限流客户端

要想使用集群限流功能,必须引入集群限流 client 相关依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-cluster-client-default</artifactId>
<version>1.7.2</version>
</dependency>

用户可以通过 API 将当前模式置为客户端模式:http://<ip>:<port>/setClusterMode?mode=<xxx>

或者通过 ClusterStateManager API 手动指定模式:ClusterStateManager.applyState(ClusterStateManager.CLUSTER_CLIENT);

其中 mode 为 0 代表 client(ClusterStateManager.CLUSTER_CLIENT),1 代表 server。设置成功后,集群限流客户端将会开启并连接远程的 token server。我们可以在 ~/logs/csp/sentinel-record.log 日志中查看连接的相关日志。

若集群限流客户端未进行配置,则用户需要对客户端进行基本的配置,比如指定集群限流 token server。Sentinel 提供了 API 进行配置:http://<ip>:<port>/cluster/client/modifyConfig?data=<config>

其中 data 是 JSON 格式的配置项:

  • serverHost: token server host
  • serverPort: token server 端口
  • requestTimeout: 请求的超时时间(默认为 20 ms)

当然也可以通过动态配置源进行配置。总的来说,集群限流 token client 共有两种配置:

  • 客户端分配配置(ClusterClientAssignConfig),包括要连接的对端 token server 地址等相关信息。我们可以通过 ClusterClientConfigManager 的 registerServerAssignProperty 方法注册动态配置源。分配配置通常通过统一的分配表解析而来。
  • 客户端通信配置(ClusterClientConfig),包括通信的超时时长等配置。我们可以通过 ClusterClientConfigManager 的 registerClientConfigProperty 方法注册动态配置源。

配置源注册的相关逻辑也可以置于 InitFunc 实现类中,并通过 SPI 注册,在 Sentinel 初始化时即可自动进行配置源加载监听。

若用户未引入集群限流 client 相关依赖,或者 client 未开启/连接失败/通信失败,则对于开启了集群模式的规则:

  • 集群热点限流默认直接通过
  • 普通集群限流会退化到 local 模式的限流,即在本地按照单机阈值执行限流检查

当 token client 与 server 之间的连接意外断开时,token client 会不断进行重试,每次重试的间隔时间以 n * 2000 ms 的形式递增。

集群限流服务端

要想使用集群限流服务端,必须引入集群限流 server 相关依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-cluster-server-default</artifactId>
<version>1.7.2</version>
</dependency>

Sentinel 集群限流服务端有两种启动方式:

  • 独立模式(Alone),即作为独立的 token server 进程启动,独立部署,隔离性好,但是需要额外的部署操作。独立模式适合作为 Global Rate Limiter 给集群提供流控服务。
    token-server-alone
  • 嵌入模式(Embedded),即作为内置的 token server 与服务在同一进程中启动。在此模式下,集群中各个实例都是对等的,token server 和 client 可以随时进行转变,因此无需单独部署,灵活性比较好。但是隔离性不佳,需要限制 token server 的总 QPS,防止影响应用本身。嵌入模式适合某个应用集群内部的流控。
    token-server-embedded

Sentinel 提供了 HTTP API 用于在 embedded 模式下转换集群流控身份:http://<ip>:<port>/setClusterMode?mode=<xxx>。其中 mode 为 0 代表 client,1 代表 server,-1 代表关闭。注意应用端需要引入集群限流客户端或服务端的相应依赖。在独立模式下,我们可以直接创建对应的 ClusterTokenServer 实例并在 main 函数中通过 start 方法启动 Token Server。

在使用集群限流服务端时,一般会为其注册动态配置源来动态地进行配置修改。配置类型有以下几种:

  • namespace set: 集群限流服务端的作用域(命名空间),用于指定该 token server 可以服务哪些应用或分组,嵌入模式下可以设置为自己的应用名。集群限流 client 在连接到 token server 后会上报自己的命名空间(默认为 project.name 配置的应用名),token server 会根据上报的命名空间名称统计连接数。
  • transport config: 集群限流服务端通信相关配置,如 server port
  • flow config: 集群限流服务端限流相关配置,如滑动窗口统计时长、格子数目、最大允许总 QPS等

我们可以通过 ClusterServerConfigManager 的各个 registerXxxProperty 方法来注册相关的配置源。而且,从 1.4.1 版本开始,Sentinel 支持给 token server 配置最大允许的总 QPS(maxAllowedQps),用于对 Token Server 的资源使用进行限制,防止在嵌入模式下影响应用本身。下图展示了集群限流的服务端和客户端如何根据配置切换角色,并适用相应的配置信息。
token-server-client-config
此外,若在生产环境使用集群限流,管控端还需要关注以下的问题(目前 sentinel 中是没有提供这些问题的完备解决方案的):

  • Token Server 自动管理、调度(分配/选举 Token Server)
  • Token Server 高可用,在某个 server 不可用时自动 failover 到其它机器

网关流控

Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。
gateway-flow-control
Sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 模块,此模块中包含网关限流的规则和自定义 API 的实体和管理逻辑:

  • GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
  • ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api,请求 path 模式为 /foo/** 和 /baz/** 的都归到 my_api 这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。

其中网关限流规则 GatewayFlowRule 的字段解释如下:

  • resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。
  • resourceMode:规则是针对 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的 API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是 route。
  • grade:限流指标维度,同限流规则的 grade 字段。
  • count:限流阈值
  • intervalSec:统计时间窗口,单位是秒,默认是 1 秒。
  • controlBehavior:流量整形的控制效果,同限流规则的 controlBehavior 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。
  • burst:应对突发请求时额外允许的请求数目。
  • maxQueueingTimeoutMs:匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。
  • paramItem:参数限流配置。若不提供,则代表不针对参数进行限流,该网关规则将会被转换成普通流控规则;否则会转换成热点规则。其中的字段:
    • parseStrategy:从请求中提取参数的策略,目前支持提取来源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 参数(PARAM_PARSE_STRATEGY_URL_PARAM)四种模式。
    • fieldName:若提取策略选择 Header 模式或 URL 参数模式,则需要指定对应的 header 名称或 URL 参数名称。
    • pattern:参数值的匹配模式,只有匹配该模式的请求属性值会纳入统计和流控;若为空则统计该请求属性的所有值。(1.6.2 版本开始支持)
    • matchStrategy:参数值的匹配策略,目前支持精确匹配(PARAM_MATCH_STRATEGY_EXACT)、子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)和正则匹配(PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本开始支持)

用户可以通过 GatewayRuleManager.loadRules(rules) 手动加载网关规则,或通过 GatewayRuleManager.register2Property(property) 注册动态规则源动态推送(推荐方式)。

下面我们以 Spring Cloud Gateway 为例,介绍如何在网关中使用 sentinel。从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:

  • route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
  • 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组

在使用时,首先需要引入以下模块(以 Maven 为例):

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>x.y.z</version>
</dependency>

此外,还需要将 Sentinel 的 SentinelGatewayFilter 实例以及 SentinelGatewayBlockExceptionHandler 实例注入到 Spring Cloud Gateway 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
public class GatewayConfiguration {

private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;

public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
}

通过上述方式,对于 Spring 配置文件中配置的路由条目,我们都可以根据其 route_id 进行流控。除此之外,如果您想要对不同路由条目进行组合流控,可以自定义一些 API 分组。

比如我们在 Spring Cloud Gateway 中配置了以下路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server:
port: 8090
spring:
application:
name: spring-cloud-gateway
cloud:
gateway:
enabled: true
discovery:
locator:
lower-case-service-id: true
routes:
# Add your routes here.
- id: product_route
uri: lb://product
predicates:
- Path=/product/**
- id: httpbin_route
uri: https://httpbin.org
predicates:
- Path=/httpbin/**
filters:
- RewritePath=/httpbin/(?<segment>.*), /$\{segment}

同时自定义了一些 API 分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("some_customized_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/product/baz"));
add(new ApiPathPredicateItem().setPattern("/product/foo/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("another_customized_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
add(new ApiPathPredicateItem().setPattern("/ahas"));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}

那么这里面的 route ID(如 product_route)和 API name(如 some_customized_api)都会被标识为 Sentinel 的资源。比如访问网关的 URL 为 http://localhost:8090/product/foo/22 时,对应的统计量会加到 product_route 和 some_customized_api 这两个资源上面,而 http://localhost:8090/httpbin/json 只会统计到 httpbin_route 资源上面。

此外,您也可以在 GatewayCallbackManager 注册回调:

  • setBlockHandler:注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为 BlockRequestHandler。默认实现为 DefaultBlockRequestHandler,当被限流时会返回类似于下面的错误信息:Blocked by Sentinel: FlowException。

当通过 GatewayRuleManager 加载网关流控规则(GatewayFlowRule)时,无论是否针对请求属性进行限流,Sentinel 底层都会将网关流控规则转化为热点参数规则(ParamFlowRule),存储在 GatewayRuleManager 中,与正常的热点参数规则相隔离。当我们在 Sentinel 中启用网关的参数流控功能后,它会为网关流控规则设置参数索引(idx),并同步之前生成的热点参数规则中。

外部请求进入 API Gateway 时会经过 Sentinel 实现的 filter,其中会依次进行 路由/API 分组匹配、请求属性解析和参数组装。Sentinel 会根据配置的网关流控规则来解析请求属性,并依照参数索引顺序组装参数数组,最终传入 SphU.entry(res, args) 中。Sentinel API Gateway Adapter Common 模块向 Slot Chain 中添加了一个 GatewayFlowSlot,专门用来做网关规则的检查。GatewayFlowSlot 会从 GatewayRuleManager 中提取生成的热点参数规则,根据传入的参数依次进行规则检查。若某条规则不针对请求属性,则会将一个空数组喂到 SphU.entry 接口的 args 参数中,达到普通流控的效果。

gateway-flow-control-impl

熔断降级

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

熔断降级规则包含下面几个重要的属性:

Field 说明 默认值
resource 资源名,资源名是限流规则的作用对象
count 限流阈值
grade 熔断策略,支持秒级 RT/秒级异常比例/分钟级异常数 秒级平均 RT
timeWindow 降级的时间,单位为 s
rtSlowRequestAmount RT 模式下 1 秒内连续多少个请求的平均 RT 超出阈值方可触发熔断(1.7.0 引入) 5
minRequestAmount 异常熔断的触发最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) 5

同一个资源可以同时有多个降级规则。

理解上面规则的定义之后,我们可以通过调用 DegradeRuleManager.loadRules() 方法来定义流量控制规则。

1
2
3
4
5
6
7
8
9
10
11
private void initDegradeRule() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule rule = new DegradeRule();
rule.setResource(KEY);
// set threshold RT, 10 ms
rule.setCount(10);
rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
rule.setTimeWindow(10);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}

我们通常用以下几种方式来衡量资源是否处于稳定的状态:

  • 平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 N 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
  • 异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= N(可配置),并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。

异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。为了统计异常比例或异常数,需要通过 Tracer.trace(ex) 记录业务异常。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Entry entry = null;
try {
entry = SphU.entry(key, EntryType.IN, key);

// Write your biz code here.
// <<BIZ CODE>>
} catch (Throwable t) {
if (!BlockException.isBlockException(t)) {
Tracer.trace(t);
}
} finally {
if (entry != null) {
entry.exit();
}
}

开源整合模块,如 Sentinel Dubbo Adapter, Sentinel Web Servlet Filter 或 @SentinelResource 注解会自动统计业务异常,无需手动调用。

系统保护

Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统规则包含下面几个重要的属性:

Field 说明 默认值
avgRt 所有入口流量的平均响应时间 -1 (不生效)
maxThread 入口流量的最大并发数 -1 (不生效)
qps 所有入口资源的 QPS -1 (不生效)
highestCpuUsage 当前系统的 CPU 使用率(0.0-1.0) -1 (不生效)
highestSystemLoad load1(最近一分钟的 CPU 平均负载)触发值,用于触发自适应控制阶段 -1 (不生效)

理解上面规则的定义之后,我们可以通过调用 SystemRuleManager.loadRules() 方法来定义流量控制规则。

1
2
3
4
5
6
7
private void initSystemRule() {
List<SystemRule> rules = new ArrayList<>();
SystemRule rule = new SystemRule();
rule.setHighestSystemLoad(10);
rules.add(rule);
SystemRuleManager.loadRules(rules);
}

注意系统规则只针对入口资源(EntryType=IN)生效。

上述的规则中,CPU 使用率、总体平均 RT、入口 QPS 和并发线程数都是很简单易懂的度量值,而 highestSystemLoad 不是一个度量值,而是一个触发值,当 load1 大于 highestSystemLoad 时会触发自适应控制阶段,那么自适应控制阶段又是什么呢?

长期以来,系统保护的思路是根据硬指标,即系统的负载 (load1) 来做系统过载保护。当系统负载高于某个阈值,就禁止或者减少流量的进入;当 load 开始好转,则恢复流量的进入。这个思路给我们带来了不可避免的两个问题:

  • load 是一个“结果”,如果根据 load 的情况来调节流量的通过率,那么就始终有延迟性。也就意味着通过率的任何调整,都会过一段时间才能看到效果。当前通过率是使 load 恶化的一个动作,那么也至少要过 1 秒之后才能观测到;同理,如果当前通过率调整是让 load 好转的一个动作,也需要 1 秒之后才能继续调整,这样就浪费了系统的处理能力。所以我们看到的曲线,总是会有抖动。
  • 恢复慢, 想象一下这样的一个场景,下游应用不可靠,导致应用 RT 很高,从而 load 到了一个很高的点。过了一段时间之后下游应用恢复了,应用 RT 也相应减少。这个时候,其实应该大幅度增大流量的通过率;但是由于这个时候 load 仍然很高,通过率的恢复仍然不高。

TCP BBR 的思想是 Sentinel 自适应流控规则出发点。我们应该根据系统能够处理的请求,和允许进来的请求,来做平衡,而不是根据一个间接的指标(系统 load)来做限流。最终我们追求的目标是: 在系统不被拖垮的情况下,提高系统的吞吐率,而不是 load 一定要到低于某个阈值。如果我们还是按照固有的思维,超过特定的 load 就禁止流量进入,系统 load 恢复就放开流量,这样做的结果是无论我们怎么调参数,调比例,都是按照果来调节因,都无法取得良好的效果。

Sentinel 在系统自适应保护的做法是,用 load1 作为启动自适应保护的因子,而允许通过的流量由处理请求的能力,即请求的响应时间以及当前系统正在处理的请求速率来决定。
tcp-bbr
我们把系统处理请求的过程想象为一个水管,到来的请求是往这个水管灌水,当系统处理顺畅的时候,请求不需要排队,直接从水管中穿过,这个请求的RT是最短的;反之,当请求堆积的时候,那么处理请求的时间则会变为:排队时间 + 最短处理时间。

  • 推论一: 如果我们能够保证水管里的水量,能够让水顺畅的流动,则不会增加排队的请求;也就是说,这个时候的系统负载不会进一步恶化。

我们用 T 来表示(水管内部的水量),用RT来表示请求的处理时间,用QPS来表示每秒能处理的请求数,那么一个请求从进入水管道到从水管出来这整个过程中,这个水管最多会存在 QPS * RT 个请求。换一句话来说,当 T ≈ QPS* Avg(RT) 的时候,我们可以认为系统的处理能力和允许进入的请求个数达到了平衡,系统的负载不会进一步恶化。

通过上述方式限流水管的水位是可以达到了一个平衡,接下来的问题是,这个平衡点只能保证水管的水位不再继续增高,但是在达到平衡点之前,这个水管里已经堆积了多少水呢?如果之前水管的水已经过多了,那么这个时候系统允许通过的水量可能只能缓慢通过,RT会很大,之前堆积在水管里的水会滞留导致系统总的吞吐量并不高;反之,如果之前的水管水位偏低,那么又会浪费了系统的处理能力。

  • 推论二: 当保持入口的流量是水管出口流量的最大的值的时候,可以最大利用水管的处理能力。

因此,Sentinel 用一个系统负载的值(load1)来激发这套机制启动,你需要根据自己的系统特点测试并找到一个合适 load1 阈值,当系统的 load1 处于这一阈值时系统刚好达到最大吞吐,也就是说水管中既没有积压也没有空余时,启动上述的自适应功能。接下来 Sentinel 就用当前处理任务的线程数(线程数代表了水管的水量)与 QPS * RT 进行比较,如果 当前线程数>QPS* RT 则触发限流,否则放行流量。

访问控制

很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的访问控制(黑白名单)的功能。黑白名单根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

授权规则,即黑白名单规则(AuthorityRule)非常简单,主要有以下配置项:

  • resource:资源名,即规则的作用对象
  • limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB
  • strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式

比如我们希望控制对资源 test 的访问设置白名单,只有来源为 appA 和 appB 的请求才可通过,则可以配置如下白名单规则:

1
2
3
4
5
AuthorityRule rule = new AuthorityRule();
rule.setResource("test");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));

热点规则

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
hot-spot-param
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。

要使用热点参数限流功能,需要引入以下依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>x.y.z</version>
</dependency>

然后为对应的资源配置热点参数限流规则,并在 entry 的时候传入相应的参数,即可使热点参数限流生效。

那么如何传入对应的参数以便 Sentinel 统计呢?我们可以通过 SphU 类里面几个 entry 重载方法来传入:

1
2
3
public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException

public static Entry entry(Method method, EntryType type, int count, Object... args) throws BlockException

其中最后的一串 args 就是要传入的参数,有多个就按照次序依次传入。若 entry 的时候传入了热点参数,那么 exit 的时候也一定要带上对应的参数 exit(count, args),否则可能会有统计错误。比如要传入两个参数 paramA 和 paramB,则可以:

1
2
3
4
5
6
7
8
9
10
11
Entry entry = null;
try {
entry = SphU.entry(resourceName, EntryType.IN, 1, paramA, paramB);
// Your logic here.
} catch (BlockException ex) {
// Handle request rejection.
} finally {
if (entry != null) {
entry.exit(1, paramA, paramB);
}
}

对于 @SentinelResource 注解方式定义的资源,若注解作用的方法上有参数,Sentinel 会将它们作为参数传入 SphU.entry(res, args)。比如以下的方法里面 uid 和 type 会分别作为第一个和第二个参数传入 Sentinel API,从而可以用于热点规则判断:

1
2
3
4
@SentinelResource("myMethod")
public Result doSomething(String uid, int type) {
// some logic here...
}

热点参数规则(ParamFlowRule)类似于流量控制规则(FlowRule):

Field 说明 默认值
resource 资源名,必填
count 限流阈值,必填
grade 限流模式 QPS 模式
durationInSec 统计窗口时间长度(单位为秒),1.6.0 版本开始支持 1s
controlBehavior 流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持 快速失败
maxQueueingTimeMs 最大排队等待时长(仅在匀速排队模式生效),1.6.0 版本开始支持 0ms
paramIdx 热点参数的索引,必填,对应SphU.entry(xxx, args)中的参数索引位置
paramFlowItemList 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面count阈值的限制。仅支持基本类型和字符串类型
clusterMode 是否是集群参数流控规则 false
clusterConfig 集群流控相关配置

我们可以通过 ParamFlowRuleManager 的 loadRules 方法更新热点参数规则,下面是一个示例:

1
2
3
4
5
6
7
8
9
10
ParamFlowRule rule = new ParamFlowRule(resourceName)
.setParamIdx(0)
.setCount(5);
// 针对 int 类型的参数 PARAM_B,单独设置限流 QPS 阈值为 10,而不是全局的阈值 5.
ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B))
.setClassType(int.class.getName())
.setCount(10);
rule.setParamFlowItemList(Collections.singletonList(item));

ParamFlowRuleManager.loadRules(Collections.singletonList(rule));

Sentinel 中会为每个 resource 的每个参数,维护一个计数器,热点流控就是通过检查各个参数对应的计数器来实现的。

各种规则的效果

在 Sentinel 中所有流控降级相关的异常都是异常类 BlockException 的子类:

  • 流控异常:FlowException
  • 熔断降级异常:DegradeException
  • 系统保护异常:SystemBlockException
  • 热点参数限流异常:ParamFlowException

我们可以通过以下函数判断是否为 Sentinel 的流控降级异常:

1
BlockException.isBlockException(Throwable t);

除了在业务代码逻辑上看到规则生效,我们也可以通过下面简单的方法,来校验规则生效的效果:

  • 暴露的 HTTP 接口:通过运行下面命令 curl http://localhost:8719/cnode?id=<资源名称>,观察返回的数据。如果规则生效,在返回的数据栏中的 block 以及 block(m) 中会有显示

  • 日志:Sentinel 提供秒级的资源运行日志以及限流日志

    1. 拦截详情日志(block 日志): 无论触发了限流、熔断降级还是系统保护,它们的秒级拦截详情日志都在 ${user_home}/logs/csp/sentinel-block.log里。如果没有发生拦截,则该日志不会出现。日志格式如下:

      1
      2
      2014-06-20 16:35:10|1|sayHello(java.lang.String,long),FlowException,default,origin|61,0
      2014-06-20 16:35:11|1|sayHello(java.lang.String,long),FlowException,default,origin|1,0

      日志含义:时间戳|该秒发生的第N个资源|资源名称, 拦截的原因, default, origin, 被拦截的数量

    2. 秒级监控日志: 所有的资源都会产生秒级日志,它在 ${user_home}/logs/csp/${app_name}-${pid}-metrics.log 里。格式如下:

      1
      1532415661000|2018-07-24 15:01:01|sayHello(java.lang.String)|12|3|4|2|295

      日志含义:时间戳|格式化之后的时间戳|资源名|表示到来的数量|被拦截的数量|每秒结束的资源个数(完成调用),包括正常结束和异常结束的情况|异常的数量|资源的平均响应时间

    3. 业务日志: 其它的日志在 ${user_home}/logs/csp/sentinel-record.log.xxx 里。该日志包含规则的推送、接收、处理等记录,排查问题的时候会非常有帮助。

    4. 集群限流日志: ${log_dir}/sentinel-cluster-client.log:Token Client 日志,会记录请求失败的信息

Sentinel 提供以下扩展接口,可以通过 StatisticSlotCallbackRegistry 向 StatisticSlot 注册回调函数:

  • ProcessorSlotEntryCallback: callback when resource entry passed (onPass) or blocked (onBlocked)
  • ProcessorSlotExitCallback: callback when resource entry successfully completed (onExit)

可以利用这些回调接口来实现报警等功能,实时的监控信息可以从 ClusterNode 中实时获取。

参考内容

[1] Sentinel GitHub 仓库
[2] Sentinel 官方 Wiki
[3] Sentinel 1.6.0 网关流控新特性介绍
[4] Sentinel 微服务流控降级实践
[5] Sentinel 1.7.0 新特性展望
[6] Sentinel 为 Dubbo 服务保驾护航
[7] 在生产环境中使用 Sentinel
[8] Sentinel 与 Hystrix 的对比
[9] 大流量下的服务质量治理 Dubbo Sentinel初涉
[10] Alibaba Sentinel RESTful 接口流控处理优化
[11] 阿里 Sentinel 源码解析
[12] Sentinel 教程 by 逅弈
[13] Sentinel 专题文章 by 一滴水的坚持

贝克街的流浪猫 wechat
您的打赏将鼓励我继续分享!
  • 本文作者: 贝克街的流浪猫
  • 本文链接: https://www.beikejiedeliulangmao.top/middleware/sentinel/rules/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议,转载请注明出处!
  • 创作声明: 本文基于上述所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。