令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务,令牌桶算法的限流的特点:让流量平稳,而不是瞬间流量,1000 QPS 相对平均的分摊在这一秒内,而不是第 1 ms 999 请求,后面 999 ms 0 请求。
Eureka的限流策略入口是在Eureka-Server的web.xml里,是个过滤器,使用的是RateLimiting,接下来来看看RateLimiting是怎么实现的。
<filter>
<filter-name>rateLimitingFilter</filter-name>
<filter-class>com.netflix.eureka.RateLimitingFilter</filter-class>
</filter>
RateLimiter 目前支持分钟级和秒级两种速率限制。
/**
* 支持分钟级和秒级的限流
*/
public RateLimiter(TimeUnit averageRateUnit) {
switch (averageRateUnit) {
case SECONDS:
rateToMsConversion = 1000;
break;
case MINUTES:
rateToMsConversion = 60 * 1000;
break;
default:
throw new IllegalArgumentException("TimeUnit of " + averageRateUnit + " is not supported");
}
}
调用 #acquire(...) 方法,获取令牌,并返回是否获取成功。
/**
* 获取令牌
*
* @param burstSize 令牌桶上限
* @param averageRate 令牌再装平均速率
* @return
*/
public boolean acquire(int burstSize, long averageRate) {
return acquire(burstSize, averageRate, System.currentTimeMillis());
}
public boolean acquire(int burstSize, long averageRate, long currentTimeMillis) {
if (burstSize <= 0 || averageRate <= 0) { // Instead of throwing exception, we just let all the traffic go
return true;
}
//填充令牌
refillToken(burstSize, averageRate, currentTimeMillis);
//消费令牌
return consumeToken(burstSize);
}
burstSize 参数 :令牌桶上限。
averageRate 参数 :令牌填充平均速率。
我们举个 例子来理解这两个参数 + 构造方法里的一个参数:
averageRateUnit = SECONDS
averageRate = 2000
burstSize = 10
每秒可获取 2000 个令牌。例如,每秒允许请求 2000 次。
每毫秒可填充 2000 / 1000 = 2 个消耗的令牌。
每毫秒可获取 10 个令牌。例如,每毫秒允许请求上限为 10 次,并且请求消耗掉的令牌,需要逐步填充。这里要注意下,虽然每毫秒允许请求上限为 10 次,这是在没有任何令牌被消耗的情况下,实际每秒允许请求依然是 2000 次。
以下是填充令牌和消费令牌源码。
/**
* 填充令牌
*
* @param burstSize 令牌桶上限
* @param averageRate 令牌填充平均速率
* @param currentTimeMillis
*/
private void refillToken(int burstSize, long averageRate, long currentTimeMillis) {
//获取最后填充令牌的时间
long refillTime = lastRefillTime.get();
//获取过去多少毫秒
long timeDelta = currentTimeMillis - refillTime;
//可填充最大令牌数
long newTokens = timeDelta * averageRate / rateToMsConversion;
if (newTokens > 0) {
//计算新的令牌填充时间
long newRefillTime = refillTime == 0
? currentTimeMillis
: refillTime + newTokens * rateToMsConversion / averageRate;
if (lastRefillTime.compareAndSet(refillTime, newRefillTime)) {
while (true) {
//计算 填充令牌后的已消耗令牌数量
int currentLevel = consumedTokens.get();
int adjustedLevel = Math.min(currentLevel, burstSize); // In case burstSize decreased
int newLevel = (int) Math.max(0, adjustedLevel - newTokens);
// CAS 避免和正在消费令牌的线程冲突
if (consumedTokens.compareAndSet(currentLevel, newLevel)) {
return;
}
}
}
}
}
/**
* 消费令牌
*
* @param burstSize 令牌桶上限
* @return
*/
private boolean consumeToken(int burstSize) {
// 死循环,直到没有令牌,或者获取令牌成功
while (true) {
int currentLevel = consumedTokens.get();
// 没有令牌
if (currentLevel >= burstSize) {
return false;
}
// CAS 避免和正在消费令牌或者填充令牌的线程冲突
if (consumedTokens.compareAndSet(currentLevel, currentLevel + 1)) {
return true;
}
}
}