Sentinel(二)--限流详解

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

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

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

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

  • count : 限流阈值

  • grade : 限流阈值类型(QPS 或并发线程数)

  • limitApp : 流控针对的调用来源,若为 default 则不区分调用来源

  • strategy : 限流策略

  • controlBehavior : 流量控制效果(直接拒绝、Warm Up、匀速排队)

除了资源与规则以外,还有一个很重要的角色,就是根据什么纬度来实现规则判断?

Sentinel中提供了两个纬度:

  1. 并发线程数

  2. QPS

也就是说,我们可以选择根据不同的纬度,根据这些纬度的指标去匹配限流规则,一旦达到阈值,则直接触发流量控制。

默认情况下是根据QPS来限流的,这个属性是通过grade进行设置。

并发线程数控制

并发数控制用于保护业务线程池不被慢调用耗尽。

例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。

为了应对太多线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。

这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的overhead(开销)比较大,特别是对低延时的调用有比较大的影响。

Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。

并发线程数控制通常在调用端进行配置。

并发线程数控制参数配置:

  1. grade: RuleConstant.FLOW_GRADE_THREAD

  2. count: 此时它的含义是并发线程数量

QPS流量控制

当 QPS 超过某个阈值的时候,则采取措施进行流量控制行为(类似于我们前面说过的限流算法上的差异),Sentinel提供了四种流量控制行为

  1. 直接拒绝(CONTROL_BEHAVIOR_DEFAULT)

  2. Warm Up(CONTROL_BEHAVIOR_WARM_UP)

  3. 匀速排队(CONTROL_BEHAVIOR_RATE_LIMITER,漏桶算法 )

  4. 冷启动+匀速器(CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER),除了让流量缓慢增加,还控制的了请求的间隔时间,让请求已均匀速度通过。这种策略是1.4.0版本新增的。

这四个行为,是通过FlowRule中的controlBehavior属性来控制,默认是直接拒绝

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

Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。

通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

以下都会随着系统访问量增加逐步预热来提升性能的因素。

  • 缓存预热

  • 数据库连接池初始化

如下图所示,当前系统所能够处理的最大并发数是480,首先在最下面的标记位置,系统一直处于空闲状态,接着请求量突然直线升高,这个时候系统并不是直接将QPS拉到最大值,而是在一定时间内逐步增加阈值,而中间这段时间就是一个系统逐步预热的过程。

1666491976791

属性设置:

  • controlBehavior: RuleConstant.CONTROL_BEHAVIOR_WARM_UP

  • warmUpPeriodSec:预热时间,默认60s。

案例演示:

public class FlowRuleInitFunc implements InitFunc {
    @Override
    public void init() throws Exception {
        List<FlowRule> rules=new ArrayList<>();
        FlowRule rule=new FlowRule();
        rule.setResource("doTest");
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); //行为:warmup
        rule.setWarmUpPeriodSec(60); //warmup时间 60s
        rule.setCount(1000); //并发数量1000
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
    }
}

Sentinel-Dashboard中的展示结果如下,看图绿色部分,当达到60s时,qps达到1000。

1666493188223

匀速排队

匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,其实对应的是漏桶算法。

当请求数量远远大于阈值时,这些请求会排队等待,这个等待时间可以设置,如果超过等待时间,那这个请求会被拒绝。

如下图所示,假设qps=5,表示请求每200ms才能通过1个,多处的请求排队等待,超时时间达标最大排队时间,超过最大排队时间则直接拒绝。

1666493689768

属性设置:

  • controlBehavior: RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER

  • maxQueueingTimeMs:排队等待时间,表示每一次请求最长等待时间,默认是500ms

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

所以,这里等待时间大一点, 可以保证让所有请求都能正常通过; 假设这里设置的排队等待时间过小的话, 导致排队等待的请求超时而抛出异常BlockException, 最终结果可能是这100个并发请求中只有一个请求或几个才能正常通过, 所以使用这种模式得根据访问资源的耗时时间决定排队等待时间。

基于调用关系的流量控制

在分布式架构中,一个请求会包含调用方和被调用方,Sentinel还提供了服务调用关系的流量控制策略,所谓的调用关系,就是根据不同的调用纬度来触发流量控制。

  1. 根据调用方限流(STRATEGY_DIRECT)

  2. 根据调用链路入口限流(STRATEGY_CHAIN)

  3. 具有关系的资源流量控制(STRATEGY_RELATE)

根据调用方限流

顾名思义,假设有两个服务分别是A和B,都向某一个服务C发起请求调用,这个时候我们希望对来自服务B的请求进行限流,那就可以采用调用方限流策略,具体配置如下:

  1. 设置FlowRule的strategy为STRATEGY_DIRECT

  2. 设置FlowRule的LimitApp,表示指定调用方,这个字段有三种选项

  • default,表示不区分调用者,任何调用者的请求都会进行流量统计。

  • ${some_origin_name},针对某个特定的调用者,只有这个调用者的请求才会进行流量控制

  • other,表示针对除了${some_origin_name}以外的其他调用方的流量进行流量控制。 假设资源NodeA配置了一条针对调用者caller1的限流规则,接着又配置了一条调用者为other的规则,那么任意来自非caller1的对NodeA的调用,请求并发数都不能超过other这条规则定义的阈值。

public class OtherRuleExample {

    // 如果ContextUtil.enter中的origin设置为`caller11`,表示是非`caller`调用,则按照limitApp=other的匹配规则进行流量控制,也就是QPS=3
    // 如果ContextUtil.enter中的origin设置为`caller`, 表示当前是`caller`的调用,则按照limitApp=caller的匹配规则进行流量控制,也就是QPS=4
    private static void init() {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        rule.setResource("hello");
        rule.setCount(4);
        rule.setLimitApp("caller"); //配置caller为调用者
        rules.add(rule);
        FlowRule rule1 = new FlowRule();
        rule1.setResource("hello");
        rule1.setLimitApp("other"); //配置other
        rule1.setCount(3);
        rules.add(rule1);
        FlowRuleManager.loadRules(rules);
    }

    public static void main(String[] args) {
        init();
        for (int i = 0; i < 20; i++) {
            ContextUtil.enter("context", "caller11");
            //声明当前的调用方的应用名称, 通过origin参数
            try (Entry entry = SphU.entry("hello")) {
                System.out.println("访问成功");
            } catch (BlockException e) {
                System.out.println("被限流了");
            }
        }
    }
}

根据调用链路入口限流

一个被限流的保护方法,可能来自于不同的调用链路,比如针对资源NodeA,入口Entrance1 和 Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许只根据某个入口的统计信息对资源限流。

1666494462237

设置方式:

  1. 设置FlowRule中的strategy=STRATEGY_CHAIN

  2. 设置FlowRule中的refResource为Entrance1来表示只有从入口Entrance1的调用才会进行流量控制

具有关系的资源流量控制

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

比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。

如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。

可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_db 和write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:

设置方式:

  1. 设置FlowRule中的strategy=STRATEGY_RELATE

  2. 设置FlowRule中的refResource为write_db表示设置关联资源

通过这样的设置后,如果write_db资源超过阈值时,就会对read_db资源进行限流。

总结

在这个阶段,我们跟进一步了解了Sentinel限流规则中的各种限流纬度,也让我们对Sentinel有了更深刻的理解。

不管是基于并发线程数、还是QPS、还是根据调用链路等纬度,本质上都是当前系统所关注的资源保护指标,最终意义是让系统平稳运行!

posted @ 2022-10-23 11:23  snail灬  阅读(686)  评论(0编辑  收藏  举报