限流-Guava-RateLimiter

转载声明

本文大量内容系转载自以下文章,有删改,并参考其他文档资料加入了一些内容:

转载仅为方便学习查看,一切权利属于原作者,本人只是做了整理和排版,如果带来不便请联系我删除。

1 前言

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流

  • 缓存
    缓存的目的是提升系统访问速度和增大系统处理容量。
  • 降级
    降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开。
  • 限流
    限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。

2 常用的限流算法

2.1 漏桶算法

在这里插入图片描述
漏桶算法思路很简单:

  1. 水(请求)先进入到漏桶里
  2. 漏桶以恒定的速度出水
  3. 注意,当水流入速度过大时会直接溢出。

漏桶算法能强行限制数据的传输速率为恒定速率,当流入的请求过大时就拒绝抛掉了。

2.2 令牌桶算法

对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。

在这里插入图片描述

在这里插入图片描述
如上图所示,令牌桶算法的原理:

  1. 系统会以一个恒定的速度往桶里放入令牌
  2. 如果请求需要被处理,需要先从桶里获取一个令牌
  3. 当桶里没有令牌可取时,则拒绝服务或等待

2.3 漏桶对比令牌桶

2.3.1 当请求流入速度小于请求处理速度时
  • 漏桶
    整个系统处理速度取决于请求流入速度;
  • 令牌桶
    此时请求获取令牌速度小于令牌放入速度,此时整个系统处理速度也取决于请求流入速度。
2.3.2 当请求流入速度大于请求处理速度时
  • 漏桶
    整个系统处理速度取决于系统限制的处理请求的平均速度,处理不过来的请求溢出(抛掉)。
  • 令牌桶
    整个系统处理速度取决于系统限制的放入令牌速度,处理不过来的请求抛掉或排队等待令牌。
2.3.3 关键区别
  • 漏桶
    无论怎么样,使用漏桶算法最多也就能将处理速度达到请求流入的恒定速度上限,而面对突发的请求流入就无能为力了,只能抛掉。
  • 令牌桶
    因为可以提前放入令牌,所以可能低峰期累积了一定量令牌,可在突发请求时使用这些令牌。有一些算法如Guava-RateLimiter可在令牌不足时提前直接使用,而只是在下一次使用令牌时等待这些数量的令牌。
2.3.4 使用场景区别

并不能说明令牌桶一定比漏洞好,她们使用场景不一样:

  • 令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮
    所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制(之前由于放入令牌速度大于令牌申请速度而放了预备的一些令牌)。
  • 而漏桶算法,这是用来保护他人,也就是保护他所调用的系统
    主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。

总结起来:如果要让自己的系统不被打垮,用令牌桶。如果保证被别人的系统不被打垮,用漏桶算法。

3 Guava-RateLimiter的使用

3.1 概述

Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法实现流量限制,使用十分方便,而且十分高效。

3.2 RateLimiter使用

简单例子:

public void testAcquire() {
      // 首先创建一个限流器,参数代表每秒生成的令牌数
      RateLimiter limiter = RateLimiter.create(1);

      for(int i = 1; i < 10; i = i + 2 ) {
          // limiter.acquire以阻塞的方式获取令牌。
          // 当然也可以通过tryAcquire(int permits, long timeout, TimeUnit unit)来设置等待超时时间的方式获取令牌,
          // 如果超timeout为0,则代表非阻塞,获取不到立即返回
          double waitTime = limiter.acquire(i);
          System.out.println("cutTime=" + System.currentTimeMillis() + " acq:" + i + " waitTime:" + waitTime);
      }
  }

输出结果:

cutTime=1535439657427 acq:1 waitTime:0.0
cutTime=1535439658431 acq:3 waitTime:0.997045
cutTime=1535439661429 acq:5 waitTime:2.993028
cutTime=1535439666426 acq:7 waitTime:4.995625
cutTime=1535439673426 acq:9 waitTime:6.999223

RateLimiter是预消费机制:

  • 第一次acq:1 waitTime:0.0,预消费了1个令牌
  • 第二次acq:3 waitTime:0.997045,等待的时间是第一次获取一个令牌耗费的1秒。此次预消费了3个令牌。
  • 第三次acq:5 waitTime:2.993028,等待的时间是第二次获取3个令牌耗费的3秒。此次预消费了5个令牌。
  • 依次类推。也就是说,并不是每次消费都等待令牌放入,而是先消费指定数量令牌,让下一次消费来等待上一次消费的令牌数对应的生产时间。

RateLimiter通过限制后面请求的等待时间,来支持一定程度的突发请求(预消费),在使用过程中需要注意这一点,具体实现原理后面再分析。

4 RateLimiter实现原理

4.1 简介

Guava有两种限流模式:

  • 恒定模式(SmoothBursty:令牌生成速度恒定)
  • 渐进模式(SmoothWarmingUp:令牌生成速度缓慢提升直到维持在一个稳定值,可以强制平滑限流)
    两种模式实现思路类似,主要区别在等待时间的计算上,本篇重点介绍SmoothBursty恒定模式。

4.2 RateLimiter的创建

通过调用RateLimiter的create接口来创建实例,实际是调用的SmoothBuisty稳定模式创建的实例。

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

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

SmoothBursty中的两个构造参数含义:

  • SleepingStopwatch
    guava中的一个时钟类实例,会通过这个来计算时间及令牌
  • maxBurstSeconds
    官方解释,在ReteLimiter未使用时,最多保存几秒的令牌,默认是1

在解析SmoothBursty原理前,重点解释下SmoothBursty中几个属性的含义

/**
 * The work (permits) of how many seconds can be saved up if this RateLimiter is unused?
 * 在RateLimiter未使用时,令牌最多被存储几秒
 * */
 final double maxBurstSeconds;
 
/**
 * The currently stored permits.
 * 当前存储令牌数
 */
double storedPermits;

/**
 * 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.
 * 添加令牌时间间隔 = SECONDS.toMicros(1L) / permitsPerSecond;(每秒生产的令牌数)
 * 比如5令牌,代表每秒内,间隔200ms 1个令牌
 */
double stableIntervalMicros;

/**
 * The maximum number of stored permits.
 * 最大存储令牌数 = maxBurstSeconds * stableIntervalMicros
 * 即 最大存储令牌数 = 令牌最大空闲存在时间 * 令牌生产间隔时间
 */
double maxPermits;

/**
 * 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.
 * 下一次请求可以获取令牌的起始时间
 * 由于RateLimiter允许预消费,上次请求预消费令牌后
 * 下次请求需要等待相应的时间到nextFreeTicketMicros时刻才可以获取令牌
 */
private long nextFreeTicketMicros = 0L; // could be either in the past or future

接下来介绍几个关键函数

  • RateLimiter.setRate
    通过这个方法设置令牌通每秒生成令牌的数量,内部时间通过调用SmoothRateLimiter的doSetRate来实现
    public final void setRate(double permitsPerSecond) {
      checkArgument(
          permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
      synchronized (mutex()) {
        // permitsPerSecond为每秒令牌数
        // stopwatch.readMicros()是当前耗时
        doSetRate(permitsPerSecond, stopwatch.readMicros());
      }
    }
    
  • SmoothRateLimiter.doSetRate
    这里先通过调用resync生成令牌以及更新下一期令牌生成时间,然后更新stableIntervalMicros,最后又调用了SmoothBursty的doSetRate
    @Override
    final void doSetRate(double permitsPerSecond, long nowMicros) {
      // 生成令牌以及更新下一期令牌生成时间
      resync(nowMicros);
      // 计算出每次令牌生成间隔时间
      double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
      this.stableIntervalMicros = stableIntervalMicros;
      doSetRate(permitsPerSecond, stableIntervalMicros);
    }
    
  • SmoothRateLimiter.resync
    基于当前耗时,更新当前存储的令牌以及下一次请求令牌的时间
    /**
     * Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time.
     * 基于当前耗时,更新当前存储的令牌以及下一次请求令牌的时间
     */
    void resync(long nowMicros) {
        // 只有当上一轮计算的预消费令牌计算出的需要等待时间大于当前消费时间时,才会不走以下逻辑。
        // 此时会阻塞,时长为上一轮计算的预消费令牌计算出的需要等待时间减去当前轮消耗时间
        if (nowMicros > nextFreeTicketMicros) {
          // nextFreeTicketMicros初始为0,
          // coolDownIntervalMicros是stableIntervalMicros,初始状态也为0
          // 所以初始时,newPermits为无穷大Infinity
          // 以后每次根据当前耗时减去上一轮耗时再除以令牌生成间隔,
          // 生成这段时间内新的令牌数
          double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
          // maxPermits和storedPermits初始都为0,所以这里storedPermits = 0
          // 即初始时,当前存储令牌为0 
          // 以后每次根据时间来更新现有的令牌数
          storedPermits = min(maxPermits, storedPermits + newPermits);
          // 更新下次请求令牌时间为此次消耗时间
          nextFreeTicketMicros = nowMicros;
        }
    }
    

根据令牌桶算法,桶中的令牌是持续生成存放的,有请求时需要先从桶中拿到令牌才能开始执行,谁来持续生成令牌存放呢?

一种解法是,开启一个定时任务,由定时任务持续生成令牌。这样的问题在于会极大的消耗系统资源,如,某接口需要分别对每个用户做访问频率限制,假设系统中存在6W用户,则至多需要开启6W个定时任务来维持每个桶中的令牌数,这样的开销是巨大的。

另一种解法则是延迟计算,如上resync函数。该函数会在每次获取令牌之前调用,其实现思路为,若当前时间晚于nextFreeTicketMicros,则计算该段时间内可以生成多少令牌,将生成的令牌加入令牌桶中并更新数据。这样一来,只需要在获取令牌时计算一次即可。

  • SmoothBursty.doSetRate
    桶中可存放的最大令牌数由maxBurstSeconds计算而来,其含义为最大存储maxBurstSeconds秒生成的令牌。
    该参数的作用在于,可以更为灵活地控制流量。如,某些接口限制为300次/20秒,某些接口限制为50次/45秒等。也就是流量不局限于qps
    @Override
    void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
      // 初始为0
      double oldMaxPermits = this.maxPermits;
      // 更新最大令牌数为1乘以每秒令牌数
      maxPermits = maxBurstSeconds * permitsPerSecond;
      if (oldMaxPermits == Double.POSITIVE_INFINITY) {
        // if we don't special-case this, we would get storedPermits == NaN, below
        // Double.POSITIVE_INFINITY 代表无穷大
        storedPermits = maxPermits;
      } else {
        // 更新当前令牌数,初始为0,
        // 否则为 maxPermits / oldMaxPermits
        storedPermits =
            (oldMaxPermits == 0.0)
                ? 0.0 // initial state
                : storedPermits * maxPermits / oldMaxPermits;
      }
    }
    

4.3 RateLimiter对外暴露的常用方法

在了解以上概念后,就非常容易理解RateLimiter暴露出来的方法。

4.3.1 令牌申请
  • RateLimiter.acquire()
    @CanIgnoreReturnValue
    public double acquire() {
      return acquire(1);
    }
    
    不指定令牌数的申请其实就是调用以下方法,固定申请一个令牌。
  • RateLimiter.acquire(int permits)
    获取令牌,如果需要的话会挂起等待。
    最后返回阻塞的时间.
    @CanIgnoreReturnValue
    public double acquire(int permits) {
      // 申请令牌,得到还需要阻塞时间
      long microsToWait = reserve(permits);
      // 阻塞
      stopwatch.sleepMicrosUninterruptibly(microsToWait);
      // 返回等待时间
      return 1.0 * microsToWait / SECONDS.toMicros(1L);
    }
    
  • RateLimiter.reserve
    预留固定数量的令牌,以备未来使用。该方法会返回这些预留的令牌可被消费前需要等待的时间点。
final long reserve(int permits) {
  checkPermits(permits);
  // 使用了同步锁
  synchronized (mutex()) {
    // 调用reserveAndGetWaitLength获取等待时间
    return reserveAndGetWaitLength(permits, stopwatch.readMicros());
  }
}
  • RateLimiter.reserveAndGetWaitLength
/**
 * Reserves next ticket and returns the wait time that the caller must wait for.
 *
 * @return the required wait time, never negative
 */
final long reserveAndGetWaitLength(int permits, long nowMicros) {
  // 得到上一轮取令牌时间
  long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
  // 返回上一轮取令牌时间减去当前此轮已经消耗时间进行阻塞
  // 只有当上一轮计算的预消费令牌计算出的需要等待时间大于当前消费时间时,才会需要阻塞,时长为上一轮计算的预消费令牌计算出的需要等待时间减去当前轮消耗时间
  return max(momentAvailable - nowMicros, 0);
}
  • RateLimiter.reserveEarliestAvailable
    首先通过resync生成令牌以及同步nextFreeTicketMicros时间戳,freshPermits从令牌桶中获取令牌后还需要的令牌数量,通过storedPermitsToWaitTime计算出获取freshPermits还需要等待的时间,在稳定模式中,这里就是(long) (freshPermits * stableIntervalMicros) ,然后更新nextFreeTicketMicros以及storedPermits,这次获取令牌需要的等待到的时间点, reserveAndGetWaitLength返回需要等待的时间间隔。

    reserveEarliestAvailable可以看出RateLimiter的预消费原理,以及获取令牌的等待时间时间原理(可以解释示例结果),再获取令牌不足时,并没有等待到令牌全部生成,而是更新了下次获取令牌时的nextFreeTicketMicros,从而影响的是下次获取令牌的等待时间。

    @Override
    final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
      // 基于当前耗时,更新当前存储的令牌以及下一次请求令牌的时间
      // 其实就是(当前消耗时间-上一轮耗时)/令牌间隔得到新的令牌数
      // 拿这个数和每秒最大令牌数取较小的那个,作为新的现存令牌数
      // 并以此轮耗时来更新nextFreeTicketMicros
      resync(nowMicros);
      long returnValue = nextFreeTicketMicros;
      // 消耗的令牌
      double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
      // 还需要的令牌数
      double freshPermits = requiredPermits - storedPermitsToSpend;
      // 获得令牌尚需的时间,如果当前令牌足够就为0
      // 否则就是令牌数*令牌生成间隔
      long waitMicros =
          storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
              + (long) (freshPermits * stableIntervalMicros);
    
      // 更新下次获取令牌时间
      this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
      // 更新当前令牌为当前令牌减去消耗令牌数
      this.storedPermits -= storedPermitsToSpend;
      // 返回上一轮取令牌时间
      return returnValue;
    }
    
  • stopwatch.sleepMicrosUninterruptibly
    reserve这里返回等待时间后,acquire通过调用stopwatch.sleepMicrosUninterruptibly(microsToWait);进行sleep操作,这里不同于Thread.sleep(), 这个函数的sleep是uninterruptibly不可中断的,内部实现:

    public static void sleepUninterruptibly(long sleepFor, TimeUnit unit) {
       //sleep 阻塞线程 内部通过Thread.sleep()
     boolean interrupted = false;
     try {
       long remainingNanos = unit.toNanos(sleepFor);
       long end = System.nanoTime() + remainingNanos;
       while (true) {
         try {
           // TimeUnit.sleep() treats negative timeouts just like zero.
           NANOSECONDS.sleep(remainingNanos);
           return;
         } catch (InterruptedException e) {
          //如果被interrupt可以继续,更新sleep时间,循环继续sleep
           interrupted = true;
           remainingNanos = end - System.nanoTime();
         }
       }
     } finally {
       if (interrupted) {
        // 如果被打断过,sleep过后再真正中断线程
         Thread.currentThread().interrupt();
       }
     }
    }
    

sleep之后,acquire返回sleep的时间,阻塞结束,获取到令牌。

4.3.2 令牌尝试申请

通过tryAcquire(int permits, long timeout, TimeUnit unit)来设置等待超时时间的方式获取令牌。

  • tryAcquire
    尝试在timeout时间内获取令牌,如果可以则挂起等待相应时间并返回true,否则立即返回false。有三个版本:

    public boolean tryAcquire(int permits) {
      return tryAcquire(permits, 0, MICROSECONDS);
    }
    
    public boolean tryAcquire() {
      return tryAcquire(1, 0, MICROSECONDS);
    }
    
    public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
      long timeoutMicros = max(unit.toMicros(timeout), 0);
      checkPermits(permits);
      long microsToWait;
      synchronized (mutex()) {
        long nowMicros = stopwatch.readMicros();
        if (!canAcquire(nowMicros, timeoutMicros)) {
          return false;
        } else {
          microsToWait = reserveAndGetWaitLength(permits, nowMicros);
        }
      }
      stopwatch.sleepMicrosUninterruptibly(microsToWait);
      return true;
    }
    
  • canAcquire
    用于判断timeout时间内是否可以获取令牌,通过判断当前时间+超时时间是否大于nextFreeTicketMicros来决定是否能够拿到足够的令牌数,如果可以获取到,则过程同acquire,线程sleep等待。

    如果通过canAcquire在此超时时间内不能回去到令牌,则可以快速返回,不需要等待timeout后才知道能否获取到令牌。

    private boolean canAcquire(long nowMicros, long timeoutMicros) {
      return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
    }
    
    @Override
    final long queryEarliestAvailable(long nowMicros) {
      return nextFreeTicketMicros;
    }
    

5 更多好文

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值