常见的限流算法
1、计数器(固定窗口)算法
计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。
此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松实现。
对于秒级以上的时间周期来说,会存在一个非常严重的问题,那就是临界问题,如下图:
假设1min内服务器的负载能力为100,因此一个周期的访问量限制在100,然而在第一个周期的最后5秒和下一个周期的开始5秒时间段内,分别涌入100的访问量,虽然没有超过每个周期的限制量,但是整体上10秒内已达到200的访问量,已远远超过服务器的负载能力,由此可见,计数器算法方式限流对于周期比较长的限流,存在很大的弊端。
2、滑动窗口算法
限流中的滑动窗口可以简单理解为,设定的单位时间就是一个窗口,窗口可以分割多个更小的时间单元,随着时间的推移,窗口会向右移动。比如一个接口一分钟限制调用1000次,1分钟就可以理解为一个窗口,可以把1分钟分割为4个单元格,每个单元格就是15秒。每个时间片段都有独立的计数器,我们在计算整个时间窗口内的请求总数时会累加所有的时间片段内的计数器。时间窗口划分的越细,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
可以把这些小单元看作一个环,可以用一个数组存储,比如:A[0]、A[1]、A[2]、A[3],用一个变量k表示当前窗口,再用一个变量t表示当前窗口的开始时刻,数组的每个元素保存一个计数器,初始时,k=0,t等于当前时刻,因为滑动窗口是1分钟,所以每个小窗口间隔是15秒,收到请求时,先找当前窗口,n=(当前时间 - t)/15作为当前窗口往后多少个窗口,假如n=0,则请求还是落在当前窗口,然后累加4个窗口的计数器,如果小于限流预设值,则可以进去,当前窗口的计数器加1,如果窗口发生了移动,也就是n不等于0,则当前窗口往后直到新当前窗口的计数器要清零,比如n=1,则新当前窗口k=k+1,也就是1,则下标为1的窗口的计数器要清零,然后再累加四个窗口的计数器判断是否能进去,同理,如果n=2,则下标为1和2的窗口的计数器都要清零,如果(当前时间-t)大于1分钟,也就是当前窗口一次性移动了4格或以上,此时所有窗口计数器都都要清零。假如限流为100,A[0]=60,A[1]=20,A[2]=10,当前窗口k=3,则当前窗口的时间范围内,只能再接收10个请求,超过10则限流,直到下个时间窗口也就是k=0时才能重新接收请求,因为A[1]=20,A[2]=10,A[3]=10,所以k=0时,最多只能接收60个请求,可以看出,不会出现固定窗口算法中的临界问题。
3、漏桶算法
漏桶算法是访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。
4、令牌桶算法
令牌桶算法生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的,当桶中无令牌可用时,则请求丢弃,这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌。
RateLimiter是基于令牌桶实现的开源工具,下面是RateLimiter的原理及使用。
Guava 提供了两种 RateLimiter 的实现:
- SmoothBursty
- SmoothWarmingUp
1)RateLimiter对于持续生成令牌,采用的不是定时任务的方式(过于耗费资源,不适合高并发),而是使用延迟计算的方式,即在获取令牌时计算上一次时间nextFreeTicketMicros和当前时间之间的差值,计算这段时间之内按照用户设定的速率可以生产多少令牌;
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;//修改时间
}
}
- storedPermits 当前许可的数量
- maxPermits 许可数量的最大值
- nextFreeTicketMicros 用于标记下次请求获得许可的时间,其更新的逻辑在 reserveEarliestAvailable中
2)RateLimiter计算延迟时间,即过度消费部分需要由下一次调用买单,因此其具有处理突发流量的能力
reserveEarliestAvailable是一个抽象方法,其实现逻辑在 SmoothRateLimiter 中如下:
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
resync(nowMicros);//调用resync方法生成令牌
long returnValue = nextFreeTicketMicros; // 返回的是上次计算的nextFreeTicketMicros
double storedPermitsToSpend = min(requiredPermits, this.storedPermits); // 可以消费的最大令牌数
double freshPermits = requiredPermits - storedPermitsToSpend; // 还需要的令牌数(若需要的大于已有的令牌,则下次请求需要延时等待;否则此值为0,即不需要等待)
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros); // SmoothBursty为0(SmoothWarmingUp具体计算)+需要等待时间
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); // 计算此次用了这么多令牌后,下次需要等待到什么时候
this.storedPermits -= storedPermitsToSpend;//减少消耗掉的令牌
return returnValue;
}
stableIntervalMicros 两次请求之间的时间差。例如每秒5个许可,那么该值为200ms。
如果每次都只取一个令牌,则requiredPermits=1,则只要可消费令牌数storedPermitsToSpend>=1即可,否则等待一个令牌。
在 SmoothBursty 中,storedPermitsToWaitTime 为 0,也就是说对于已经生成的许可,不需要再等待。
3)父类tryAcquire 方法,对于延迟,RateLimiter使用StopWatch实现不可中断挂起
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;
}
private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
可见当 timeout 为 0 时, microsToWait 总是为 0,所以不会造成阻塞。
queryEarliestAvailable 用于计算最早可以获取许可的时间点,如果该时间点大于 timeout,那么返回 false,也就是无法获得许可。queryEarliestAvailable 是抽象方法,在 SmoothRateLimiter 中也就是nextFreeTicketMicros:
@Override
final long queryEarliestAvailable(long nowMicros) {
return nextFreeTicketMicros;
}
reserveAndGetWaitLength返回获得令牌需要等待多长时间,即nextFreeTicketMicros(下次请求获得许可的时间)减去当前时间,如果小于零则无需等待。从resync方法看到,如果当前时间大于nextFreeTicketMicros,则nextFreeTicketMicros=当前时间,而reserveEarliestAvailable返回的是nextFreeTicketMicros,reserveAndGetWaitLength返回0,即无需等待,如果当前时间小于nextFreeTicketMicros,resync方法不会起作用,通过reserveEarliestAvailable方法看到,会重新计算nextFreeTicketMicros=nextFreeTicketMicros+申请的令牌数*令牌间隔时间,虽然reserveEarliestAvailable返回的是重新计算之前的nextFreeTicketMicros,但原来的nextFreeTicketMicros是比当前时间大的,所以还是会等待,但等待的时间并不是下一个令牌生成的时间,而是提前消费一个未生成的令牌,但并不是一定能提前消费令牌的,从canAcquire方法可知,如果timeout=0,则nextFreeTicketMicros大于当前时间时即返回false,而不会等待,而且等待的时间(nextFreeTicketMicros-当前时间)不能比超时时间大。
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
4)剩下的就是3个细节了:
maxPermits计算:
SmoothBursty 中代码如下:
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = this.maxPermits;
maxPermits = maxBurstSeconds * permitsPerSecond;
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;
}
}
maxBurstSeconds 默认为 1.0,所以 SmoothBursty 最大突发 permitsPerSecond 个许可
在 SmoothWarmingUp 中实现如下:
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;
}
}
coolDownIntervalMicros
在 SmoothBursty 中,coolDownIntervalMicros 就是 stableIntervalMicros,也就是说匀速生成许可。
在 SmoothWarmingUp 中实现如下:
@Override
double coolDownIntervalMicros() {
return warmupPeriodMicros / maxPermits;
}
storedPermitsToWaitTime
在 SmoothBursty 中,storedPermitsToWaitTime 为 0,也就是说对于已经生成的许可,不需要再等待。
在 SmoothWarmingUp 中实现如下:
@Override
long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
long micros = 0;
// measuring the integral on the right part of the function (the climbing line)
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;
}
// measuring the integral on the left part of the function (the horizontal line)
micros += (long) (stableIntervalMicros * permitsToTake);
return micros;
}
private double permitsToTime(double permits) {
return stableIntervalMicros + permits * slope;
}
总的来说,SmoothBursty 比较容易理解,而 SmoothWarmingUp 添加了预热机制,代码不太直观,可以配合下图来理解代码。
/**
* ^ 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
*/
许可数量从 thresholdPermits 降至 0 的时间周期等于 warmupPeriod/2,因此 thresholdPermits = 0.5 * warmupPeriod / stableInterval
。
从 maxPermits 降至 thresholdPermits 的时间周期等于 warmupPeriod,因此 maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval)
。
coolDownIntervalMicros 方法控制许可的生成速度,也就是在 warmupPeriod 周期匀速生成 maxPermits 个许可。
对于已经生成的许可,storedPermitsToWaitTime 方法增加了等待时间,计算方式也就是对上图求个积分,初始态在x轴的右边,然后向左移动,也就是说一开始等待时间比较大,然后逐渐减小,最后平稳。
由于 coldFactor 硬编码为 3.0, 所以预热的“斜率”由 warmupPeriod 控制,warmupPeriod 越大,慢启动速率衰减越平缓,达到稳定的时间也比较长。
以上内容来自https://www.jianshu.com/p/4a78088252f0