12-限流降级与流量效果控制器(下)

原创 吴就业 130 0 2020-09-22

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/11f00723dbfe438d8d6796bfc2339aff

作者:吴就业
链接:https://wujiuye.com/article/11f00723dbfe438d8d6796bfc2339aff
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

WarmUpController

Warm Up,冷启动。在应用升级重启时,应用自身需要一个预热的过程,预热之后才能到达一个稳定的性能状态,比如说,接口预热阶段完成JIT即时编译、完成一些单例对象的创建、线程池的创建、各种连接池的初始化、或者执行首次需要加锁执行的代码块。

冷启动并非只在应用重启时需要,在一段时间没有访问的情况下,连接池存在大量过期连接需要待下次使用才移除掉并创建新的连接、一些热点数据缓存过期需要重新查数据库写入缓存等,这些场景下也需要冷启动。

WarmUpController支持设置冷启动周期(冷启动时长),默认为10秒,WarmUpController在这10秒内会控制流量平缓的增长到限量阈值。例如,对某个接口限制QPS为200,10秒预热时间,那么这10秒内,相当于每秒的限流阈值分别为:5qps、15qps、35qps、70qps、90qps、115qps、145qps、170qps、190qps、200qps,当然,这组数据只是假设。

如果要使用WarmUpController,则限量规则阈值类型必须配置为GRADE_QPS,例如。

 FlowRule flowRule = new FlowRule();
 // 限流QPS阈值
 flowRule.setCount(200);
 // 流量控制效果配置为使用冷启动控制器
 flowRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
 // 冷启动周期,单位秒
 flowRule.setWarmUpPeriodSec(10); 
 flowRule.setResource("GET:/hello");
 FlowRuleManager.loadRules(Collections.singletonList(flowRule));

Sentinel冷启动限流算法参考了Guava的SmoothRateLimiter实现的冷启动限流算法,但实现上有很大的区别,Sentinel主要用于控制每秒的QPS,不会控制每个请求的间隔时间,只要满足每秒通过的QPS即可。正因为与Guava的不同,官方文档目前也没有很详细的介绍具体实现,单看源码很难揣摩作者的思路,加上笔者水平有限,没能切底理解Sentinel冷启动限流算法实现的细节,因此我们也不过深的去分析WarmUpController的源码,只是结合Guava的实现算法作个简单介绍。

Guava的SmoothRateLimiter基于Token Bucket算法实现冷启动。我们先看一张图,从而了解SmoothRateLimiter中的一些基础知识。

12-01-warmup01

在SmoothRateLimiter中,冷启动系数(coldFactor)的值固定为3,假设我们设置冷启动周期为10s、限流为每秒钟生成令牌数200个。那么warmupPeriod为10s,将1秒中内的微秒数除以每秒钟需要生产的令牌数计算出生产令牌的时间间隔(stableInterval)为5000μs,冷启动阶段最长的生产令牌的时间间隔(coldInterval)等于稳定速率下生产令牌的时间间隔(stableInterval)乘以3,即15000μs。

// stableIntervalMicros:stableInterval转为微秒
// permitsPerSecond: 每秒钟生成的令牌数上限为200
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;

由于coldFactor等于3,且coldInterval等于stableInterval乘以coldFactor,所以(coldInterval-stableInterval)是stableInterval的两倍,所以从thresholdPermits到0的时间是从maxPermits到thresholdPermits时间的一半,也就是warmupPeriod的一半。因为梯形的面积等于warmupPeriod,所以长方形面积是梯形面积的一半,长方形的面积是warmupPeriod / 2。

根据长方形的面积计算公式:面积 = 长 * 宽

可得:stableInterval * thresholdPermits = 12 * warmupPeriod

所以:thresholdPermits = 0.5 * warmupPeriod / stableInterval

// warmupPeriodMicros: warmupPeriod转为微秒
// stableIntervalMicros:stableInterval转为微秒
thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;

所以:thresholdPermits = 0.5 * 10s / 5000μs = 1000

由梯形面积公式:(上低+下低)* 高 / 2

可得:warmupPeriod = ((stableInterval + coldInterval) * (maxPermits-thresholdPermits)) / 2

推出:maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval)

// warmupPeriodMicros: warmupPeriod转为微秒
// stableIntervalMicros:stableInterval转为微秒
// coldIntervalMicros: coldInterval转为微秒
maxPermits = thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros);

所以:maxPermits = 1000 + 2.0 * 10s / (20000μs) = 2000

由直线的斜率计算公式:斜率 = (y2-y1) / (x2-x1)

可得:slope = (coldInterval - stableInterval) / (maxPermits - thresholdPermits)

所以:slope = 10000μs / 1000 = 10

正常情况下,令牌以稳定时间间隔stableInterval生产令牌,一秒钟内能生产的令牌就刚好是限流的阈值。

如果初始化令牌数为maxPermits时,系统直接进入冷启动阶段,此时生产令牌的间隔时间最长,等于coldInterval。如果此时以稳定的速率消费存储桶中的令牌,由于消费速度大于生产速度,那么令牌桶中的令牌将会慢慢减少,当storedPermits中的令牌数慢慢下降到thresholdPermits时,冷启动周期结束,将会以稳定的时间间隔stableInterval生产令牌。当消费速度等于生产速度,则稳定在限量阈值,而当消费速度远小于生产速度时,存储桶中的令牌数就会堆积,如果堆积的令牌数超过thresholdPermits,又会是一轮新的冷启动。

SmoothRateLimiter中,在每个请求获取令牌时根据当前时间与上一次获取令牌时间(nextFreeTicketMicros)的间隔时间计算需要生成新的令牌数并加入到令牌桶中。在应用重启时或者接口很久没有被访问后,nextFreeTicketMicros的值要么为0,要么远远小于当前时间,当前时间与nextFreeTicketMicros的间隔非常大,导致第一次生产令牌数就会达到maxPermits,所以就会进入冷启动阶段。

SmoothRateLimiter#resync方法源码如下。

// 该方法是被加锁同步调用的
void resync(long nowMicros) {
    // nextFreeTicket: 上次生产令牌的时间
    if (nowMicros > nextFreeTicketMicros) {
      // coolDownIntervalMicros的值为stableInterval
      // nowMicros - nextFreeTicketMicros: 当前时间与上次生产令牌的时间间隔
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
      // 存储桶的数量 = 桶中剩余的 + 新生产的, 与maxPermits取最小值
      storedPermits = min(maxPermits, storedPermits + newPermits);
      // 更新上次生产令牌的时间
      nextFreeTicketMicros = nowMicros;
    }
}

了解了Guava的SmoothRateLimiter实现后,我们再来看下Sentinel的WarmUpController。

public class WarmUpController implements TrafficShapingController {

    protected double count;
    private int coldFactor;
    protected int warningToken = 0;
    private int maxToken;
    protected double slope;

    protected AtomicLong storedTokens = new AtomicLong(0);
    protected AtomicLong lastFilledTime = new AtomicLong(0);
}

warningToken、maxToken、slope的计算可参考Guava的SmoothRateLimiter。

WarmUpController#canPass方法源码如下。

@Override
    public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        // 当前时间窗口通过的qps
        long passQps = (long) node.passQps();
        // 前一个时间窗口通过的qps
        long previousQps = (long) node.previousPassQps();
        // resync
        syncToken(previousQps);

        long restToken = storedTokens.get();
        // 如果令牌桶中存放的令牌数超过警戒线,则进入冷启动阶段,调整QPS。
        if (restToken >= warningToken) {
            // 超过thresholdPermits的当前令牌数
            long aboveToken = restToken - warningToken;
            double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
            // 小于warningQps才放行
            if (passQps + acquireCount <= warningQps) {
                return true;
            }
        } else {
            // 未超过警戒线的情况下按正常限流,如果放行当前请求之后会导致通过的QPS超过阈值则拦截当前请求,
            // 否则放行。
            if (passQps + acquireCount <= count) {
                return true;
            }
        }
        return false;
    }

canPass方法中,首先获取当前存储桶的令牌数,如果大于warningToken,则控制QPS。根据当前令牌桶中存储的令牌数量超出warningToken的令牌数计算当前秒需要控制的QPS的阈值,这两行代码是关键。

// restToken:当前令牌桶中的令牌数量
long aboveToken = restToken - warningToken;
// 1.0表示1秒
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));

我们看图理解这个公式。

12-01-warmup02

结合上图我们可以看出:

根据斜率和x1可计算出y1的值为:

y1 = slope * aboveToken

而1.0 / count计算出来的值是正常情况下每隔多少毫秒生产一个令牌,即stableInterval。

所以计算warningQps的公式等同于:

// 当前生产令牌的间隔时间:aboveToken * slope + stableInterval
// 1.0 / 生产令牌间隔时间 = 当前1秒所能生产的令牌数量
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + stableInterval));

当前生产令牌的间隔时间为:aboveToken * slope + stableInterval = stableInterval + y1;

当前每秒所能生产的令牌数为:1.0 / (stableInterval+y1)

所以warningQps就等于当前每秒所能生产的令牌数。

Sentinel中的resync与SmoothRateLimiter的resync方法不同,Sentinel每秒只生产一次令牌。WarmUpController的syncToken方法源码如下。

   // passQps:上一秒通过的QPS总数 
   protected void syncToken(long passQps) {
        long currentTime = TimeUtil.currentTimeMillis();
        // 去掉毫秒,取秒
        currentTime = currentTime - currentTime % 1000;
        long oldLastFillTime = lastFilledTime.get();
        // 控制每秒只更新一次
        if (currentTime <= oldLastFillTime) {
            return;
        }

        long oldValue = storedTokens.get();
        // 计算新的存储桶存储的令牌数
        long newValue = coolDownTokens(currentTime, passQps);
        if (storedTokens.compareAndSet(oldValue, newValue)) {
            // storedTokens扣减上个时间窗口的qps
            long currentValue = storedTokens.addAndGet(-passQps);
            if (currentValue < 0) {
                storedTokens.set(0L);
            }
            lastFilledTime.set(currentTime);
        }
    }

Sentinel并不是在每个请求通过时从桶中移除Token,而是每秒在更新存储桶的令牌数时,再扣除上一秒消耗的令牌数量,上一秒消耗的令牌数量等于上一秒通过的请求数,这就是官方文档所写的每秒会自动掉落令牌。减少每一次请求都使用CAS更新令牌桶的令牌数可以降低Sentinel对应用性能的影响,这是非常巧妙的做法。

更新令牌桶中的令牌总数=当前令牌桶中剩余的令牌总数+当前需要生成的令牌数(1秒时间可生产的令牌总数)。

coolDownTokens方法的源码如下。

   //  currentTime: 当前时间戳,单位为秒,但后面3位全为0
   //  passQps:上一秒通过的QPS
   private long coolDownTokens(long currentTime, long passQps) {
        long oldValue = storedTokens.get();
        long newValue = oldValue;
        // 添加令牌的判断前提条件: 当令牌的消耗远低于警戒线的时候
        if (oldValue < warningToken) {
            newValue = (long) (oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
        } else if (oldValue > warningToken) {
            // 上一秒通过的请求数少于限流阈值的 1/coldFactor 时
            if (passQps < (int) count / coldFactor) {
                newValue = (long) (oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
            }
        }
        return Math.min(newValue, maxToken);
    }

其中 (currentTime - lastFilledTime.get())为当前时间与上一次生产令牌时间的间隔时间,虽然单位为毫秒,但是已经去掉了毫秒的部分(毫秒部分全为0)。如果currentTime - lastFilledTime.get()等于1秒,根据1秒等于1000 毫秒,那么新生成的令牌数(newValue)等于限流阈值(count)。

newValue = oldValue + 1000 * count / 1000
         = oldValue + count

如果是很久没有访问的情况下,lastFilledTime远小于currentTime,那么第一次生产的令牌数量将等于maxToken。

参考文献:

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Spring Data R2DBC快速上手指南

本篇内容介绍如何使用r2dbc-mysql驱动程序包与mysql数据库建立连接、使用r2dbc-pool获取数据库连接、Spring-Data-R2DBC增删改查API、事务的使用,以及R2DBC Repository。

使用Spring WebFlux + R2DBC搭建消息推送服务

消息推送服务主要是处理同步给用户推送短信通知或是异步推送短信通知、微信模板消息通知等。本篇介绍如何使用Spring WebFlux + R2DBC搭建消息推送服务。

教你如何编写一个IDEA插件,并掌握核心知识点PSI

IDEA有着极强的扩展功能,它提供插件扩展支持,让开发者能够参与到IDEA生态建设中,为更多开发者提供便利、提高开发效率。我们常用的插件有Lombok、Mybatis插件,这些插件都大大提高了我们的开发效率。即便IDEA功能已经很强大,并且也已有很多的插件,但也不可能面面俱到,有时候我们需要自给自足。

Spring Boot实现加载自定义配置文件

本篇将介绍两种加载自定义配置文件的实现方式,并通过分析源码了解SpringBoot加载配置文件的流程,从而加深理解。

设计模式那些模糊不清的概念

23种设计模式属于结构型模式,而mvc模式等属于架构型模式。本篇要讨论的设计模式指的是结构型设计模式。

实现一个分布式调用链路追踪Java探针你可能会遇到的问题

Instrumentation之所以难驾驭,在于需要了解Java类加载机制以及字节码,一不小心就能遇到各种陌生的Exception。笔者在实现Java探针时就踩过不少坑,其中一类就是类加载相关的问题,也是本篇所要跟大家分享的。