美文网首页Spring Cloud
Guava RateLimiter代码梳理(2)预热限流器算法分

Guava RateLimiter代码梳理(2)预热限流器算法分

作者: ro9er | 来源:发表于2018-11-13 12:14 被阅读11次

    前言

    上文针对RateLimiter的基本实现做了一个代码分析,本文将对Guava中另一个RateLimiter实现:带预热功能的SmoothWarmingUp RateLimiter算法和代码做一个简单的分析。使用带预热的限流器的需求是很明显的,如果突发的大量流量到来,在没有预热的情况下,大量请求会打到后台的服务,如果后台服务的缓存陈旧,db和io操作耗时,就有可能会拖垮后台服务,如果有一个预热阶段的话就能够将流量比较平滑的过渡,从而降低后台服务down掉的风险。

    变量关系

    (这里基本上是翻译的Guava RateLimiter的注释)
    我们首先来看一张图:


    算法示意图

    有几个变量需要说明:

    • stableInterval 这个跟之前的文章描述的有点不同,这里考虑是放行令牌桶令牌的速率,类似于漏桶算法
    • coldInterval 这个是在冷却情况下发放令牌的速率,目前取值为:coldInterval = coldFactor * stableInterval
    • thresholdPermits 当令牌桶中存储的令牌数目到达阈值时,发放令牌的速率到达稳定
    • maxPermits 令牌桶中存放令牌的最大数目
    • warmUpPeriod 预热的时间长度,在这段预热期内令牌桶的存放数目从maxPermits减少到了thresholdPermits

    首先我们来明确几个基本点:

    • 限流器的状态(storedPermits)可以视为图中的水平线
    • 当限流器没有流量进来,限流器的状态向右变化(最大到maxPermits)
    • 当限流器发挥功能,它的状态向左变化(最少到0),因为我们本身令牌桶中有storedPermits,我们首先会通过它来获取令牌
    • 如果限流器空闲,那么它将会以一个恒定速率向右变化,变化的速率是maxPermits/warmupPeriod,这样保证了限流器令牌数目从0到maxPermits一共用了warmupPeriod的时长
    • 如果限流器使用中,取得K个令牌的耗时,等于我们上图所示的函数在X个令牌到X-K个令牌之间积分(不要害怕)。举个例子,我们从maxPermits到了thresholdPermits用的时间就是这两个横坐标之间的积分,也就是上图中的梯形区域,这块区域本身就等于warmupPeriod
    • 从thresholdPermits到0所需要的时间时warmupPeriod/2(这里我确实没懂,原话是:'The reason that this is warmupPeriod/2 is to maintain the behavior of the original implementation where coldFactor was hard coded as 3'。我真心推不出来,大家可以参考这里Google Groups 讨论

    在上面几点的基础上,我们就可以建立各个变量之间的关系了:

    • 已知量:我们创建限流器时预设stableInterval 和warmupPeriod
    • 由thresholdPermits * stableInterval = warmupPeriod / 2推出:thresholdPermits = 0.5 * warmupPeriod / stableInterval
    • 由warmupPeriod = (stableInterval + coldInterval) * (maxPermits - thresholdPermits) / 2 推出(这里就是通过上底加下底乘高除2来得到梯形面积):maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval)

    代码分析

    说了这么多,我们再看代码:

         //  初始化设置限流器
        @Override
        void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
          double oldMaxPermits = maxPermits;
          //跟之前部分描述的计算一致
          double coldIntervalMicros = stableIntervalMicros * coldFactor;
          thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;
          maxPermits =
              thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros);
          //slop可以看作是梯形斜边的斜率,用于计算threshold到maxPermits之间的限流速率
          slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);
          if (oldMaxPermits == Double.POSITIVE_INFINITY) {
            // if we don't special-case this, we would get storedPermits == NaN, below
            storedPermits = 0.0;
          } else {
            storedPermits =
                (oldMaxPermits == 0.0)
                    ? maxPermits // initial state is cold
                    : storedPermits * maxPermits / oldMaxPermits;
          }
        }
    

    初始化的代码跟之前我们描述的计算基本一致。
    我们记得之前的文章说过,再调用acquire之后会先调用resync方法去更新桶中的令牌数量:

      void resync(long nowMicros) {
        // if nextFreeTicket is in the past, resync to now
        if (nowMicros > nextFreeTicketMicros) {
          storedPermits = min(maxPermits,
              storedPermits
                + (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros());
          nextFreeTicketMicros = nowMicros;
        }
      }
    

    在不预热的版本中,coolDownIntervalMicros函数稳定返回stableInterval:

        double coolDownIntervalMicros() {
          return stableIntervalMicros;
        }
    

    而在预热版本中,根据我们之前分析,限流器状态往右运动到maxPermits的速率恒定为maxPermits/warmupPeriod,因此这里
    coolDownIntervalMicros函数的实现就应该是:

        double coolDownIntervalMicros() {
          return warmupPeriodMicros / maxPermits;
        }
    

    然后我们再来看看计算等待时间的函数:

    final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
        resync(nowMicros);
        long returnValue = nextFreeTicketMicros;
        double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
        double freshPermits = requiredPermits - storedPermitsToSpend;
        long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
            + (long) (freshPermits * stableIntervalMicros);
    
        try {
          this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros);
        } catch (ArithmeticException e) {
          this.nextFreeTicketMicros = Long.MAX_VALUE;
        }
        this.storedPermits -= storedPermitsToSpend;
        return returnValue;
      }
    

    这里基本的功能流程和描述我在之前的文章也已经说明了,这里也不再赘述,大家可以看到真正有变化的就是在这一行

    long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
    

    这一行计算了消费令牌桶中storedPermits需要的时间,我们知道再不预热的版本中这里是直接返回了0,那么在预热的版本中应该是怎样的呢?

        long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
          //在阈值以上可用的令牌数目
          double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
          long micros = 0;
          // measuring the integral on the right part of the function (the climbing line)
          //计算右边梯形区域的积分(面积)
          if (availablePermitsAboveThreshold > 0.0) {
            //计算在阈值以上能拿的令牌数目
            double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);
            //梯形高:permitsAboveThresholdToTake
            //梯形下底:permitsToTime(availablePermitsAboveThreshold)
            //梯形上帝:permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake)
            micros = (long) (permitsAboveThresholdToTake
                * (permitsToTime(availablePermitsAboveThreshold)
                + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake)) / 2.0);
            permitsToTake -= permitsAboveThresholdToTake;
          }
          // measuring the integral on the left part of the function (the horizontal line)
          // 加上示意图左边的矩形区域,即放行速率稳定在stableInterval
          micros += (stableIntervalMicros * permitsToTake);
          return micros;
        }
        //根据slop和stableIntervalMicros计算放行的时间
        //可以想象计算出的结果为梯形斜边上的某个点
        private double permitsToTime(double permits) {
          return stableIntervalMicros + permits * slope;
        }
    

    上面代码计算出了获取storedPermits需要的时间长度。可能代码和注释大家看起来比较模糊,我这里也用图来说明:


    storedPermits获取时间计算

    测试

    测试代码如下:

    /**
     * @author: Yuanqing Luo
     * @date: 2018/11/13
     **/
    public class GuavaWarmupDemo {
    
        public static void main(String[] args){
            RateLimiter limiter = RateLimiter.create(5, 2, TimeUnit.SECONDS);
            ExecutorService executorService = Executors.newFixedThreadPool(2);
            final long start = System.currentTimeMillis();
            for(int i=2;i>0;i--){
                executorService.submit(() -> {
                    int round = 5;
                    while(round-- > 0){
                        limiter.acquire();
                        try {
                            Thread.sleep(300);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("time:" + (System.currentTimeMillis() - start));
                    }
    
                });
            }
    
        }
    }
    

    得到的结果

    time:411
    time:964
    time:1454
    time:1841
    time:2162
    time:2399
    time:2615
    time:2801
    time:3000
    time:3200
    

    可以看到,最开始获取令牌的时间在450ms左右,此时属于预热阶段,到第5次之后慢慢稳定到了200ms,这与我们在之前设置的QPS=5是一致的。证实了这个算法的有效性

    结语

    这篇文章作为之前Guava RateLimiter的补充,解释了Guava中预热的限流器的算法与工作方式。希望大家喜欢!

    相关文章

      网友评论

        本文标题:Guava RateLimiter代码梳理(2)预热限流器算法分

        本文链接:https://www.haomeiwen.com/subject/fndkfqtx.html