guava限流使用场景与源码分析

ⅰ亾dé卋堺 提交于 2021-01-23 00:25:51

我们首先讲一个场景,假如一个接口只能支持每秒5次访问速率,假如要实现这个功能要如何实现?通用的方法我们知道有计数器,漏斗,令牌桶等方法,本文主要介绍Guava如何是如何实现限流的

Guava是使用令牌桶算法来实现限流功能的,但也有自己的一些特点:

  1. 在每次有请求来获取token的时候,会同时添加令牌到令牌桶中,并不是通过异步任务固定速率添加令牌到令牌桶中,是一个惰性的方法。
  2. 超额信用消费,即用将来时间产生的令牌,来满足当前的请求,这样能应对一定的瞬间流量

可以带着这两个特点来继续阅读下文,首先看下Guava是如何使用的,简单代码如下(创建每秒5次的限流器):

RateLimiter limiter = RateLimiter.create(5.0); //创建每秒一次的限流器 
double timeWaited = limiter.acquire(); //获取令牌,返回值为获取令牌等待的时间 System.out.println("time waited: " + timeWaited);

上面的代码使用时很简单的,下面来剖析下代码实现,Guava 中的限流器有两种实现,

  1. SmoothBursty,顾名思义,该实现主要通过令牌桶方法实现,能够应对突发流量
  2. SmoothWarmingUp,实现也是通过令牌桶方法,但在应对突发流量与流量整形中取了折中,能应对一定的突发流量,同时具有一定的流量整形功能

首先我们先看下整体的类结构,下图是类图:

image.jpeg

首先介绍下基类SmoothRateLimiter的关键属性:

属性 作用 描述
storedPermits 当前限流器可用的令牌总数
maxPermits 当前限流器最大的可用令牌数总数 storedPermits<= maxPermits
stableIntervalMicros 请求的平均时间间隔 如假设当前限流器是每秒5次,所以两次请求的平均间隔是200ms,也表示每隔stableIntervalMicros,会产生一个token
nextFreeTicketMicros 下一次可以产生令牌的时间

###SmoothBursty限流实现分析

SmoothBursty类中有一个自己的属性:maxBurstSeconds,值默认为1,表示流量最大持续时间,maxPermits = maxBurstSeconds * permitsPerSecond,permitsPerSecond表示每秒qps

此时一次请求token的请求会首先到acquire()方法中,该方法会顺序执行以下三个步骤

  1. 补充token到令牌桶中,若当前时间nowMicros大于nextFreeTicketMicros,说明有新的token可以产生,并可以加入到令牌桶中的,具体方法在SmoothRateLimiter#resync
  2. 获取token,并计算获取这些token需要等待的时间,具体方法在SmoothRateLimiter#reserveEarliestAvailable
  3. 根据获取token时返回的等待时间,阻塞当前线程,直到指定的等待时间,具体执行入口是在acquire()中的stopwatch.sleepMicrosUninterruptibly(microsToWait)触发

抛开代码实现流程,从token的数据处理流程得到下图:

image.jpeg

上面的各个属性可以参考一开始的表格,如上图,当在nowMicros时间点请求requiredPermits个token时,会先判断nowMicros是否大于nextFreeTicketMicros,

  1. 若大于则会多走一步step1,计算出在[nextFreeTicketMicros,nowMicros]产生的tokens,并将这些token加到令牌桶总数中storedPermits,同时将nextFreeTicketMicros设置为当前时间,其中coolDownIntervalMicros这个方法是获取产生一个token所需要的时间间隔

  2. step2不管怎样都会被执行,在步骤中会计算获取token所需要的等待时间,同时更新nextFreeTicketMicros时间,即下一次可以获取token的时间点:

  3. storedPermitsToSpend表示可以从令牌桶中获取的token数量

  4. freshPermits表示需要新生成的token数量

  5. waitMicros表示获取requiredPermits个token所需要的消耗时间,该等待时间是由两部分组成,一个是新生成freshPermits个token需要的时间,以及消耗令牌桶已经存在的storedPermitsToSpend个token所需要的时间,即方法storedPermitsToWaitTime(这个地方可能会有人产生疑问,为啥令牌都已经产生了,直接使用还需要消耗时间,这个会在后面介绍)

现在重点从代码分析介绍:

acquire方法,获取token的入口

public double acquire(int permits) {
//更新token同时返回本次请求需要等待的时间,即 nowMicros-nextFreeTicketMicros(注意该时间为step1后的值)
 long microsToWait = reserve(permits);  
//返回本次请求阻塞等待的时间
 stopwatch.sleepMicrosUninterruptibly(microsToWait);
 return 1.0 * microsToWait / SECONDS.toMicros(1L); 
 }

reserve方法会调用reserveAndGetWaitLength方法,

final long reserveAndGetWaitLength(int permits, long nowMicros) { 
//表示当前时间开始,要等到哪个时间点,即为(momentAvailable),才获取permits个toke,
  long momentAvailable = reserveEarliestAvailable(permits, nowMicros); 
 return max(momentAvailable - nowMicros, 0); }

reserve方法,最终会进入到reserveEarliestAvailable方法中,注意storedPermitsToWaitTime方法在SmoothBursty中是默认返回0

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
 resync(nowMicros); //执行step1步骤,更新令牌桶数据
 long returnValue = nextFreeTicketMicros;//保存此刻的nextFreeTicketMicros值 
//获取本次请求中可以从令牌桶中获取的token数量
 double storedPermitsToSpend = min(requiredPermits, this.storedPermits); 
//本次请求中,需要新生成的token数量,即令牌桶的令牌不够用了 
 double freshPermits = requiredPermits - storedPermitsToSpend; 
 long waitMicros = //计算获取requiredPermits所消耗的总时间 
 storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + (long) (freshPermits * 
 stableIntervalMicros);
//更新nextFreeTicketMicros,表示下次可以产生token的时间点
 this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); 
 this.storedPermits -= storedPermitsToSpend; //更新令牌桶,减去被消耗的令牌数据
 return returnValue;
 }

resync方法,更新令牌桶数量和nextFreeTicketMicros,coolDownIntervalMicros用来获取产生令牌的时间间隔,在SmoothBursty中,该方法默认返回stableIntervalMicros

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

以上就是SmoothBursty限流的实现方法,从上面我们可以看出nextFreeTicketMicros可能是大于当前时间的,即用将来的时间产生token来满足当前的token需求

###SmoothWarmingUp 限流实现分析

以上其实还有一个未解问题,为啥会有storedPermitsToWaitTime方法(虽然在SmoothBursty中是默认返回0,所以在SmoothBursty实现中是没有任何意义的),为啥使用令牌桶中已经存在的token还要消耗时间?

首先引入一个问题:SmoothBursty实现能够很好的应对突发流量,但瞬时流量来了,后端服务是否准备好了?服务中的数据是否“热起来了”,会不会造成缓存击穿等问题。所以瞬时流量对服务还是有一定隐患存在的

为了解决这个问题,Guava提供了另外一个限流实现,SmoothWarmingUp,如名字一样,请求会被缓慢放行,即使使用已经存在存在的令牌,也是要消耗时间的,假设令牌桶的token是满的,说明在当前时间访问服务的请求是很少的,此时服务是处于一个“冷状态”,所以更不能一下放行大量请求进来,SmoothWarmingUp实现的主流程是和SmoothBursty一样的,只不过针对storedPermitsToWaitTime和coolDownIntervalMicros有不同的实现

SmoothWarmingUp关键属性:

  1. warmupPeriodMicros,表示处于平滑过渡的时间长度
  2. slope ,在平滑过渡时间段是,coldInterval随着storedPermits变化的变化率
  3. thresholdPermits,平滑过渡时间段与正常时间段的分界点
  4. coldFactor,coldInterval = coldFactor * stableInterval 表示coldInterval与stableInterval倍数关系

image.png

先看下上面这个图,图中横坐标表示令牌桶中可用数量,纵坐标表示使用当前令牌所消耗的时间,所以storedPermitsToWaitTime(storedPermits, storedPermitsToSpend)方法返回的是从令牌桶中使用storedPermitsToSpend个token所需要的时间,而该时间可以表示为上图中横坐标为[storedPermits-storedPermitsToSpend,storedPermits]对应的面积。

storedPermitsToWaitTime方法具体源码:

long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
  double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
  long micros = 0;
  // 假如permitsToTake大于thresholdPermits,则计算【thresholdPermits,storedPermits】的梯形面积
  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;
  }
  // 计算【storedPermits - thresholdPermits,thresholdPermits】的面积
  micros += (long) (stableIntervalMicros * permitsToTake);
  return micros;
}

在SmoothWarmingUp限流实现中会在doSetRate方法中进行属性的初始化

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

doSetRate方法解释:

  1. thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros; 我们默认为warmupPeriodMicros平滑过渡的时间长度是正常时间长度的2倍,而如图所示,在正常阶段,thresholdPermits个令牌与与stableInterval的乘积就是产生thresholdPermits令牌的总时间
  2. slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits),表示在平滑过渡时间段是,coldInterval随着storedPermits变化的变化率

参考文档:

  1. https://segmentfault.com/a/1190000016240755
  2. https://www.lagou.com/lgeduarticle/66250.html
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!