剖析Guava RateLimiter限流原理

一、限流的基础算法

1.1 漏桶算法

漏桶算法
  如上图所示,假设有一个水桶,水桶有一定的容量,入水口不限速度将水全部注入到水桶中,然后水桶的出水口以一个恒定的速度将水放出,当入水口速度过大时,这个漏斗中就会积水,如果水太多了就会溢出。

优点:平滑突发请求,削减峰值
缺点:漏出的速度可能会拖慢整个系统,不能有效地利用系统的资源

1.2 令牌桶算法

令牌桶算法示意图
  如上图所示,令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝。当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不同,该请求就要被限流,要么直接丢弃,要么在缓冲区等待。

优点:相比漏桶算法,令牌桶算法允许一定的突发流量,但是又不会让突发流量超过我们给定的限制(单位时间窗口内的令牌数)。

二、RateLimiter限流原理

  Google开源工具包Guava的RateLimiter提供了令牌桶算法实现,包含平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)。

2.1 RateLimiter使用示例

  首先通过RateLimiter.create(5);创建一个限流器,参数代表每秒生成的令牌数,通过limiter.acquire(i)以阻塞的方式获取令牌。
  RateLimiter对象可以保证1秒内不会给超过5个令牌,并且以固定速率进行放置,达到平滑输出的效果。

public void testBurstyLimiter1() {
        RateLimiter limiter = RateLimiter.create(5);
        while (true) {
            System.out.println("get 1 tokens: " + limiter.acquire() + "s");
        }
    }

输出结果:

 get 1 tokens: 0.0s
 get 1 tokens: 0.190636s
 get 1 tokens: 0.193058s
 get 1 tokens: 0.195473s
 get 1 tokens: 0.189132s
 get 1 tokens: 0.197889s
 get 1 tokens: 0.197371s
 get 1 tokens: 0.197741s
 get 1 tokens: 0.198837s
 get 1 tokens: 0.195929s

  RateLimiter使用令牌桶算法,会进行令牌的累积,如果获取令牌的频率比较低,则不会导致等待,直接获取令牌。
  RateLimiter在没有足够令牌发放时,采用滞后处理的方式,也就是前一个请求获取令牌所需等待的时间由下一次请求来进行等待。

public void testBurstyLimiter2() {
        RateLimiter limiter = RateLimiter.create(2);
        while (true) {
            try {
                Thread.sleep(2000);
            } catch (Exception e) {}
            System.out.println("get 1 tokens: " + limiter.acquire(1) + "s");
            System.out.println("get 1 tokens: " + limiter.acquire(1) + "s");
            System.out.println("get 1 tokens: " + limiter.acquire(1) + "s");
            System.out.println("get 1 tokens: " + limiter.acquire(1) + "s");
            System.out.println("end");
        }
    }

输出结果:

get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.494773s
end
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.499717s
end
2.2 RateLimiter实现原理

  了解RateLimiter的基本使用方法后,我们来学习一下它的实现原理,首先来看一下类图。
类图

  RateLimiter是入口类,它提供了两套工厂方法来创建出两个子类。工厂方法会调用下面两个函数,生成RateLimiter的两个子类。

  static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {
    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
    rateLimiter.setRate(permitsPerSecond);
    return rateLimiter;
  }
  
  static RateLimiter create(
      SleepingStopwatch stopwatch, double permitsPerSecond, long warmupPeriod, TimeUnit unit,
      double coldFactor) {
    RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);
    rateLimiter.setRate(permitsPerSecond);
    return rateLimiter;
  }

RateLimiter几个重要的成员变量含义:

  /**
   * The currently stored permits.
   * 当前存储令牌数
   */
  double storedPermits;

  /**
   * The maximum number of stored permits.
   * 最大可存储令牌数
   */
  double maxPermits;

  /**
   * The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits
   * per second has a stable interval of 200ms.
   * 添加令牌时间间隔(微秒)
   */
  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; // could be either in the past or future

  前面咱们了解过令牌桶算法原理,桶中的令牌是持续生成存放的,有请求到来时需要先从桶中拿到令牌才能开始执行,怎么持续生成令牌并放入桶中呢?
  我们一般最容易想到的就是开启一个定时任务,由定时任务持续生成令牌并放入桶中。但是这样的问题在于会极大的消耗系统资源,如某秒杀接口需要分别对每个用户做访问频率限制,那就需要每个用户开启一个定时任务,这样的开销是系统不能承受的。

  RateLimiter采用的是延迟计算,等到需要拿令牌的时候再去更新桶中的令牌数量。原理就是每次调用acquire时用当前时间和nextFreeTicketMicros进行比较,根据二者的间隔和添加单位令牌的时间间隔stableIntervalMicros来刷新存储令牌数storedPermits。然后acquire会进行休眠,直到nextFreeTicketMicros,如果nextFreeTicketMicros是过去的时间则直接返回。

acquire函数实现如下:

  1. 调用reserve函数预定指定数量的令牌,并返回所需等待时间
  2. 使用SleepStopwatch进行休眠
  3. 等待时间转换为秒并返回
  public double acquire(int permits) {
    // 计算获取令牌需等待的时间
    long microsToWait = reserve(permits);
    // 线程sleep
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    // 等待时间转换为秒并返回
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }
  
  final long reserve(int permits) {
    checkPermits(permits);
    synchronized (mutex()) {
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
  }
  
  final long reserveAndGetWaitLength(int permits, long nowMicros) {
    // 预定请求令牌数量,并返回令牌可使用的时间
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    // 两个时间相减,获得需要等待的时间
    return max(momentAvailable - nowMicros, 0);
  }

reserveEarliestAvailable是更新存储令牌数和下次获取令牌起始时间的关键函数,分为一下几步:

  1. 调用resync函数更新存储令牌数
  2. 计算预支付令牌所需等待的时间
  3. 更新下次获取令牌时间nextFreeTicketMicros并扣除本次消耗令牌数
  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 {
      // 更新nextFreeTicketMicros,预先支付令牌等待的时间让下一次请求来实际等待
      this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros);
    } catch (ArithmeticException e) {
      this.nextFreeTicketMicros = Long.MAX_VALUE;
    }
    // 更新当前存储令牌数
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
  }

  void resync(long nowMicros) {
    // nextFreeTicketMicros早于当前时间nowMicros,同步为当前时间,并刷新存储令牌数
    if (nowMicros > nextFreeTicketMicros) {
      // coolDownIntervalMicros函数获取生成令牌时间间隔
      storedPermits = min(maxPermits,
          storedPermits
            + (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros());
      nextFreeTicketMicros = nowMicros;
    }
  }
  
  // SmoothBursty
  double coolDownIntervalMicros() {
    return stableIntervalMicros;
  }
  
  // SmoothBursty
  long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
    return 0L;
  }

上面是平滑突发限流的实现,下面看一下加上预热缓冲期的实现原理。

   *
   *          ^ 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

  SmoothWarmingUp在初始阶段与SmoothBursty有点不同,SmoothWarmingUp初始storePermits = maxPermits。如上图所示,从右至左分发令牌的速率会先慢后快,一直使用permits直至storePermits减少到thresholdPermits放入token的时间便稳定下来,到达了“热状态”,之后令牌消费和SmoothBursty一样。

SmoothWarmingUp计算消耗令牌等待时间的代码如下:

    // SmoothWarmingUp计算等待时间就是计算上图中梯形或者矩形的面积。
    long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
      // 当前存储令牌数超出阈值的部分
      double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
      long micros = 0;
      // 如果当前存储的令牌数超出阈值thresholdPermits
      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;
      }
      // 稳定时消耗令牌等待时间
      micros += (stableIntervalMicros * permitsToTake);
      return micros;
    }

    private double permitsToTime(double permits) {
      return stableIntervalMicros + permits * slope;
    }

    double coolDownIntervalMicros() {
      // warmuptime时间内增长的令牌数为maxPermits
      return warmupPeriodMicros / maxPermits;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值