RateLimiter解析

RateLimiter解析

0. 写在最前(最后)的总结

RateLimter实现了令牌桶的限流算法,从头看到尾后,发现内容还是比较简单的,可以举个例子来快速理解。

假设我们希望每秒最多发送5个请求,那么相当于每0.2秒发送一个。
- 当第一个请求发送后,记为开始,即0秒。
- 0.1秒时,来了第二个请求,这时候还没到第0.2秒(可以认为当前令牌数为0),那么我们不需要考虑别的,等到0.2秒就能执行,但是,必须知道,这时候,下次允许执行的请求时间应该是第0.4秒了。
- 假如第二个请求执行完后,到2.0秒还是没有新的请求到来,那么我们可以理解0.4秒到2秒的空闲时间就保存了5个令牌(最多不能超过5个,而且这个计算实际上是在新请求到来的时刻计算出来的)。
- 当2.1秒时,有新请求进来了
如果它需要消耗3个请求,那么它可以立刻执行,然后存储令牌就变成了2个,而且如果此时再有新请求进来,可以立刻执行(smoothbursty),或者等待一个消耗3个存储令牌的积分时间执行(smoothwarmingup)。
如果它需要消耗9个请求,那么它也可以立刻执行,然后存储令牌清空,而且如果此时再有新请求进来,需要等待4个令牌的时间0.8秒(smoothbursty),或者等待一个消耗5个存储令牌的积分时间执行加上等待4个令牌的时间(smoothwarmingup)。

需要注意的是,桶里的令牌只有在空闲时才会增加,如果正常每0.2秒来一个请求,那么桶里的令牌一直为0。
空闲时的令牌最大可存储的数量5(smoothbursty),或者根据warmupPeriod调整(smoothwarmingup)。

1. SmoothRateLimiter的设计哲学

RateLimiter是怎么设计的,为什么这样设计?

RateLimiter的主要特性是它的“稳定速率”,表示正常情况下允许的最大速率。这是根据“节流”输入请求来强制执行的,例如,对于一个请求而言,计算合适的节流时间,然后让改线程等待相应的时间。

最简单的维持QPS速率的方法是保持上次请求的时间戳,并且保证(1/QPS)秒的间隔。比如说,当QPS=5时,我们需要确保不会在上个请求的200ms内处理新的请求,这样就能维持想要的速率。如果一个新的请求到来,而上一个请求仅在100ms前,我们就需要再等待100ms。按照这样的速度,服务15个许可证(例如一个aquire(15)的请求)自然需要3秒钟。

对于这样一个RateLimiter,我们需要认识到它对过去有很肤浅的记忆:它只记得最后一个请求。如果这个RateLimiter有很长一段时间没有使用了,然后突然来了一个请求,那么它马上就会被执行。这个RateLimiter忘记了之前的“不饱和”状态。这个结果会导致“不饱和”或者溢出,取决于不使用期望速率的真实后果。

一方面,过去的“不饱和”可能意味着过剩的可用资源。然后,这个RateLimiter需要一定的时间提速,来充分利用这些资源。这一点很重要,这个在网络带宽受限的时候,过去的不饱和通常意味着“几乎空的缓存”,会被立即填满。

另一方面,过去的不饱和可能意味着,负责处理请求的服务器可能没有做好准备来处理之后的请求。比如,它的cache变得陈旧,并且请求可能触发更加昂贵的操作(一个极端的例子是一个服务器可能刚刚重启,它可能正在忙于处理自身的事情)。

为了解决这个问题,我们引入了一个额外的方面,对“过去的不饱和”状态,用“storePermits”(翻译成:存储许可,其实就是令牌)变量来建模。当没有不饱和时,这个变量为0,当有了非常大的不饱和情况时,它能够增长到最大允许存储许可数。所以,一个请求需要许可时,派发的许可来源于两方面:存储的许可(如果有的话),和新产生的许可

它是如何生效的呢?
对于一个RateLimiter来说,它每秒产生一个token,随着RateLimiter闲置,存储许可每秒会增长1。也就是说,Ratelimiter闲着10秒,存储许可就达到10了(假设最大允许许可数≥10)。在这种情况下,一个请求acquire(3)到达。我们使用存储许可来为这个请求服务,然后存储许可降低到7。紧接着,假设一个acquire(10)的请求到来,我们使用所有的7个存储许可,然后剩下的3个许可,我们通过Ratelimiter产生新的许可。
我们知道,如果Ratelimiter产生许可的速率是每秒一个token的话,那就需要三秒钟。那么,那7个存储许可怎么利用呢?正如上面解释的,没有唯一的答案。如果我们的关注点在处理不饱和,那么我们就希望存储许可被利用得比新产生许开要快,因为不饱和程度意味着可被利用的空闲资源;如果我们主要关注的是处理溢出,那么存储许开就要被利用得比新产生许可要慢。因此,针对不同情况,我们需要一个函数来表示存储许可与空闲时间的关系。

这个角色由storedPermitsToWaitTime(double storedPermits, double permitsToTake)来扮演。基础模型是一个连续函数,映射了存储许可(从0到最大允许存储许可)在 1/Rate 上的有效存储许可。“存储许可”基本上能够度量出未使用时间。Rate是“permits/time”,所以“1/rate”就是“time/permits”,所以和permit数量相乘就表示了时间 。例如,在这个函数上做积分(就是 storedPermitsToWaitTime ()的计算方式),对于给定数量的请求许可,等价于后续请求的最小间隔时间。这个在SmoothBursty和SmoothWarmingUp是不同的实现,SmoothBursty直接返回无需等待,SmoothWarmingUp则要计数出等待时间。

这里有个例子。如果storedPermits==10,然后我们需要3个permits,我们从存储许可中拿走3个,还剩7个。那么对于这消耗3个许可的时间,就需要调用storedPermitsToWaitTime(storedPermits = 10.0, permitsToTake = 3.0)进行计算节流时间,就是对7到10进行积分计算。
使用积分保证了一个单独的acquire(3)等价于三个acquire(1),或者一个acquire(2)和一个acquire(1),不管使用怎样的积分函数。
需要注意的是,对于这个积分函数,如果我们选择一个水平线,高度正好等于 1/QPS,那么这个函数就没啥作用了,因为它表示存储许可和新产生许可的速率是一致的。
如果我们选择一个积分函数低于这个水平线,它表示我们减少了积分面积,也就是时间。因此,Ratelimiter在一段空闲后变得更加快速了。反之,如果我们选择一个积分函数高于这个水平线,那就意味着面积(时间)增大了,因此存储许可就比新产生许可要更加耗时,也就是Ratelimiter在一段空闲后变慢了。

最后一点是,如果RateLimiter 采用QPS=1的限定速度,那么开销较大的acquire(100)请求到达时,它是没必要等到100s 才开始实际任务。我们可以先开始任务执行,并把未来的请求推后100s,这样我们就可以同时工作,同时生产需要的permits,而不是空等待permits生产够才开始工作。

这里有一个重要的结论:RateLimiter不需要记住上个请求的时间,它只需要记住“希望下个请求到来的时间”即可。这样使得我们能够马上识别出,一个确切的超时时间(跟tryAcquire(timeout)相关)是否满足下一个计划时间点。 如果当前时间大于“期望的下个请求到来的时间”,那么这两个时间的差值就是Ratelimiter的未使用时间t,通过t*QPS计算出storedPermits。

这个方法就是 SmoothRateLimiter 中的reserveEarliestAvailable方法。

2. 源码解析

我们先来看看SmoothRateLimiter类中的主要属性:

    // 最大许可数,比如RateLimiter.create(2);表明最大的许可数就是2

    double maxPermits;

    // 当前剩余的许可数

    double storedPermits;

    // 固定的微秒周期

    double stableIntervalMicros;

    // 下一个空闲票据的时间点

    private long nextFreeTicketMicros = 0L;

因为RateLimiter需要保证许可被稳定连续的输出,比如每秒有5个许可,那么你获取许可的间隔时间是200毫秒,而stableIntervalMicros就是用来保存这个固定的间隔时间的,方便后面计算使用。

SmoothRateLimiter有两个子类:SmoothBursty和SmoothWarmingUp,从名字可以看出,SmoothWarmingUp类的实现带有预热功能,而SmoothBursty类是没有的,什么是预热功能呢?就好比缓存一样,当使用SmoothWarmingUp的实现时,不会在前几秒就给足全量的许可,就是说许可数会慢慢的增长,知道达到我们预定义的值,所以,SmoothRateLimiter会留下如下抽象方法交给其子类来实现,包括前文所述的关键方法storedPermitsToWaitTime():

    // 重设流量相关参数,需要子类来实现,不同子类参数不尽相同,比如SmoothWarmingUp肯定有增长比率相关参数

    void doSetRate(double permitsPerSecond, double stableIntervalMicros);

    // 计算生成这些许可数需要等待的时间

    long storedPermitsToWaitTime(double storedPermits, double permitsToTake);

    // 返回许可冷却(间隔)时间

    double coolDownIntervalMicros();

看完源码后发现,令牌桶算法其实不能望文生义,实现上并不是有个定时器在那里放令牌,然后消耗令牌,而是将空闲时间利用令牌的思想进行建模,实质上是计算并保留下次可执行请求的时间,并根据这个时间与请求到来的时间作比较,然后决定是否执行

具体的实现可以继续看下面的源码。

2.1 以子类SmoothBursty为例

抽象类RateLimiter的create方法返回的是一个“平滑突发”类型SmoothBursty的实例:

    public static RateLimiter create(double permitsPerSecond) { 
        return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
    }

    @VisibleForTesting
    static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
        // 这里就默认设置了1秒钟,表示如果SmoothBursty没被使用,许可数能被保存1秒钟
        RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
        rateLimiter.setRate(permitsPerSecond);
        return rateLimiter;
    }

SmoothBursty的构造函数有两个入参,一个是SleepingStopwatch 实例,它是一个阻止睡眠观察器,可以理解成一个闹钟,如果你想睡十秒,它会在10秒钟内一直阻塞你(让你睡觉),到了十秒钟,它将对你放行(唤醒你),另一个参数是,每秒有几个许可,描述了限速的大小。

我们直接看获取令牌的阻塞实现acquire:

    public double acquire(int permits) {
        // 预定permits个许可供未来使用,并返回等待这些许可需要的时间(微秒数)
        long microsToWait = reserve(permits);
        // 等待指定的时间
        stopwatch.sleepMicrosUninterruptibly(microsToWait);
        // 返回消耗的时间
        return 1.0 * microsToWait / SECONDS.toMicros(1L);
    }

思路比较清楚,关键在于计算需要等待的时间,继续往下看

  /**
   * 向RateLimiter预定许可数,返回预定许可可以被消费的时间
   */
    final long reserve(int permits) {
        // permits的值必须大于0
        checkPermits(permits);
        // 这里通过一个对象锁进行同步
        synchronized (mutex()) {
            // 预定permits数目的许可,并返回调用方需要等待的时间
            return reserveAndGetWaitLength(permits, stopwatch.readMicros());
        }
    }

当拿到mutexDoNotUseDirectly锁后,我们看看reserveAndGetWaitLength如何计算等待时间

    final long reserveAndGetWaitLength(int permits, long nowMicros) {
        //reserveEarliestAvailable()由RateLimiter的子类SmoothRateLimiter实现
        //计算可执行时间点
        long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
        //(等待时间 = 可执行时间点 - 当前时间点) 或者 马上执行
        return max(momentAvailable - nowMicros, 0);
    }

可以看出,获取许可需要等待的时间是由reserveEarliestAvailable方法来实现的,它是RateLimiter的抽象方法,由其抽象子类SmoothRateLimiter来实现。

现在,继续看reserveEarliestAvailable方法如何实现

    /**
    * 返回可执行时间点
    */
    @Override
    final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
        // 基于当前时间nowMicros来更新storedPermits和nextFreeTicketMicros
        resync(nowMicros);
        long returnValue = nextFreeTicketMicros;
        // 从需要申请的许可数和当前可用的许可数中找到最小值
        double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
        double freshPermits = requiredPermits - storedPermitsToSpend;
        long waitMicros =
            storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
                + (long) (freshPermits * stableIntervalMicros);
        // 更新下一个票据的等待时间
        this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
        // 更新剩余可用许可数
        this.storedPermits -= storedPermitsToSpend;
        return returnValue;
    }

    /** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. */
    void resync(long nowMicros) {
        // 如果下个可执行时间点大于当前时间,那么就继续等待,不需要更新
        // 反之,说明又空闲了一段时间,所以存储许可的数量肯定又增多了,可执行时间就是当前时间
        if (nowMicros > nextFreeTicketMicros) {
            //coolDownIntervalMicros()SmoothBurst中采用固定的间隔,SmoothWarmingUp中则跟warmupPeriodMicros有关
            //计算出新的空闲时间内产生的存储许可数量
            double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
            storedPermits = min(maxPermits, storedPermits + newPermits);
            nextFreeTicketMicros = nowMicros;
        }
    }

在SmoothBurst中的storedPermitsToWaitTime()非常简单,直接返回了0

    @Override
    long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
      return 0L;
    }

也就是说,对于原本存储的许可直接消耗,不设定另外的等待时间,只用考虑freshPermit的生产时间。
也就是 waitMicros = 0 + freshPermits * stableIntervalMicros

2.2 子类SmoothWarmingUp的关键不同点

create

create的时候初始化了一些参数,包括最大令牌数、冷却时间等。
SmoothWarmingUp子类创建的时候需要说明warmupPeriod

    public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) {
        checkArgument(warmupPeriod >= 0, "warmupPeriod must not be negative: %s", warmupPeriod);
        //手动设置了coldfactor
        return create(
            permitsPerSecond, warmupPeriod, unit, 3.0, SleepingStopwatch.createFromSystemTimer());
    }

    @VisibleForTesting
    static RateLimiter create(
        double permitsPerSecond,long warmupPeriod,TimeUnit unit, double coldFactor,        SleepingStopwatch stopwatch) {
        RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);
        rateLimiter.setRate(permitsPerSecond);
        return rateLimiter;
    }

2.1中说明的create方法中,也有setRate()方法,这个方法里面检查了参数的合法性,然后就是调用doSetRate()方法,这是SmoothRateLimiter留下的三个交给其子类来实现抽象方法之一,我们对它们作个比较。

    /**
    * 这里做的事情就比较简单,就是计算下最大允许存储许可数量和初始化存储许可数量
    */
    @Override
    void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
      double oldMaxPermits = this.maxPermits;
      maxPermits = maxBurstSeconds * permitsPerSecond;
      if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        // if we don't special-case this, we would get storedPermits == NaN, below
        storedPermits = maxPermits;
      } else {
        storedPermits =
            (oldMaxPermits == 0.0)
                ? 0.0 // initial state
                : storedPermits * maxPermits / oldMaxPermits;
      }
    }


    @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);
      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;
      }
    }

两者的初始storePermit是不一样的。
然后这里的算法在javadoc中给出了详细的图文说明(简单的数学推导,有兴趣可以看看,反正就是神奇的自定义参数

/**
   * This implements the following function where coldInterval = coldFactor * stableInterval.
   *
   * <pre>
   *          ^ throttling
   *          |
   *    cold  +                  /
   * interval |                 /.
   *          |                / .
   *          |               /  .   ← "warmup period" is the area of the trapezoid between
   *          |              /   .     thresholdPermits and maxPermits
   *          |             /    .
   *          |            /     .
   *          |           /      .
   *   stable +----------/  WARM .
   * interval |          .   UP  .
   *          |          . PERIOD.
   *          |          .       .
   *        0 +----------+-------+--------------→ storedPermits
   *          0 thresholdPermits maxPermits
   * </pre>
   *
   * 对这个函数进行详细说明前,我们需要了解以下基本信息:

   * <ol>
   *   <li>RateLimiter的状态 (storedPermits数量)是图中的垂直线
   *   <li>当RateLimiter进入空闲后,这个线会向右移动(直到 maxPermits)
   *   <li>当RateLimiter 被使用后, 这个线会向左移动(直到 zero), 因为有存储的许可,我们就会先从存储许可中拿
   *   <li>当不使用的时候,我们的线向右移动,保持恒定的速率。这个rate是 maxPermits / warmupPeriod. 
   *       这样就能保证从0移动到maxPermits的时间等于warmupPeriod
   *   <li>当使用许可的时候,它所需要的时间,如设计哲学中的解释,它等于在X-K到X上对该函数的积分,假定我们
   *       需要消耗X个许可
   * </ol>
   *
   * <p>总的来说,向左移动K个permit(就是消耗K个permit)的时间就是这个函数上间隔为K的积分面积。
   *
   * <p>假设我们有一个饱和模型(百度说是理想的人为定义模型),从maxPermit到thresholdPermit的
   * 时间就等于warmupPeriod. 然后从thresholdPermit到0的时间等于warmupPeriod/2. 
   * (这里是warmupPeriod/2的原因是为了保持原始的实现行为,也就是coldFactor被硬编码为3.)
   *
   * <p>这里还需要去计算 thresholdsPermits 和 maxPermits.
   *
   * <ul>
   *   <li>从 thresholdPermits 到 0 的积分是 thresholdPermits * stableIntervals. 也就是 warmupPeriod/2. 
   *       这部分就是最大速率的消耗时间,也就是正常速度
   *       因此
   *       thresholdPermits = 0.5 * warmupPeriod / stableInterval
   *   <li>从 maxPermits 到 thresholdPermits 的积分是一个梯形面积,等于
   *        0.5 * (stableInterval + coldInterval) * (maxPermits - thresholdPermits). 
   *       等于 warmupPeriod
   *       这部分是热身限速阶段
   *       因此
   *       maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval)
   * </ul>
   */
acquire()

acquire()基本过程是一致的,差别注意体现在对存储许可消耗的时间上。

    @Override
    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);

        this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
        this.storedPermits -= storedPermitsToSpend;
        return returnValue;
    }

第一处不同:

    void resync(long nowMicros) {
        if (nowMicros > nextFreeTicketMicros) {
            //计算出新的空闲时间内产生的存储许可数量
            double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
            storedPermits = min(maxPermits, storedPermits + newPermits);
            nextFreeTicketMicros = nowMicros;
        }
    }

coolDownIntervalMicros()SmoothBurst中采用固定的间隔stableIntervalMicros,SmoothWarmingUp中则跟warmupPeriodMicros有关,这个在上面的算法中有介绍到,很容易理解

    @Override
    double coolDownIntervalMicros() {
      return warmupPeriodMicros / maxPermits;
    }

第二处不同:

    //这里就是把存储的令牌马上消耗,不计入新的等待时间内。只要有令牌空闲,就能执行请求
    @Override
    long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
      return 0L;
    }

    //就是上面对warmupPeriod的计算,分为水平线部分和梯形部分
    @Override
    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);
        // TODO(cpovirk): Figure out a good name for this variable.
        double length =
            permitsToTime(availablePermitsAboveThreshold)
                + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);
        micros = (long) (permitsAboveThresholdToTake * length / 2.0);
        permitsToTake -= permitsAboveThresholdToTake;
      }
      // measuring the integral on the left part of the function (the horizontal line)
      micros += (long) (stableIntervalMicros * permitsToTake);
      return micros;
    }

所以就是对上面那个函数的代码实现。原理也很清楚了。
That’s all !

3. 有意思的代码

// Can't be initialized in the constructor because mocks don't call the constructor.
  @MonotonicNonNullDecl private volatile Object mutexDoNotUseDirectly;

  private Object mutex() {
    Object mutex = mutexDoNotUseDirectly;
    if (mutex == null) {
      synchronized (this) {
        mutex = mutexDoNotUseDirectly;
        if (mutex == null) {
          mutexDoNotUseDirectly = mutex = new Object();
        }
      }
    }
    return mutex;
  }
  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值