Guava RateLimiter源码阅读

1,970 阅读7分钟

RateLimiter主要用于作限流,对于限流,现在的主要几种算法参考:帮助你理解熔断、降级和限流

RateLimiter便是基于令牌桶实现的流量限制

那么就让我们开始把!


一个官方文档中的例子:假设现在有一系列任务需要执行,并且你希望每秒钟被执行的任务不能超过2个

final RateLimiter rateLimiter = RateLimiter.create(2.0); 
void submitTasks(List<Runnable> tasks, Executor executor) {
    for (Runnable task : tasks) {
        rateLimiter.acquire(); // 可能需要等待
        executor.execute(task);
    }
}

就由这个例子入手,首先看看RateLimiter的create方法,permitsPerSecond保证在任意选定的一秒内均能保证其不超过,这是计数器算法不具有的

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

这里创建的是一个平稳突发启动的RateLimiter,SleepingStopwatch是一个基础计时器,用于度量经过的时间和必要的休眠时间

  • 平稳突发启动
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
    rateLimiter.setRate(permitsPerSecond);
    return rateLimiter;
}
  • 冷启动

比上面的多一个参数,表示冷启动的时候,即:多少时间之内达到permitsPerSecond,单位时间:NANOSECONDS

public static RateLimiter create(double permitsPerSecond, Duration warmupPeriod) {
    return create(permitsPerSecond, toNanosSaturated(warmupPeriod), TimeUnit.NANOSECONDS);
}

这里创建的是一个冷启动的RateLimiter

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

到这里需要理解一个概念,预消费,前一个请求需求的令牌数超过了当前持有的令牌数,那么这个请求可以预消费,直接取走自己所需要的的量,而这个量会由后一个请求来偿还(通过等待时间来偿还)

对于QPS的设置可以通过以下俩个方法进行,通过setRate设置的最新值,对于当前正在睡眠的线程是透明的,即他们不会感知到Rate的变化,只有新进来的Request才能感知到

并且对于setRate的后一个请求,新的Rate也是它感知不到的,这是因为上面提到的预消费,这时候的它已经为上一个请求买单,新的Rate对它而言并没有意义

重新设置Rate如果是冷启动,就还要进行一次冷启动

public final void setRate(double permitsPerSecond) {
    checkArgument(
        permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
    synchronized (mutex()) {
      doSetRate(permitsPerSecond, stopwatch.readMicros());
    }
}
public final double getRate() {
    synchronized (mutex()) {
      return doGetRate();
    }
}

再来到这样一个方法,这个方法决定预消费的实时,后一个消费需要为前一个消费买单,那么买单是需要等待多长时间呢,就是通过这个方法来

final long reserve(int permits) {
    checkPermits(permits);
    synchronized (mutex()) {
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
}

进入实现查看,最后找到的reserveEarliestAvailable方法是一个抽象类,需要子类实现具体的算法

final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    return max(momentAvailable - nowMicros, 0);
}
abstract long reserveEarliestAvailable(int permits, long nowMicros);

不妨来到它的其中一个子类,实现平稳突发启动的SmoothBursty的实现,先看看几个变量

  • double storedPermits; 当前持有令牌数
  • double maxPermits; 最大持有令牌数
  • double stableIntervalMicros; 俩次发放令牌的时间间隔
  /**
   * The time when the next request (no matter its size) will be granted. After granting a request,
   * this is pushed further in the future. Large requests push this further than small requests.
   * 下一次发放令牌的时间,可以是过去也可以是将来
   */
  private long nextFreeTicketMicros = 0L; 
void resync(long nowMicros) {
    // 如果当前时间已经过了下次发放令牌时间,计算相差时间内所能产生的令牌数,将令牌加入令牌桶
    if (nowMicros > nextFreeTicketMicros) {
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros(); //这里的coolDownIntervalMicros = stableIntervalMicros
      storedPermits = min(maxPermits, storedPermits + newPermits);
      nextFreeTicketMicros = nowMicros;  //将这次放牌的时间记录更新
    }
}
@Override
  final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    resync(nowMicros);  //误差修正令牌数和发牌时间
    long returnValue = nextFreeTicketMicros;  
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);  //storedPermitsToSpend:如果需要的令牌数小于桶内令牌数,就是需要的令牌数,这没啥,但是如果需要的比较大,那么这个值就是当前桶内数量
    double freshPermits = requiredPermits - storedPermitsToSpend;  //freshPermits就是还差多少个令牌
    long waitMicros =
        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)    
            + (long) (freshPermits * stableIntervalMicros);  //通过计算获取预消费的时间
    //storedPermitsToWaitTime 在这里空实现 返回0L

    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); //将预消费时间加入到 下次获取令牌时间,就是因为这里,所以导致下次请求需要为前一个请求买单
    this.storedPermits -= storedPermitsToSpend; //重新计算桶内令牌
    return returnValue; //返回上次消费的时间,即:返回的时候本次请求刚进来的时间,而不是本次请求进来后,预消费后的时间
}

上面的处理,返回的本次请求刚进来的时间,而不是返回本次请求进来后,再经过它预消费的时间,就是为了预消费作处理,如果不需要预消费,请在这里返回最新的nextFreeTicketMicros

来了解一下冷启动

SmoothRateLimiter源码,请到line145找这张图

认识几个变量:

  • thresholdPermits 阙值,如果桶内令牌数大于这个值,即变为冷启动,增大产生令牌间隔 它的值是 预热的时候内所能产生的令牌数的一半,0.5 * warmupPeriodMicros / stableIntervalMicros

  • coldFactor 冷却系数,默认为3.0

  • slope 表示斜率,用正常的斜率公式计算就是了(coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);

对于冷启动的实现,其实主要是在3个方法上重写方式不同

  1. 一个是doSetRate在俩处的实现是不一样的

直接启动是通过直接重新计算产生令牌的间隔,然后重新设置间隔,最大令牌数,以及当前持有令牌数

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

冷启动的doSetRate:

void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
      double oldMaxPermits = maxPermits;
      //冷启动状态下,时间间隔为正常时间间隔的coldFactor倍数,coldFactor默认为3.0
      double coldIntervalMicros = stableIntervalMicros * coldFactor;
      //计算thresholdPermits,它的值是 预热的时候内所能产生的令牌数的一半
      thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;
      //计算maxPermits,通过梯形公式反推,面积是预热时间,上底正常间隔,下底冷启动间隔,高=max-阙值,反推max
      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;
      }
}
  1. 一个是coolDownIntervalMicros,他是用来计算延迟的时间内产生的令牌数,直接启动其实就是返回stableIntervalMicros间隔时间

而冷启动:warmupPeriodMicros / maxPermits,冷启动时间/最大令牌数(warmupPeriodMicros / maxPermits),即计算 冷启动时间内生产令牌到达最大令牌数的间隔

  1. 一个是reserveEarliestAvailable中的storedPermitsToWaitTime,就是在计算获取预消费时间的时候多了一处计算,直接启动返回0L,而冷启动的代码如下:

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)  //这一行求的是当前间隔,里面是stableIntervalMicros + permits * slope; 其实是: 平缓的时间间隔 + 当前超过阈值令牌数 * 斜率 (理解成 初速度 + 加速度 * 时间),下同
                + 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;
}

如若有错,烦请指出