Sentinel 快速入门

Sentinel 简介

什么是 Sentinel?

Sentinel』是阿里中间件团队开源的,面向分布式服务架构的轻量级高可用流量控制组件,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。

  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。

  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。

  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel 的主要特性

Sentinel 的主要特性

Sentinel 分为两个部分:

  • 「核心库(Java 客户端)」不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。

  • 「控制台(Dashboard)」基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

Sentinel 基础概念

资源

『资源』是 Sentinel 的关键概念,只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。

它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。

规则

围绕资源的实时状态设定的规则,可以包括「流量控制规则」、「熔断降级规则」以及「系统保护规则」。我们通过规则定义如何程序保护资源,所有规则可以动态实时调整。

Sentinel 入门 demo

使用 Sentinel 来进行资源保护,主要分为几个步骤:

  1. 定义资源

  2. 定义规则

  3. 检验规则是否生效

先把可能需要保护的资源定义好,之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。

这里我们按照官网上的示例给出 Sentinel 的一个简单demo

1. 引入 Sentinel 依赖

如果您的应用使用了 Maven,则在 pom.xml 文件中加入以下代码即可:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.6.3</version>
</dependency>

2. 定义资源

一般我们会将 Java 中的方法定义为资源,我们也可以更灵活地对资源进行定义。

在下面的例子中,我们定义了一个名为「HelloWorld」的资源,我们将System.out.println("hello world");这个语句作为被保护的逻辑,把它用Sentinel API SphU.entry("HelloWorld")entry.exit()包围起来即可。参考代码如下:

public static void main(String[] args) {
    // 配置规则.
    initFlowRules();

    while (true) {
        // 1.5.0 版本开始可以直接利用 try-with-resources 特性
        // 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
        try (Entry entry = SphU.entry("HelloWorld")) {
            // 被保护的逻辑
            System.out.println("hello world");
        } catch (BlockException ex) {
            // 处理被流控的逻辑
            System.out.println("blocked!");
        }
    }
}

3. 定义规则

接下来,我们通过流控规则来指定允许该资源通过的请求次数,例如下面的代码定义了我们资源「HelloWorld」每秒最多只能通过 20 个请求(即将 QPS 设为 20)。

private static void initFlowRules(){
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule();
    rule.setResource("HelloWorld");
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    // Set limit QPS to 20.
    rule.setCount(20);
    rules.add(rule);
    FlowRuleManager.loadRules(rules);
}

4. 检查效果

Demo 运行之后,我们可以在日志~/logs/csp/${appName}-metrics.log.xxx里看到下面的输出:

|--timestamp-|------date time----|--resource-|p |block|s |e|rt
1529998904000|2018-06-26 15:41:44|hello world|20|0    |20|0|0
1529998905000|2018-06-26 15:41:45|hello world|20|5579 |20|0|728
1529998906000|2018-06-26 15:41:46|hello world|20|15698|20|0|0
1529998907000|2018-06-26 15:41:47|hello world|20|19262|20|0|0
1529998908000|2018-06-26 15:41:48|hello world|20|19502|20|0|0
1529998909000|2018-06-26 15:41:49|hello world|20|18386|20|0|0

其中p代表通过的请求,block代表被阻止的请求,s代表成功执行完成的请求个数,e代表用户自定义的异常,rt代表平均响应时长。

可以看到,这个程序每秒稳定输出 "hello world" 20 次,和规则中预先设定的阈值是一样的。

Sentinel 规则

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

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

流量控制规则 (FlowRule)

什么是流量控制

『流量控制』在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统单位时间的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。

Sentinel 流量控制原理

Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:

Sentinel流量控制

Sentinel 如何实现流量控制

我们可以通过以下的几个角度实现流量控制:

  • 资源的调用关系:如资源的调用链路,资源和资源之间的关系

  • 运行指标:例如 QPS、线程池、系统负载等;

  • 控制的效果:例如直接限流、冷启动、排队等。

Sentinel 的「设计理念」是让编码人员自由选择控制的角度,并进行灵活组合,从而达到想要的效果。

Sentinel 的流量控制规则由下面的关键属性组成:

Field 说明 默认值
resource 资源名,资源名是限流规则的作用对象
count 限流阈值
grade 限流阈值类型,QPS 或线程数模式 QPS 模式
limitApp 流控针对的调用来源 default,代表不区分调用来源
strategy 判断的根据是资源自身,还是根据其它关联资源 (refResource),还是根据链路入口 根据资源本身
controlBehavior 流控效果(直接拒绝 / 排队等待 / 慢启动模式) 直接拒绝

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

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

基于QPS/并发数的流量控制

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

并发线程数流量控制

并发线程数限流用于保护业务线程数不被耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对太多线程占用的情况,业内有使用隔离的方案,比如「Hystrix」通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。

Sentinel 并发线程数限流不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目,如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。例子参见:ThreadDemo

QPS流量控制

当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的手段包括以下几种:「直接拒绝」、「Warm Up」、「匀速排队」。对应FlowRule中的controlBehavior字段。

1. 直接拒绝

「直接拒绝RuleConstant.CONTROL_BEHAVIOR_DEFAULT」是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。具体的例子参见 FlowQpsDemo

这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。

2. Warm Up

「Warm UpRuleConstant.CONTROL_BEHAVIOR_WARM_UP」,即预热/冷启动方式。

当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限(即给定的count),给冷系统一个预热的时间,避免冷系统被压垮。详细文档可以参考 流量控制 - Warm Up 文档,具体的例子可以参见 WarmUpFlowDemo

下面我们构建一个使用冷启动规则的流控规则:

FlowRule rule = new FlowRule();
rule.setResource(resourceName);
rule.setCount(480);
rule.setGrade(RuleConstant.GRADE_QPS);
rule.setLimitApp("default");
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); // 流控效果:冷启动模式
rule.setWarmUpPeriodSec(10); // 预热时长

其中,CONTROL_BEHAVIOR_WARM_UP表示启用冷启动模式,warmUpPeriodSec代表期待系统进入稳定状态的时间(即预热时长)。

下图为将 QPS 设为480的冷启动示例图,可以看到在冷启动后系统是逐渐将 QPS 增加到480的:

冷启动示例图

3. 匀速排队

「匀速排队RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER」会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考流量控制 - 匀速器模式,具体的例子可以参见 PaceFlowDemo

漏桶算法

Leaky Bucket 对应 流量整形 中的匀速器。它的中心思想是,以固定的间隔时间让请求通过。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过(排队等待处理);若预期的通过时间超出最大排队时长,则直接拒接这个请求。

下面我们构建一个使用匀速排队规则的流控规则:

rule.setGrade(RuleConstant.GRADE_QPS);
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER); // 流控效果:匀速排队模式
rule.setCount(10);
rule.setMaxQueueingTimeMs(20 * 1000); // 最长排队等待时间:20s

count 设为 10 ,代表一秒匀速的通过 10 个请求,也就是每个请求平均间隔恒定为 1000 / 10 = 100 ms,每一个请求的最长等待时间maxQueueingTimeMs为 20 * 1000ms = 20s。

下图为 QPS 为2时,匀速排队规则的示例图:

匀速排队示例图

和之前的两种规则相比,「匀速排队」对于超过当前阈值的请求不会立刻丢弃,而是允许其保留maxQueueingTimeMs的最长等待时间。它主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求

基于调用关系的流量控制

「调用关系」包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关系,这里主要基于 strategylimitApp 两个属性完成基于调用关系的限流。

根据调用方限流

这种流控规则要求strategy 选择 DIRECT(strategy 默认值就是 DIRECT),以及根据限流规则中的 limitApp 字段根据调用方在不同的场景中进行流量控制

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

  • default

    表示「不区分调用者」,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。

  • 表示「针对特定的调用者」,只有来自这个调用者的请求才会进行流量控制。

    例如 NodeA 配置了一条针对调用者 caller1 的规则,那么当且仅当来自 caller1NodeA 的请求才会触发流量控制。

  • other

    表示「针对除 {some_origin_name} 以外的其余调用方」的流量进行流量控制。

    例如,资源 NodeA 配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1NodeA 的调用,都不能超过 other 这条规则定义的阈值。

使用和调用来源相关规则时,「调用方信息」通过 ContextUtil.enter(resourceName, origin) 方法中的 origin 参数传入。

同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default

根据调用链路入口限流:链路限流

NodeSelectorSlot 中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为「machine-root」的虚拟节点,调用链的入口都是这个虚节点的子节点。

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

     	          machine-root
                    /       \
                   /         \
             Entrance1     Entrance2
                /             \
               /               \
      DefaultNode(nodeA)   DefaultNode(nodeA)

上图中来自入口 Entrance1Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许根据特定入口的统计信息对资源限流。

比如我们可以设置 FlowRule.strategyRuleConstant.CHAIN,同时设置 FlowRule.ref_identityEntrance1 来表示只有从入口「Entrance1」的调用才会记录到 NodeA 的限流统计当中,而不关心经 Entrance2 到来的调用。

调用链的入口(上下文)是通过 API 方法 ContextUtil.enter(contextName) 定义的,其中 contextName 即对应调用链路入口名称。详情可以参考 ContextUtil 文档

具有关系的资源流量控制:关联流量控制

当两个资源之间具有「资源争抢」或者「依赖关系」的时候,这两个资源便具有了关联。

比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢

举例来说,「read_db」和「write_db」这两个资源分别代表数据库读写,我们可以给「read_db」设置限流规则来达到写优先的目的:设置 FlowRule.strategyRuleConstant.RELATE 同时设置 FlowRule.ref_identitywrite_db。这样当写库操作过于频繁时,读数据的请求会被限流。

可以注意到,上面根据 『strategy』属性的不同对流控采取不同的策略,下面我们对「strategy」属性进行以下概括:

strategy 属性

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

  • STRATEGY_DIRECT(默认属性)

    根据「调用方」进行限流。ContextUtil.enter(resourceName, origin) 方法中的 origin 参数标明了调用方的身份。

    如果 strategy 选择了DIRECT ,则还需要根据限流规则中的 limitApp 字段根据调用方在不同的场景中进行流量控制,包括有:「所有调用方default」、「特定调用方origin」、「除特定调用方origin之外的调用方」。

  • STRATEGY_RELATE

    根据「关联流量」限流。可使用关联限流来避免具有关联关系的资源之间过度的争抢。

    例如相关联的「a」操作和「b」操作,在「a」操作过于频繁时对「b」操作进行限流以避免两种操作之间的资源过度争抢,以保证「b」操作的优先执行。

  • STRATEGY_CHAIN

    根据「调用链路入口」限流。限定 Sentinel 允许根据某个特定入口的统计信息对资源进行限流。

最后放一张 Sentinel 的具体 FlowRule 表示图:

sentinel流量控制总结

熔断降级规则 (DegradeRule)

为什么要熔断降级

除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。

请求堆积

Sentinel 熔断降级原理

对于熔断降级,之前还有一个广泛使用的框架「Hystrix」。Sentinel 和 Hystrix 的原则是一致的: 当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个不稳定资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。

Sentinel 如何实现熔断降级

在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。

Hystrix 通过 线程池隔离 的方式,来对依赖(在 Sentinel 的概念中对应 资源)进行了隔离。这样做的「好处」是资源和资源之间做到了最彻底的隔离。「缺点」是除了增加了线程切换的成本(过多的线程池导致线程数目过多),还需要预先给各个资源做线程池大小的分配。

Sentinel 对这个问题采取了两种手段:

  • 通过并发线程数进行限制

    和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要预先分配线程池的大小。

    当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。

  • 通过响应时间对资源进行降级

    除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。

    当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。

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

Field 说明 默认值
resource 资源名,即限流规则的作用对象
count 阈值
grade 降级模式,根据 RT 降级还是根据异常比例降级 RT
timeWindow 降级的时间,单位为 s

当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

类似于流量控制规则,同一个资源也可以同时有多个降级规则。

降级判断标准

Sentinel 通过设置规则的grade属性,用以下几种方式来衡量资源是否处于稳定的状态:

1. 平均响应时间 DEGRADE_GRADE_RT

当 1s 内持续进入 5 个请求,且对应时刻的平均响应时间rt(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。

下面代码即是将平均响应时间设为 10ms 的一个示例代码:

rule.setResource(KEY);
// set threshold rt, 10 ms
rule.setCount(10);
rule.setGrade(RuleConstant.DEGRADE_GRADE_RT); // 设置降级模式为 rt
rule.setTimeWindow(10);

注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。

2. 异常比例 DEGRADE_GRADE_EXCEPTION_RATIO

当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。

异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。

3. 异常数 DEGRADE_GRADE_EXCEPTION_COUNT

当资源近 1 分钟的异常数目超过阈值之后会进行熔断。

由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。

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

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 的「开源整合模块」时,如 Sentinel Dubbo Adapter, Sentinel Web Servlet Filter 或 @SentinelResource 注解,它们能够自动统计业务异常,无需手动调用Tracer.trace(ex)

熔断降级示例 demo

RT Degrade demo 中,我们将 rt 的阈值设为 10ms,时间窗口长度设为 10s:

private static void initDegradeRule() {
    List<DegradeRule> rules = new ArrayList<DegradeRule>();
    DegradeRule rule = new DegradeRule();
    rule.setResource(KEY);
    // set threshold rt, 10 ms
    rule.setCount(10); // rt 阈值
    rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
    rule.setTimeWindow(10); // 时间窗口长度
    rules.add(rule);
    DegradeRuleManager.loadRules(rules);
}

模拟每个请求的实际 rt 为 600ms:

Entry entry = null;
try {
    TimeUnit.MILLISECONDS.sleep(5);
    entry = SphU.entry(KEY);
    // token acquired
    pass.incrementAndGet();
    // sleep 600 ms, as rt
    TimeUnit.MILLISECONDS.sleep(600);
} catch (Exception e) {
    block.incrementAndGet();
} finally {
    total.incrementAndGet();
    if (entry != null) {
        entry.exit();
    }
}

运行 Demo,将可以看到:

1529399827825,total:0, pass:0, block:0
1529399828825,total:4263, pass:100, block:4164  // 第一秒的平均RT都还比较小
1529399829825,total:19179, pass:4, block:19176
1529399830824,total:19806, pass:0, block:19806  // 开始被降级
1529399831825,total:19198, pass:0, block:19198  
1529399832824,total:19481, pass:0, block:19481
1529399833826,total:19241, pass:0, block:19241
1529399834826,total:17276, pass:0, block:17276
1529399835826,total:18722, pass:0, block:18722
1529399836826,total:19490, pass:0, block:19492
1529399837828,total:19355, pass:0, block:19355
1529399838827,total:11388, pass:0, block:11388
1529399839829,total:14494, pass:104, block:14390 // 10秒之后恢复,然而又迅速地被降级
1529399840854,total:18505, pass:0, block:18505
1529399841854,total:19673, pass:0, block:19676

系统保护规则 (SystemRule)

为什么要进行系统保护

当系统负载较高的时候,如果还持续让请求进入,可能会导致系统崩溃,无法响应。

在集群环境下,网络负载均衡会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,这个增加的流量就会导致这台机器也崩溃,最后导致整个集群不可用。

这样我们便知道了系统自适应限流的目的:

  • 保证系统不被拖垮
  • 在系统稳定的前提下,保持系统的吞吐量

我们希望让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

Sentinel 如何实现系统保护

长期以来,系统自适应保护的思路是根据硬指标,即系统的负载 (load1) 来做系统过载保护。当系统负载高于某个阈值,就禁止或者减少流量的进入;当 load 开始好转,则恢复流量的进入。

这个传统思路给我们带来了不可避免的两个问题:

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

如果我们还是按照固有的思维,超过特定的 load 就禁止流量进入,系统 load 恢复就放开流量,这样做的结果是无论我们怎么调参数,调比例,都是按照「果」来调节「因」,都无法取得良好的效果

『Sentinel 』在系统自适应保护的做法是,用 load1 作为启动控制流量的值,而允许通过的流量由处理请求的能力,即请求的响应时间以及当前系统正在处理的请求速率来决定

我们需要额外注意,这种系统自适应算法对于低 load 的请求,它的效果是一个「兜底」的角色。对于不是应用本身造成的 load 高的情况(如其它进程导致的不稳定的情况),效果不明显。

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

Field 说明 默认值
highestSystemLoad 最大的 load1,参考值(仅对 Linux/Unix-like 机器生效) -1 (不生效)
avgRt 所有入口流量的平均响应时间 -1 (不生效)
maxThread 入口流量的最大并发数 -1 (不生效)
qps 所有入口资源的 QPS -1 (不生效)

上述四个属性是 Sentinel 对于是否需要启动系统保护的四种触发标准,一个系统保护规则可以同时设置多种触发条件。系统防护启动之后 Sentinel 便会自动地对系统进行自适应防护。

系统保护原理

对于 Sentinel 的系统防护规则,四个属性都是是否启动的触发标准,因此我们需要充分理解四种属性的关联,才能够灵活使用 Sentinel 的系统自适应保护。

看下面这样的一张图:

Sentinel系统保护原理图

我们把系统处理请求的过程想象为一个水管,到来的请求是往这个水管灌水:当系统处理顺畅的时候,请求不需要排队,直接从水管中穿过,这个请求的RT是最短的;反之,当请求堆积的时候,那么处理请求的时间则会变为:排队时间 + 最短处理时间。

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

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

接下来的问题是,水管的水位是可以达到了一个平衡点,但是这个平衡点只能保证水管的水位不再继续增高,但是还面临一个问题,就是在达到平衡点之前,这个水管里已经堆积了多少水。如果之前水管的水已经在一个量级了,那么这个时候系统允许通过的水量可能只能缓慢通过,RT 会大,之前堆积在水管里的水会滞留;反之,如果之前的水管水位偏低,那么又会浪费了系统的处理能力。

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

上面的两个推论实际上就是 Sentinel 帮助我们做的事,可以看到其实只出现了三个属性,以 maxThread ≈ qps * avgRt 的联系出现。然而,对于 Sentinel,我们还需要用一个系统负载的值(load1)来激发这套机制启动,也就是第四个属性 highestSystemLod,我们需要注意这个属性仅在 Unix-like 系统上有效。

这里给出一个配置系统保护规则的示例代码:

private static void initSystemRule() {
    List<SystemRule> rules = new ArrayList<SystemRule>();
    SystemRule rule = new SystemRule();
    // max load is 3
    rule.setHighestSystemLoad(3.0);
    // max cpu usage is 60%
    rule.setHighestCpuUsage(0.6);
    // max avg rt of all request is 10 ms
    rule.setAvgRt(10);
    // max total qps is 20
    rule.setQps(20);
    // max parallel working thread is 10
    rule.setMaxThread(10);

    rules.add(rule);
    SystemRuleManager.loadRules(Collections.singletonList(rule));
}

黑白名单规则 (AuthorityRule)

很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的黑白名单控制的功能。

「黑白名单」根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。之前我们还接触了「流量控制规则」中类似的根据调用方限流,不过流控规则仅是作出限流,而黑白名单规则是直接限定资源能否通过,

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

  • resource:资源名,即限流规则的作用对象

  • limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB

  • strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式

黑白名单规则示例

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

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

和流控规则中一样,使用黑白名单规则时,「调用方信息」通过 ContextUtil.enter(resourceName, origin) 方法中的 origin 参数传入。

需要特别注意的是,如果对一个「AuthorityRuleManager」同时配置了多个黑白名单规则,只有第一个规则会生效

详细示例请参考 AuthorityDemo.

热点规则 (ParamFlowRule)

什么是热点限流

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

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制

  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

热点限流示意图

Sentinel 如何实现热点限流

Sentinel 利用「LRU 策略」统计最近最常访问的热点参数,结合「令牌桶算法」来进行参数级别的流控。

热点参数规则配置

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

属性 说明 默认值
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 集群流控相关配置

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

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-demo-parameter-flow-control

posted @ 2019-10-11 16:29  Bylight  阅读(1328)  评论(0编辑  收藏  举报