当我设计RateLimiter的时候我在想些什么?(原文:How is the RateLimiter designed, and why?)

本文其实对google开源的guava库中的SmoothRateLimiter函数的设计思路的翻译和解析。

当我设计RateLimiter的时候我在想些什么?

  RateLimiter最重要的特征是能够提供稳定的速率,也就是在正常情况下可以允许使用的最大速率。这就要求我们对于想要进来的请求进行限流。比如,让希望进入的线程进行等待。

  一个最为简单的思路实现限制QPS的速度是保存最后一个请求的时间戳,并且保证在下一个请求到来之前已经过去了1/QPS秒。例如我们creat(5),我们需要保证上一个请求是在200ms之前,这样就可以达到我们预期的速度。如果前一个请求是100ms之前的,那我们需要限制当前线程等待100ms。

  显然这个方法存在对于过去的请求记忆非常浅的问题,换句话说,RateLimiter只能记着最后的请求的时间。如果在过去的很长一段时间RateLimiter都没有被使用,这时来了一个请求,我们是否应该立刻批准呢?上述的这种简单的设计思路会忽略过去没有被充分利用这一事实。这有可能导致利用率不足或者过载。

  从一方面来说,过去的资源没有得到充分的利用表明系统负载不满,RateLimiter应该允许更多的流量进来从而充分利用资源。

  另一方面,过去未充分利用资源也可能意味着服务器处于冷状态,无法迅速的承接未来将要到来的大流量,服务器需要进行预热启动。

  为了处理这个场景,我们引入了变量storedPermits,当资源被充分利用时,storedPermits=0;当未被充分利用,会逐渐积累直到达到上限maxStoredPermits。因此,当我们请求许可证时候(调用acquire函数),如果有存储的permit会从存储的中扣减,然后再等待新发的许可证。

  我们用一个例子来展现具体的流程:

  假设RateLimiter的每一秒生成一个令牌,当RateLimiter没有被使用时候,我们对storedPermits加一。当RateLimiter闲置10s时候,storedPermits已经变为10;此时,有一个acquire(3)的请求到达,我们使用storedPermits中的令牌满足这个请求,并将其减少到7。假设紧接着来了一个acquire(10)的请求,我们只能部分使用storedPermits中的7个令牌满足,额外需要的三个令牌由RateLimiter新生成满足。

  我们知道RateLimiter为满足要求新生产三个令牌的时间是3s。那么通过积累,分配请求的7个令牌会对系统有什么影响呢?如果我们希望充分利用资源,那我们希望积累的这部分许可证比新生成许可证更快。如果我们更担心系统的安全性,那我们希望将积累的令牌分发的速度比正常生成新的令牌更慢一些,以希望给系统一些缓冲的空间。因此我们需要一个函数来将积累的令牌转换为节流时间。

  这部分工作是由storedPermitsToWaitTime(double storedPermits, double permitsToTake)函数完成的,函数的底层是一个连续函数,考虑斜率是1/rate,将storedPermits映射到连续函数上。storedPermits本质上表明了空闲时间。考虑请求的许可/时间=1/rate。因此,“ 1 /比率”(时间/许可)乘以“许可”给出的时间,即,对于指定数量的许可,此函数的积分对应于后续请求之间的最小间隔。

  这里给出一个storedPermitsToWaitTime执行的例子,如果storedPermits == 10.0,并且我们希望得到3个请求,我们可以直接从storedPermits中获取,并将storedPermits 扣减为7;并且计算收到流量限制而应该被等待的时间storedPermitsToWaitTime(storedPermits = 10.0, permitsToTake = 3.0),这等于对函数从7积分到10。

  使用积分的方法可以保证一次获取3个令牌的行为与分三次每次获取一个令牌的行为是一致的。无论这个映射函数是什么。这样保证了我们可以公平的处理不同的许可,而不用在意函数的具体形式和参数。

  需要注意的是,如果我们选择了一条高度恰好为1/QPS的水平线,那么这个函数的的积分效果将与新生成许可证的成本完全一样。如果我们令该函数是一个高度低于1/QPS的水平线,相当于我们缩小了函数的面积,也就是略微加快了令牌的获取速度。如果我们令该函数是一个高度高于1/QPS的水平线,相当于我们增大了函数的面积,也就是略微放慢了令牌的获取速度。

  最后,我们考虑一种特殊情况。假设RateLimiter的限速qps是1,并且目前没有被完全的使用,此时来了一个请求100个令牌的超大请求。如果我们要求这个这个请求等待100s然后再运行时显然不好的一个选择。我们可以让这个请求先运行,然后推迟后续到来的请求。也就是说,我们可以立即运行这个请求,但是后续再有新的请求想要执行就需要等待100s了。显然这个方法更好。

  为了实现这个功能,这意味着RateLimiter应该记住的不是前一个请求的时间,而是下一个请求可以开始执行的时间。这也就意味着我们可以立即判断一个请求是否可以运行,我们只需要比较这个请求到来的时间和下一个请求可以开始的时间即可。类似的,我们也可以用这个方法判断当前的RateLimiter有没有被充分使用。如果我们发现下一个请求可以执行的时间早于当前时间说明系统是不饱和的,这个时间差也就是系统空闲的时间。通过这个时间我们可以计算出积累的令牌数量。

某种冷启动模型

在这里插入图片描述

  在深入了解这个特定函数的细节之前,我们需要有一些简单的定义。当RateLimiter没有被使用时候,是横轴的最右侧;当被使用时候,会一直左移直到成为0。我们会先使用之前积累的令牌。当RateLimiter没有被使用时,我们会逐渐右移直到最大值。当被使用的时候,需要等待的时间是积分,对于warm up区域,其实就是梯形的面积。对于存储总是一个稳定的时间间隔。

  也就是说,当积累的令牌数量小于最大令牌数量的一半时候,我们的速度是和新生成令牌的速度是一样的。

  当积累的令牌数量大于一般时候,这部分时间会更长。我们会通过调整斜率从而实现面积等于指定的热身时间。

  我们定义热机时间是从存储max到存储half的时间,冷却时间是从0到max的时间。

  但是我们需要注意的是,我们只需要热机时间的一半就可以将令牌的积累从half累计到max,这是因为的梯形面积是等宽矩形的两倍。如果我们决定让冷却时间等于热身时间,那其实就是max*interval = 梯形面积。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值