关于Sentinel中冷启动限流原理WarmUpController

 更新时间:2023年04月26日 16:20:10   作者:@Kong  
这篇文章主要介绍了关于Sentinel中冷启动限流原理WarmUpController,具有很好的参考价值,希望对大家有所帮助。

冷启动

所谓冷启动,或预热是指,系统长时间处理低水平请求状态,当大量请求突然到来时,并非所有请求都放行,而是慢慢的增加请求,目的时防止大量请求冲垮应用,达到保护应用的目的。

Sentinel中冷启动是采用令牌桶算法实现。

令牌桶算法图例如下:

在这里插入图片描述

预热模型

Sentinel中的令牌桶算法,是参照Google Guava中的RateLimiter,在学习Sentinel中预热算法之前,先了解下整个预热模型,如下图:

在这里插入图片描述

Guava中预热是通过控制令牌的生成时间,而Sentinel中实现不同:

  • 不控制每个请求通过的时间间隔,而是控制每秒通过的请求数。
  • 在Guava中,冷却因子coldFactor固定为3,上图中②是①的两倍
  • Sentinel增加冷却因子coldFactor的作用,在Sentinel模型中,②是①的(coldFactor-1)倍,coldFactor默认为3,可以通过csp.sentinel.flow.cold.factor参数修改

原理分析

Sentinel中冷启动对应的FlowRule配置为RuleConstant.CONTROL_BEHAVIOR_WARM_UP,对应的Controller为WarmUpController,首先了解其中的属性和构造方法:

  • count:FlowRule中设定的阈值
  • warmUpPeriodSec:系统预热时间,代表上图中的②
  • coldFactor:冷却因子,默认为3,表示倍数,即系统最"冷"时(令牌桶饱和时),令牌生成时间间隔是正常情况下的多少倍
  • warningToken:预警值,表示进入预热或预热完毕
  • maxToken:最大可用token值,计算公式:warningToken+(2*时间*阈值)/(1+因子),默认情况下为warningToken的2倍
  • slope:斜度,(coldFactor-1)/count/(maxToken-warningToken),用于计算token生成的时间间隔,进而计算当前token生成速度,最终比较token生成速度与消费速度,决定是否限流
  • storedTokens:姑且可以理解为令牌桶中令牌的数量
public class WarmUpController implements TrafficShapingController {
	// FlowRule中设置的阈值
    protected double count;
    // 冷却因子,默认为3,通过SentinelConfig加载,可以修改
    private int coldFactor;
    // 预警token数量
    protected int warningToken = 0;
    // 最大token数量
    private int maxToken;
    // 斜率,用于计算当前生成token的时间间隔,即生成速率
    protected double slope;
	// 令牌桶中剩余令牌数
    protected AtomicLong storedTokens = new AtomicLong(0);
    // 最后一次添加令牌的时间戳
    protected AtomicLong lastFilledTime = new AtomicLong(0);
    public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {
        construct(count, warmUpPeriodInSec, coldFactor);
    }
    public WarmUpController(double count, int warmUpPeriodInSec) {
        construct(count, warmUpPeriodInSec, 3);
    }
    private void construct(double count, int warmUpPeriodInSec, int coldFactor) {
        if (coldFactor <= 1) {
            throw new IllegalArgumentException("Cold factor should be larger than 1");
        }
        this.count = count;
		// 默认为3
        this.coldFactor = coldFactor;
        // thresholdPermits = 0.5 * warmupPeriod / stableInterval.
        // warningToken = 100;
        // 计算预警token数量
        // 例如 count=5,warmUpPeriodInSec=10,coldFactor=3,则waringToken=5*10/2=25
        warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
        // / maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval)
        // maxToken = 200
        // 最大token数量=25+2*10*5/4=50
        maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
        // slope
        // slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits- thresholdPermits);
        // 倾斜度=(3-1)/5/(50-25) = 0.016
        slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
    }
}

举例说明:

FlowRule设定阈值count=5,即1s内QPS阈值为5,设置的预热时间默认为10s,即warmUpPeriodSec=10,冷却因子coldFactor默认为3,即count = 5,coldFactor=3,warmUpPeriodSec=10,则

stableInterval=1/count=200ms,coldInterval=coldFactor*stableInterval=600ms
warningToken=warmUpPeriodSec/(coldFactor-1)/stableInterval=(warmUpPeriodSec*count)/(coldFactor-1)=25
maxToken=2warmUpPeriodSec/(stableInterval+coldInterval)+warningToken=warningToken+2warmUpPeriodSeccount/(coldFactor+1)=50
slope=(coldInterval-stableInterval)/(maxToken-warningToken)=(coldFactor-1)/count/(maxToken-warningToken)=0.016

接下来学习,WarmUpController是如何进行限流的,进入canPass()方法:

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    // 获取当前1s的QPS
    long passQps = (long) node.passQps();
    // 获取上一窗口通过的qps
    long previousQps = (long) node.previousPassQps();
    // 生成和滑落token
    syncToken(previousQps);
    // 如果进入了警戒线,开始调整他的qps
    long restToken = storedTokens.get();
    // 如果令牌桶中的token数量大于警戒值,说明还未预热结束,需要判断token的生成速度和消费速度
    if (restToken >= warningToken) {
        long aboveToken = restToken - warningToken;
        // 消耗的速度要比warning快,但是要比慢
        // y轴,当前token生成时间 current interval = restToken*slope+stableInterval
        // 计算此时1s内能够生成token的数量
        double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
        // 判断token消费速度是否小于生成速度,如果是则正常请求,否则限流
        if (passQps + acquireCount <= warningQps) {
            return true;
        }
    } else {
        // 预热结束,直接判断是否超过设置的阈值
        if (passQps + acquireCount <= count) {
            return true;
        }
    }

    return false;
}

canPass()方法分为3个阶段:

syncToken():负责令牌的生产和滑落

判断令牌桶中剩余令牌数

  • 如果剩余令牌数大于警戒值,说明处于预热阶段,需要比较令牌的生产速率与令牌的消耗速率。若消耗速率大,则限流;否则请求正常通行

仍然以count=5进行举例,警戒线warningToken=25,maxToken=50

假设令牌桶中剩余令牌数storedTokens=30,即在预热范围内,此时restToken=30,slope=0.016,则aboveToken=30-25=5

由斜率slope推导当前token生成时间间隔:(restToken-warningToken)*slope+stableInterval=5*0.016+1/5=0.28,即280ms生成一个token

此时1s内生成token的数量=1/0.28≈4,即1s内生成4个token

假设当前窗口通过的请求数量passQps=4,acquiredCount=1,此时passQps+acquiredCount=5>4,即令牌消耗速度大于生产速度,则限流

  • 如果剩余令牌数小于警戒值,说明系统已经处于高水位,请求稳定,则直接判断QPS与阈值,超过阈值则限流

接下来分析Sentinel是如何生产及滑落token的,进入到syncToken()方法:

获取当前时间秒数currentTime,与lastFilledTime进行比较,之所以取秒数,是因为时间窗口的设定为1s,若两个时间相等,说明还处于同一秒内,不进行token填充和滑落,避免重复问题

令牌桶中添加token

  • 当流量极大,令牌桶中剩余token远低于预警值时,添加token
  • 处于预热节点,单令牌的消耗速度小于系统最冷时令牌的生成速度,则添加令牌

通过CAS操作,修改storedToken,并进行令牌扣减

protected void syncToken(long passQps) {
    long currentTime = TimeUtil.currentTimeMillis();
    // 获取整秒数
    currentTime = currentTime - currentTime % 1000;
    // 上一次的操作时间
    long oldLastFillTime = lastFilledTime.get();
    // 判断成立,如果小于,说明可能出现了时钟回拨
    // 如果等于,说明当前请求都处于同一秒内,则不进行token添加和滑落操作,避免的重复扣减
    // 时间窗口的跨度为1s
    if (currentTime <= oldLastFillTime) {
        return;
    }
    // token数量
    long oldValue = storedTokens.get();
    long newValue = coolDownTokens(currentTime, passQps);
    // 重置token数量
    if (storedTokens.compareAndSet(oldValue, newValue)) {
        // token滑落,即token消费
        // 减去上一个时间窗口的通过请求数
        long currentValue = storedTokens.addAndGet(0 - passQps);
        if (currentValue < 0) {
            storedTokens.set(0L);
        }
        // 设置最后添加令牌时间
        lastFilledTime.set(currentTime);
    }

}
private long coolDownTokens(long currentTime, long passQps) {
    long oldValue = storedTokens.get();
    long newValue = oldValue;

    // 添加令牌的判断前提条件:
    // 当令牌的消耗程度远远低于警戒线的时候
    if (oldValue < warningToken) {
        // 计算过去一段时间内,可以通过的QPS总量
        // 初始加载时,令牌数量达到maxToken
        newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
    } else if (oldValue > warningToken) {
        // 处于预热过程,且消费速度低于冷却速度,则补充令牌
        if (passQps < (int)count / coldFactor) {
            newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
        }
    }
    // 当令牌桶满了之后,抛弃多余的令牌
    return Math.min(newValue, maxToken);
}

总结

Sentinel采用令牌桶算法实现预热限流

系统流量突增,令牌消耗从maxPermits(令牌桶容量)到thresholdPermits(警戒线)所需要的时间,是从警戒线到0的(coldFactor-1)倍,并非其他博客中的2倍。另外,关于预热模型中②和①的关系,是通过结果反推而来,并没有找到模型定义的官方文档。

Sentinel限流是针对某时刻令牌的生成与消耗速度

Sentinel通过比较整秒数,来判断是否需要进行令牌扣减,并通过CAS操作,保证同一时刻只能由1个线程成功操作,从而避免多次扣减passQps导致限流失效的问题

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:

相关文章

  • Java整合Redis实现坐标附近查询功能

    Java整合Redis实现坐标附近查询功能

    这篇文章主要介绍了Java整合Redis实现坐标附近查询,我们可以在redis服务器使用命令 help xxx 查看指令的具体用法,本文给大家介绍的非常详细,感兴趣的朋友一起看看吧
    2023-11-11
  • Javaweb使用Maven工具与Tomcat的方法详解

    Javaweb使用Maven工具与Tomcat的方法详解

    这篇文章主要为大家详细介绍了Javaweb使用Maven工具与Tomcat的方法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-03-03
  • java工具类SendEmailUtil实现发送邮件

    java工具类SendEmailUtil实现发送邮件

    这篇文章主要为大家详细介绍了java工具类SendEmailUtil实现发送邮件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-02-02
  • JVM(Java Virtual Machine,Java虚拟机)的作用详解

    JVM(Java Virtual Machine,Java虚拟机)的作用详解

    JVM是Java语言实现“一次编写,到处运行”特性的基石,也是Java平台的核心组成部分,其主要作用包括平台无关性、内存管理、运行Java程序、安全性以及性能优化,通过这些功能,JVM确保了Java程序的可移植性、高效性和安全性
    2025-03-03
  • SpringBoot中Json工具类的实现

    SpringBoot中Json工具类的实现

    本文介绍在Java项目中实现一个JSON工具类,支持对象与JSON字符串之间的转换,并提供依赖和代码示例便于直接应用,感兴趣的可以了解一下
    2024-10-10
  • Spring Boot中@Conditional注解介绍

    Spring Boot中@Conditional注解介绍

    @Conditional表示仅当所有指定条件都匹配时,组件才有资格注册。该@Conditional注释可以在以下任一方式使用:作为任何@Bean方法的方法级注释、作为任何类的直接或间接注释的类型级别注释@Component,包括@Configuration类、作为元注释,目的是组成自定义构造型注释
    2022-09-09
  • ConcurrentModificationException日志关键字报警思考分析

    ConcurrentModificationException日志关键字报警思考分析

    本文将记录和分析日志中的ConcurrentModificationException关键字报警,还有一些我的思考,有需要的朋友可以借鉴参考下,希望能够有所帮助
    2023-12-12
  • MyBatis Plus实现一对多的查询场景的三种方法

    MyBatis Plus实现一对多的查询场景的三种方法

    MyBatis Plus提供了多种简便的方式来进行一对多子查询,本文主要介绍了MyBatis Plus实现一对多的查询场景的三种方法,具有一定的参考价值,感兴趣的可以了解一下
    2024-07-07
  • Java获取Cookie里的指定值的实现方法

    Java获取Cookie里的指定值的实现方法

    在Java中,我们经常需要从HTTP请求中获取Cookie,并从中提取特定的值,下面我们将介绍如何通过Java代码获取Cookie中的指定值,文章通过代码示例介绍的非常详细,需要的朋友可以参考下
    2024-09-09
  • SpringBoot中Bean生命周期自定义初始化和销毁方法详解

    SpringBoot中Bean生命周期自定义初始化和销毁方法详解

    这篇文章给大家详细介绍了SpringBoot中Bean生命周期自定义初始化和销毁方法,文中通过代码示例讲解的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-01-01

最新评论