Guava RateLimiter:原理、源码和思想
- 三种限流算法
- 计数器法
- 漏桶算法
- 令牌桶算法
- RateLimiter
- RateLimiter的使用
- RateLimiter原理
- RateLimiter获取令牌
- 获取令牌的基本流程
- 获取令牌的详细源码解读
- RateLimiter的两种限流器
- 试验:感受两种限流器的区别
- SmoothBursty和SmoothWarmingUp的思想
- SmoothBursty和SmoothWarmingUp的应用场景
- 总结
RateLimiter 是 Google Guava 包中的一个设计精美的限流器,在了解它之前,需要先了解一下常见的三种限流算法。
三种限流算法
- 这部分直接引用自知乎大佬“严肃的白小白”的 https://zhuanlan.zhihu.com/p/439682111 一文。
- 本文的其他章节亦受到上文的部分启发,同时大量引用了其中的内容,因此建议读者配合上文一起阅读。两篇文章逻辑组织与行文方式有一定差异,不过无论先读谁都行,之后再去看另一篇一定都会加深理解。
计数器法
设置一个时间窗口内允许的最大请求量,如果当前窗口请求数超过这个设定数量,则拒绝该窗口内之后的请求。
关键词:时间窗口,计数器。
举个例子,我们设置1秒钟的最大请求数量为100,使用一个计数器来记录这一秒中的请求数。每一秒开始时,计数器都从0开始记录请求量,如果在这一秒内计数器达到了100,则在这一秒未结束时,之后的请求全部拒绝。
计数器法是一个简单粗暴的方法。也就是因为简单粗暴,会有一定的问题。比如0.9秒时有100个请求,1.1秒时也有100个请求。按照计数器法的计算,第一秒和第二秒确实都在各自的时间范围内限制了100个请求,但是从0.5秒1.5秒这一秒之间有200个请求,这是计数器法的临界问题。如果请求量集中在两个时间窗口的临界点,会产生请求的尖峰。
我们可以引用滑动窗口的方式更加精细的划分时间窗口解决这个问题。将1秒钟细分为10个格子,每个格子用单独的计数器计算当前100毫秒的请求。随着时间的流逝,我们的窗口也跟着时间滑动,每次都是计算最近10个格子的请求量。如果最近10个格子的请求总量超过了100,则下一个格子里就拒绝全部的请求。格子数量越多,我们对流量控制的也就越精细,这部分逻辑可以借助LinkedList来实现。
而滑动窗口的缺点是,需要一直占用内存空间保存最近一个时间窗口内每个格子的请求。
漏桶算法
我们想象有一个固定容量的桶,桶底有个洞。请求就是从上往下倒进来的水,可能水流湍急,也可能是涓涓细流。因为桶的容量是固定的,所以灌进来太多的水,溢出去了我们就用不了了。所以无论怎样,桶底漏出来的水,都是匀速流出的。而我们要做的,就是处理这些匀速流出的水就好了。
关键词:流入速率不定,流出速率恒定,只处理流出的请求。
漏桶的优点就是,需要处理的请求一直都是固定速率的,特别稳定。而对应的缺点就是,漏桶没有办法解决突发流量的情况。
令牌桶算法
相比于漏桶而言,令牌桶的处理方式完全相反。我们想象依旧有一个固定容量的桶,不过这次是以稳定的速率生成令牌放在桶中。当有请求时,需要获取令牌才能通过。因为桶的容量固定,令牌满了就不生产了。桶中有充足的令牌时,突发的流量可以直接获取令牌通过。当令牌取光后,桶空了,后面的请求就得等生成令牌后才能通过,这种情况下请求通过的速率就变得稳定了。
关键词:令牌生成速率固定,能取到令牌即可通过。
令牌桶的优点是可用处理突发流量,而且被广泛的应用于各种限流工具中。比如Guava的RateLimiter。下面我们就来由浅及深的了解一下,RateLimiter中的令牌桶是怎么实现的吧。
RateLimiter
RateLimiter就采用了令牌桶算法,其中的经典实现方式也被借鉴到包括美团内部框架Rhino在内的其他一些框架工具中。
RateLimiter的使用
RateLimiter使用非常简单,如下的代码展示了其中最简单的一种使用方式(实际上创建的是SmoothBursty限流器,详后文)。
通过RateLimiter的create()方法,我们只需要指定好每秒生成的令牌数(RateLimiter将这个概念定义为“Permits per second,每秒令牌数”,其实和QPS是一个意思),就能创建出一个限流器对象。然后调用acquire()/tryAcquire()方法来获取令牌就可以了。
// 创建一个每秒生成5个令牌的限流器(QPS=5)
RateLimiter limiter = RateLimiter.create(5);
// 等同于limiter.acquire(1):调用者线程会阻塞获取1个令牌,方法返回阻塞等待的时间
double waitTime = limiter.acquire();
// 等同于limiter.tryAcquire(1, 0, MICROSECONDS):立即尝试能否在0秒的时间内获取到所需要的1个令牌,如果能则获取并返回true,不能则立即返回false而不会去阻塞等待
boolean canAcquireImmediately = limiter.tryAcquire();
而且,RateLimiter是线程安全的,这意味着可以多个线程同时使用一个RateLimiter对象做acquire操作获取令牌。
接下来,我们就来看看RateLimiter的原理到底是怎样的。
RateLimiter原理
RateLimiter获取令牌
获取令牌的基本流程
开门见山,我们直接来大概看一下RateLimiter请求获取令牌的流程:
-
RateLimiter的令牌桶算法并不是真的有个令牌桶对象,然后就每秒定时任务式地往桶中生产指定个数的令牌对象。而是每次acquire请求获取特定数量的令牌时,先现按照 {离上一次获取过去的时间/令牌生成速度} 的公式算一下当前时刻令牌桶中应该有多少个令牌(这是一个浮点数)。
这一步对应的方法调用链流程是:
# resync()方法负责去计算当获取令牌的请求发起时,令牌桶中应该有的令牌数。 -> acquire() -> reverse() -> reserveAndGetWaitLength() -> reserveEarliestAvailable() -> resync()
-
计算得到桶内令牌数后,根据请求的令牌数和当前现有的令牌数,通过一定的规则计算出本次请求需要等待的时间是多少(可以是0),然后让请求线程进行阻塞等待这么多时间。
这一步对应的方法调用链流程是:
# reserveEarliestAvailable() 方法负责计算满足本次请求令牌数可用的最早时间 # reserveAndGetWaitLength() 方法将最早可用时间转换为当前需要等待的时间 # reverse() 方法主要做请求校验和synchronized上锁,同时返回需要等待的时间 # stopwatch的sleepMicrosUninterruptibly() 方法用于执行等待的时间,内部调用的是JUC的sleep()方法 -> acquire() -> reverse() -> reserveAndGetWaitLength() -> reserveEarliestAvailable() -> stopwatch.sleepMicrosUninterruptibly()
-
请求线程从acquire请求处苏醒,继续执行后续代码。这样,RateLimiter对这个请求做限流的工作就完成了。
此外,需要补充的是:
-
RateLimiter实际上有两种限流器实现类:SmoothBursty限流器和SmoothWarmingUp限流器。它们的核心区别其实仅仅在于第2步中,去计算本次请求令牌需要等待的时间的方式是不同的,这其实是因为它们对于令牌的生产策略有略微的不同所导致的。具体体现为reserveEarliestAvailable()内调用的计算等待时间的方法有不同的重写,以及两者创建过程中的一些方法为了匹配不同的时间计算方式也有不同的重写。
-
在第2步中,计算出来的等待时间也可能是0,即不需要等待(请求线程不需要被阻塞),请求线程直接能马上获取到令牌。同时也可以看到,RateLimiter一定保证请求线程能拿到它想要的全部令牌,尽管拿到这些令牌可能需要等一段时间。当然,这一前提是你使用的acquire()方法。至于tryAcquire()方法,就有所不同了,如果在指定超时时间内不能获取到,那么线程将不会等待任何时间去获取令牌,而会立即返回false代表获取失败(如果能在超时时间内获取到,则会等待获取,最后返回true然后线程从等待处苏醒)。本文不会过多讨论tryAcquire()方法,因为一旦弄懂了acquire()的逻辑,tryAcquire()也就一目了然了。
接下来,我们可以看看SmoothBursty和SmoothWarmingUp这两种限流器实现类的细节。它们为什么有不同的等待时间计算方式?为了实现这些计算方式,它们在设计上又有何不同?
不过在这之前,我们不妨放慢脚步,在已有初步认识的基础上,通过下面的一小节详细看看上述流程的详细源码解读,并从中引入更深层次的理解。
获取令牌的详细源码解读
读者可以带着以下问题去阅读本节:
- RateLimiter是如何保证线程安全性的?
- RateLimiter是如何实现阻塞的?调用的是JUC的什么API来做阻塞的?
- 上述获取令牌的调用流程到底是如何一步步实现的?RateLimiter到底是如何以非定时任务的方式实现令牌桶算法的?
RateLimiter类:从acquire()出发一步步往下看
/* -----RateLimiter类中----- */
public double acquire() {
return acquire(1);
}
public double acquire(int permits) {
// reverse() 方法主要做请求校验和synchronized上锁,同时返回需要等待的时间
long microsToWait = reserve(permits);
// stopwatch的sleepMicrosUninterruptibly() 方法用于执行等待的时间,内部调用的其实就是JUC的sleep()方法
stopwatch.sleepMicrosUninterruptibly(microsToWait);
// 将微秒转为秒返回
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
// reverse() 方法主要做请求校验和synchronized上锁,同时返回需要等待的时间
final long reserve(int permits) {
// 简单校验一下permits必须是正数
checkPermits(permits);
// 可以看到,RateLimiter保证线程安全的方式是synchronized上锁,mutex()返回的是一个RateLimiter类的一个Object类型的属性,专门当锁对象,所有线程都需要用它上锁
synchronized (mutex()) {
// reserveAndGetWaitLength() 方法将 reserveAndGetWaitLength() 返回的本次请求令牌数最早可用时间转换为当前需要等待的时间
// 传入本次请求令牌数permits和stopwatch.readMicros()获取的当前时间
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}
// reserveAndGetWaitLength() 方法将 reserveAndGetWaitLength() 返回的本次请求令牌数最早可用时间转换为当前需要等待的时间
final long reserveAndGetWaitLength(int permits, long nowMicros) {
// reserveEarliestAvailable() 方法负责计算满足本次请求令牌数可用的最早时间
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
// reserveEarliestAvailable() 方法负责计算满足本次请求令牌数可用的最早时间
// 这是一个抽象方法,实际实现在子类SmoothRateLimiter中(这个类中定义了关键的SmoothBursty和SmoothWarmingUp两个内部子类,所以这个类其实有一些两个子类的通用方法)
abstract long reserveEarliestAvailable(int permits, long nowMicros);
所以接下来看看SmoothRateLimiter类中的reserveEarliestAvailable()。
SmoothRateLimiter类:RateLimiter的子类,关键的SmoothBursty和SmoothWarmingUp两个内部子类,所以这个类其实有一些两个子类的通用方法。比如其中的reserveEarliestAvailable()就是一个相当重要的方法,有必要用大篇幅专门来收一说。不过,在看下面的长篇大论之前,读者可以先草草过一遍reserveEarliestAvailable()的代码。
- 这里你应当注意 nextFreeTicketMicros 这个重要的属性,它代表了“下一次令牌桶处在正常令牌生产过程中的时点”或者“令牌桶下一次受理令牌请求的时点”或者“下一个请求可以正常获取令牌的时间点”。什么叫“下一次令牌桶处在正常令牌生产过程中的时点”呢?按理来说,令牌桶本身是会一直生产令牌的,这是令牌桶算法的核心要义,那么不应该时时刻刻都处在正常令牌生产过程中吗?
- 其实本该是这样的,但是,RateLimiter采用了一种类似于赊账的“预获取令牌模式”的方式来保证一个线程所请求的全部令牌一定能得到满足,且是尽快得到满足。
- 为了实现这个目的,当轮到一个线程A请求令牌时,这次线程A一次请求了很多令牌,RateLimiter会直接一次性算好处理线程A请求所需要花费的全部时长(根据当前令牌数、线程A请求令牌数和限流器的令牌生产策略综合得出)。算出来这一段时长就是“连续的专属于请求线程A的时长”,也就是说,RateLimiter会用这么一段时长专门服务于这个请求线程A,用来生产它所请求的令牌。所以说,这段时长就不属于“正常令牌生产过程”中了,因为这段时长及时长内生产的全部令牌其实是被“透支”给了请求线程A。用更通俗的解释来说,你可以想象其实这段时长内生产的令牌其实根本没放到令牌桶里,刚生产出来就被这个请求线程A拿走了,就像做苦力还自己的赊的账一样,自己没有一点积蓄。这样,nextFreeTicketMicros(下一次令牌桶处在令牌生产过程中的时点) 的含义就很清晰了,nextFreeTicketMicros就是专属于请求线程A的时长最终结束的那个时点(还线程A的账结束)。
- 需要注意的是,线程A并非在nextFreeTicketMicros时点才会从阻塞中唤醒,事实上,线程A会在RateLimiter受理自己的请求之后马上就唤醒,而线程A请求算出来的nextFreeTicketMicros其实代表下一个请求令牌的线程苏醒的时点(反映在代码上就是reserveEarliestAvailable方法会返回nextFreeTicketMicros值作为本次请求令牌数可用的最早时间)。所以最开始才说“预获取令牌模式”类似赊账,债主(线程A)是可以不一直等欠债人(RateLimiter)拿钱(生产令牌)的,但会逼迫欠账人接下来的时间只准拿钱还账而不准拿钱用在其他地方,直到欠账人把账还完。
- 所以,你就知道了:如果此时又有一个请求线程B紧接着过来了,由于线程A正在“透支”令牌(RateLimiter正在还线程A的“账”),所以作为后来者的请求线程B是不能正常获取令牌的,它必须等到专属于请求线程A的时长结束后(也就是nextFreeTicketMicros这个时点),才能得到RateLimiter的受理,这也是线程B从阻塞状态中苏醒的时点。同样地,此时RateLimiter也是立马向线程B透支令牌(进入“赊账”状态)的,直到线程B算出来的nextFreeTicketMicros时点结束。如果这段时间内没有新的获取令牌请求,那么之后RateLimiter终于能够正常的生产令牌放入令牌桶中了,即处于正常令牌生产过程了。
- 进一步地,可以得到一般的结论:只要现有令牌量不够请求令牌量,这一次令牌请求就会进入这种“预获取令牌模式”。这时候如果请求线程多了,就有滚雪球的现象了,各个线程依次“排队收账”,各自卡着自己后面来的那个线程的获取令牌请求。此时,令牌桶其实长期没有正常处于令牌生产过程中,即令牌桶中长期没有任何空闲的令牌,大量的线程都在排队等。
- 那如果现有令牌量够请求令牌量呢?从逻辑上讲,令牌够就一定不存在“赊账”(不存在预获取令牌),所以nextFreeTicketMicros应该比当前时间小(因为不存在线程透支霸占时长),进而就有当前线程不会被阻塞,而会马上得到令牌。实际上,这样的思路既对,又不对。“对”是对于SmoothBursty的策略,“不对”是对于SmoothWarmingUp的策略。不过先就此打住,这里暂且别想那么多,这一问题我们留待后面解决。
- 现在,你应该对线程向RateLimiter请求令牌时为什么会可能会等待一段时间以及要等待多久的时间有更进一步的理解了。那么结合上面的长篇大论,再仔细看看下面的代码。
/* -----SmoothRateLimiter类----- */
// 这个属性定义了“下一次令牌桶处在正常令牌生产过程中的时点”或者说“令牌桶下一次受理令牌请求的时点”
private long nextFreeTicketMicros = 0L; // could be either in the past or future
// reserveEarliestAvailable() 方法负责计算满足本次请求令牌数可用的最早时间
@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
// resync()方法负责去计算当获取令牌的请求发起时,令牌桶中应该有的令牌数。
resync(nowMicros);
// 将nextFreeTicketMicros记录并返回,即本次请求令牌数最早可用的时间就是nextFreeTicketMicros
long returnValue = nextFreeTicketMicros;
// 本次请求的令牌数中,当前令牌数可以供给的部分
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
// 本次请求的令牌数中,用掉全部当前令牌数,仍不够而还需要预获取的部分;如果令牌数够,这就是0
double freshPermits = requiredPermits - storedPermitsToSpend;
long waitMicros =
// 发放当前令牌可以供给的部分所花的时间:SmoothBursty就自然的是0;SmoothWarmingUp则不是0,比较复杂
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
// “赊账”的体现,这里就是当前请求线程所专属的时长;如果令牌数够,这就是0
+ (long) (freshPermits * stableIntervalMicros);
// 让nextFreeTicketMicros加上刚刚计算出来的waitMicros(当前令牌数可以供给的部分所花的时间+“赊账”时间)
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
// 当前令牌数扣掉本次用掉的令牌数
this.storedPermits -= storedPermitsToSpend;
return returnValue;
}
// resync()方法负责去计算当获取令牌的请求发起时,令牌桶中应该有的令牌数。
// 你可以看到,如果当前还处在其他线程的“专属”时长内的话(RateLimiter正在赊账),根本不考虑更新令牌数,因为现在不是处在“令牌生产过程中的时点”
void resync(long nowMicros) {
// 注意nextFreeTicketMicros意思是“下一次令牌桶处在正常令牌生产过程中的时点”
// 只有当前RateLimiter没有在做预获取令牌(没有在“赊账”),才会进入下面的if
if (nowMicros > nextFreeTicketMicros) {
// 更新当前桶中令牌数,其中 coolDownIntervalMicros()的值可以简单理解为创建RateLimiter时设定的QPS取倒数得到的令牌生产速率
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
// 但是再怎么更新也不能超过令牌桶中最大容量
storedPermits = min(maxPermits, storedPermits + newPermits);
// 时刻保持对 nextFreeTicketMicros 的更新,表明最新的令牌桶状态
nextFreeTicketMicros = nowMicros;
}
}
其实到这,我们会发现大部分的逻辑已经很清楚了。只是提到了storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)这个方法跟SmoothBursty和SmoothWarmingUp有关。所以你就能知道,这两种限流器实现类其实区别不大,核心区别就在于计算本次请求令牌需要等待的时间的方式(而且现在从代码中我们可以知道,严格来说,是等待获取当前令牌数可以供给的部分花的时间)是不同的。
此外,细心的读者之前也许还发现了另一个疑点:“我们之前create创建RateLimiter的时候为什么只指定了令牌每秒生产速度(QPS)却没有指定令牌桶的容量呢?令牌桶容量是如何确定的?” 别急,对于这一问题,在我们学习完SmoothBursty和SmoothWarmingUp这两种限流器后,就也能得到解答了。
接下来,就来最后看看这两种限流器。
RateLimiter的两种限流器
本节开头的定义(有改动)、第一小节的试验直接引用自知乎大佬“严肃的白小白”的 https://zhuanlan.zhihu.com/p/439682111 一文
Guava对RateLimiter有两种实现方式。
- SmoothBursty:平滑突发限流,以稳定的速率生成令牌。SmoothBursty实现的限流器也可称为稳定限流器。
- SmoothWarmingUp:平滑预热限流,随着请求量的增加导致桶中令牌数目减少后,令牌生成速率会缓慢提升直到一个稳定的速率。下SmoothWarmingUp实现的限流器也可称为预热限流器。
SmoothBursty和SmoothWarmingUp其实都是SmoothRateLimiter的内部类,而且都继承了后者,而后者又直接继承了RateLimiter。因此它们都是RateLimiter的实现类。
它们的特点是:
- 稳定限流器支持处理突发请求。比如令牌桶现有令牌数为5,这时连续进行10个请求,则前5个请求会全部直接通过,没有等待时间,之后5个请求则每隔200毫秒通过一次。稳定限流器在令牌数充足时会让请求直接通过而无需等待。稳定限流器令牌桶容量就等于设定的QPS,切一初始化化出来令牌桶就是空的。
- 而预热限流器则只会让第一个请求直接通过,之后的请求都会有等待时间(无论令牌数充足与否),等待时间不断缩短,直到稳定在每隔200毫秒通过一次。预热限流器令牌量容量一般会比设定的QPS更大,且一初始化出来令牌桶就是满的。
试验:感受两种限流器的区别
先上一个简单的demo,直观的感受一下这两种限流器的差别。
/**
* 突发请求情况对比
*/
public static void compareBurstyRequest() {
RateLimiter burstyLimiter = RateLimiter.create(5);
RateLimiter warmingUpLimiter = RateLimiter.create(5, 2, TimeUnit.SECONDS);
DecimalFormat df = new DecimalFormat("0.00");
// 积攒1秒,让稳定限流器的令牌桶装满
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("稳定限流器");
IntStream.range(0, 10).forEach(a -> {
double acquire = burstyLimiter.acquire();
System.out.println("第" + a + "次请求等待时间:" + df.format(acquire));
});
System.out.println("预热限流器");
IntStream.range(0, 10).forEach(a -> {
double acquire = warmingUpLimiter.acquire();
System.out.println("第" + a + "次请求等待时间:" + df.format(acquire));
});
}
代码执行结果对比如下:
结果一目了然。预热限流器确实如我们刚才所说的一样:只会让第一个请求直接通过,之后的请求都会有等待时间(无论令牌数充足与否),等待时间不断缩短,直到稳定在每隔200毫秒通过一次。
PS:你也许还注意到了,在这批10个请求发起时,稳定限流器的令牌桶中有5个令牌(预热限流器不是这样,要想知道有几个,看完下节你就懂了)。但是观察稳定限流器的结果,可以看到其前6次令牌请求都没阻塞,为什么呢?答案不言而喻,第0~4次当然是直接发放令牌,而由于“预获取令牌模式”的存在,第5次则是“赊账”式地发放令牌,并进而导致了第6次请求的阻塞。
SmoothBursty和SmoothWarmingUp的思想
之前我们在分析获取令牌的源码时,曾提到过SmoothBursty和SmoothWarmingUp对于reserveEarliestAvailable()中调用的storedPermitsToWaitTime()有着不同的重写方式。
那么不妨从它们各自重写的storedPermitsToWaitTime()方法入手,先来看看。对了,别忘了我们为什么要看storedPermitsToWaitTime()方法。前面我们已经说过,桶中令牌不够时,两者都是“预获取令牌模式”。所以说,我们现在要考虑的是令牌够时的两者的区别,这种区别体现在两者各自重写的storedPermitsToWaitTime()方法中。
/* SmoothRateLimiter类中 */
// reserveEarliestAvailable() 方法负责计算满足本次请求令牌数可用的最早时间
@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;
}
// 发放当前令牌可以供给的部分所花的时间:SmoothBursty就自然的是0;SmoothWarmingUp则不是0,比较复杂
abstract long storedPermitsToWaitTime(double storedPermits, double permitsToTake);
/* SmoothBursty内部类中 */
// SmoothBursty相当耿直,相当自然:既然这些令牌够,那么我就直接发给请求线程就好了,不用让人家等。
@Override
long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
return 0L;
}
double coolDownIntervalMicros() {
return stableIntervalMicros; //stableIntervalMicros值节就是创建RateLimiter时传入的QPS的倒数
}
那么SmoothBursty的思想就是这样,甚至没啥好说的:如果桶中令牌够,直接发放,不要让线程被阻塞。
那对于SmoothWarmingUp 呢?
事实上,SmoothWarmingUp的方案就复杂很多了。为了避免让大家因为直接看代码而陷入深深的困惑,我们干脆直接先把 SmoothWarmingUp的令牌发放策略 说一下。
- SmoothWarmingUp的令牌发放策略比较特别,它发放已有的令牌也有阻塞时间,而且阻塞时间内是不生产新令牌的:
- 当桶中令牌数较少时(低于临界值thresholdPermits),此时发放一个现有令牌的阻塞时间就是最先传入的QPS的倒数 —— 我们把这个速度记做 coolDownInterval 或 stableInterval (解冻发放耗时/稳定发放耗时),而且,这个值的大小和令牌生产速率保持一致,因此这个值也可以被理解和使用为令牌生产速率。
- 当桶中令牌数满时(达到最大值maxPermits),此时发放一个现有令牌的阻塞时间就是3倍的 coolDownInterval —— 我们把这个速度记作 coldInterval(冻结发放速率)
- 而当令牌数处在thresholdPermits和maxPermits之间时,此时发放一个现有令牌的阻塞时间可通过两点间线性插值得到。
也许看文字还不是太清楚,那么你可以通过源码中官方画的这幅图来理解。
上图中的“warmup period”是整个梯形部分的总面积,代表令牌桶会经过多长时间后,令牌发放阻塞时间会由cold interval减为stable interval。也就是说上图中横纵轴围成的面积代表时间,这个稍微想一下应该能明白。
现在再来看代码应该就清楚很多了:
/* SmoothWarmingUp内部类中 */
// 发放当前令牌可以供给的部分所花的时间
@Override
long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
// 超过threshold部分的现有令牌数
double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
long micros = 0;
// 如果当前有超过threshold部分的令牌
if (availablePermitsAboveThreshold > 0.0) {
// 计算一下会从这部分令牌中拿多少
double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);
// 说白了,现有的令牌数和拿完部分之后的令牌数对应的interval都还在插值直线上,length就是算插值直线上两个点的纵坐标之和
// 换句话说,其实就是求一个梯形的上下底之和
double length = permitsToTime(availablePermitsAboveThreshold)
+ permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);
// 求梯形面积,这里的面积就是时长(结合横纵坐标物理意义理解)
micros = (long) (permitsAboveThresholdToTake * length / 2.0);
// 剩下的不能从超过threshold部分拿的令牌数(因为超过threshold部分的令牌数不够)
permitsToTake -= permitsAboveThresholdToTake;
}
// 这个算的就是左边矩形部分的面积了,也是时长
// 然后和之前的时长加起来,就是从现有令牌中取令牌的总时长
micros += (stableIntervalMicros * permitsToTake);
return micros;
}
private double permitsToTime(double permits) {
// slope就是上图中插值直线的斜率
return stableIntervalMicros + permits * slope;
}
不过你也许会好奇:上面的thresholdPermits是在哪里赋值的?maxPermits(最大令牌数)是在哪里赋值的?warmup period是在哪里赋值的?slope又是在哪里赋值的?为什么说coldInterval是coolDownInterval的3倍?
实际上他们是在创建RateLimiter时所定义的,可以看看下面的代码,顺便也是看看SmoothWarmingUp的创建过程:
/* RateLimiter类中 */
// 传入QPS(即每秒生产令牌数)、warmupPeriod(所以warmup period一开始就要你传入就定义了,这是人为定义的),来创建一个SmoothWarmingUp实现类的RateLimiter
public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) {
checkArgument(warmupPeriod >= 0, "warmupPeriod must not be negative: %s", warmupPeriod);
return create(
// 这个传入的3.0是什么呢?好像是不是coldInterval与coolDownInterval的比值呢?往下看
SleepingStopwatch.createFromSystemTimer(), permitsPerSecond, warmupPeriod, unit, 3.0);
}
// 重载创建方法:可以看到其中的参数coldFactor,是的,它就是定义oldInterval与coolDownInterval的比值的参数,被写死为3.0
@VisibleForTesting
static RateLimiter create(
SleepingStopwatch stopwatch,
double permitsPerSecond,
long warmupPeriod,
TimeUnit unit,
double coldFactor) {
// 调用构造器new出来SmoothWarmingUp对象
RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);
// setRate方法很关键,thresholdPermits、maxPermits、slope都是从这里面定义出来的
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
// setRate方法,其中关键的是doSetRate方法
public final void setRate(double permitsPerSecond) {
checkArgument(
permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
synchronized (mutex()) {
// doSetRate是一个抽象方法,会先在SmoothRateLimiter中实现
doSetRate(permitsPerSecond, stopwatch.readMicros());
}
}
abstract void doSetRate(double permitsPerSecond, long nowMicros);
/* SmoothRateLimiter类 */
// SmoothRateLimiter类中实现的doSetRate方法
@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {
// 初始时先初始化一下storedPermits和nextFreeTicketMicros
resync(nowMicros);
// 初始化stableIntervalMicros
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
// 两个子类对doSetRate其实有不同的实现方式,这个同名的doSetRate主要负责初始化赋值maxPermits等操作(SmoothWarmingUp还要初始化coldInterval、thresholdPermits等)
doSetRate(permitsPerSecond, stableIntervalMicros);
}
// 由SmoothBursty和SmoothWarmingUp实现
abstract void doSetRate(double permitsPerSecond, double stableIntervalMicros);
/* SmoothWarmingUp类 */
// 所使用的构造器
SmoothWarmingUp(
SleepingStopwatch stopwatch, long warmupPeriod, TimeUnit timeUnit, double coldFactor) {
super(stopwatch);
this.warmupPeriodMicros = timeUnit.toMicros(warmupPeriod);
this.coldFactor = coldFactor;
}
// SmoothWarmingUp类中实现的doSetRate方法
// 其中初始化赋值了coldIntervalMicros、thresholdPermits、maxPermits、slope
@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = maxPermits;
// 现在你看到了吧,coldInterval = 3 * stableInterval
double coldIntervalMicros = stableIntervalMicros * coldFactor;
// 这是咋来的?是这样的,结合上面那个坐标图看,作者在注释中说了假定矩形部分面积是梯形部分面积(warmupPeriodMicros)的一半,那么自然就有这样的计算公式了
thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;
// 看着复杂,其实就是根据梯形面积计算公式移项得来的
// 不过这样看来,SmoothWarmingUp的令牌桶容量maxPermits就比较复杂了,一般而言算出来会比每秒的QPS大一些
// 感兴趣的读者可以算一下,比如用刚才试验中的例子,QPS=5,warmupPeriod=2s,此时算出来maxPermits是10,而thresholdPermits是5
maxPermits =
thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros);
// 高一学的直线斜率公式
slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);
// 下面这个不重要,知道一般情况下出实话时桶中令牌数就是最大令牌数就够了(和SmoothBursty不同)
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
storedPermits = 0.0;
} else {
storedPermits =
(oldMaxPermits == 0.0)
? maxPermits // initial state is cold
: storedPermits * maxPermits / oldMaxPermits;
}
}
这里关于那个坐标图各种计算面积什么数学变换我没有细讲,感兴趣的看看大佬的文章https://zhuanlan.zhihu.com/p/439682111。
总之只要理解了坐标图面积代表时间,那么我觉得就行了。有兴趣的可以像大佬一样去推一下各种公式。
那既然看了SmoothWarmingUp的创建过程,最后也顺便把SmoothBursty的创建过程看了吧:
/* RateLimiter类中 */
// 传入QPS(即每秒生产令牌数)、warmupPeriod(所以warmup period一开始就要你传入就定义了),来创建一个SmoothWarmingUp实现类的RateLimiter
public static RateLimiter create(double permitsPerSecond) {
return create(SleepingStopwatch.createFromSystemTimer(), permitsPerSecond);
}
// 重载创建方法
@VisibleForTesting
static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
// 还是这个setRate是关键咯,maxPermits就是这里面定义出来的
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
// setRate方法,其中关键的是doSetRate方法
public final void setRate(double permitsPerSecond) {
checkArgument(
permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
synchronized (mutex()) {
// doSetRate是一个抽象方法,会先在SmoothRateLimiter中实现
doSetRate(permitsPerSecond, stopwatch.readMicros());
}
}
/* SmoothRateLimiter类 */
// SmoothRateLimiter类中实现的doSetRate方法
@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {
// 初始时先初始化一下storedPermits和nextFreeTicketMicros
resync(nowMicros);
// 初始化stableIntervalMicros
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
// 两个子类对doSetRate其实有不同的实现方式,这个同名的doSetRate主要负责初始化赋值maxPermits等操作(SmoothWarmingUp还要初始化coldInterval、thresholdPermits等)
doSetRate(permitsPerSecond, stableIntervalMicros);
}
// 由SmoothBursty和SmoothWarmingUp实现
abstract void doSetRate(double permitsPerSecond, double stableIntervalMicros);
/* SmoothBursty类 */
// 所使用的构造器
SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {
super(stopwatch);
// 前面这里传入的是写死的1.0
this.maxBurstSeconds = maxBurstSeconds;
}
// SmoothBursty类中实现的doSetRate方法
// 其中初始化赋值了maxPermits
@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = this.maxPermits;
// maxBurstSeconds写死为1.0,所以对于SmoothBursty类,其令牌桶最大容量maxPermits等于令牌每秒生产速度QPS
maxPermits = maxBurstSeconds * permitsPerSecond;
// 下面这个不重要,知道初始化时令牌数一般就是0就够了(和SmoothWarmingUp不同)
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;
}
}
现在我们就讲完了SmoothBursty和SmoothWarmingUp两者的思想和区别,我觉得有几个点可以可以小结一下:
- SmoothBursty和SmoothWarmingUp是RateLimiter的两个实现类。
- SmoothBursty思想更加简单淳朴,只要桶中令牌数够发,就直接发,不会阻塞请求线程。而且初始化后桶中令牌数为0。
- SmoothWarmingUp思想更为复杂。即使桶中令牌数够,一般情况下(除了首次请求),都要阻塞请求线程。而且是令牌数越多,阻塞时间越长(超过thresholdPermits时)。不过SmoothWarmingUp的令牌桶容量一般会比SmoothBursty大一点。而且初始化后桶中令牌数为最大令牌数maxPermits。
- 两者都适用于“预获取令牌模式”,“预获取令牌模式”会在桶中令牌数不够时发挥作用。
SmoothBursty和SmoothWarmingUp的应用场景
最后有必要提一下两者的应用场景。这一部分我没有直接想到好的解释,不过看到了一篇博文中不错的总结,我直接搬过来(有改动):
引用自:https://blog.csdn.net/qq_39470742/article/details/122247481(作者:翻身已碰头),有改动
- SmoothBursty
- SmoothBursty对于令牌消耗是没有时间的,只要库存令牌充足,限流器对于流量不会有任何延迟效果。
- 因此,对于流量平稳的场景,没有突发大流量,SmoothBursty可以满足需要,可以保证流量按照指定的速率(每秒QPS个)通过。但是对于流量波动场景,譬如极端下,库存达到了上线maxPermits,一旦这时候有突发流量,那么一秒钟的流量通过数目将是:maxPermits(因为此时storedPermits就是maxPermits)+QPS = 2QPS(因为对SmoothBursty有maxPermits=QPS)。而且,此时是一瞬间久有maxPermits个令牌被瞬间发完,对服务机器的压力极大,可能会出现承受不住的情况,
- SmoothWarmingUp
- SmoothWarmingUp则不同,不但生成令牌需要时间,而且即使令牌充足,对于令牌消耗也需要一定的时间。正是通过这种方式,可以保证在突发流量场景下,不会出现大量流量一次性通过压垮服务的场景。因为即使最快的发放令牌,一秒钟也只能发QPS个。在桶中令牌很多的情况下,这个发放数量会更低,桶中令牌变少了才升高,正好能让服务机器有缓冲期慢慢应对强度更高的服务请求。
总结
简单总结一下本文中需要着重关注的点:
-
三种限流算法
-
如何创建并使用RateLimiter?
-
RateLimiter获取令牌的基本流程是怎样的?(RateLimiter是如何实现令牌桶算法的?)
请求线程acquire请求获取令牌 -> 计算桶中令牌数 -> 计算请求得到令牌的时间 -> 计算出来离当前差多久 -> 请求线程阻塞等待这么久的时间
-
RateLimiter的“预获取令牌模式”需要深入理解
对比“赊账”理解
-
RateLimiter的两种限流器的最大区别是什么?具体表现是怎样的?
关键在于“计算请求得到的令牌的时间”是不同的。根本原因在于SmoothBursty在令牌够的情况下发放令牌完全无需阻塞,而SmoothWarmingUp则无论在什么情况下发放令牌都有阻塞时间。
-
两种限流器的应用场景
SmoothBursty适用于无突发流量场景(因为SmoothBursty可能会一次性瞬间发放大量令牌),SmoothWarmingUo适用于有突发流量场景(因为SmoothBursty不仅发令牌会耗时,而且桶中令牌数越多耗时越长,给足机器动员起来的缓冲时间)
总的来说,RateLimiter其实算不上很好理解。如果要想吃透,还需要花点功夫。
本文中引用了一共两篇其他博文,最后在列出来一次,读者可以辅助看看,相信能帮助你更好的理解 Guava RateLimiter:
- https://zhuanlan.zhihu.com/p/439682111
- https://blog.csdn.net/qq_39470742/article/details/122247481