流量整形控制器
上面我们已经介绍了默认的快速失败流量整形控制器DefaultController,本节我们介绍其他几个控制器
RateLimiterController
匀速通过,漏斗算法
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// Pass when acquire count is less or equal than 0.
if (acquireCount <= 0) {//请求通过数为0,直接返回true
return true;
}
// Reject when count is less or equal than 0.
// Otherwise,the costTime will be max of long and waitTime will overflow in some cases.
if (count <= 0) {//允许通过的大小小于0直接返回false
return false;
}
long currentTime = TimeUtil.currentTimeMillis();
// Calculate the interval between every two requests.
//计算两个请求之间的间隔时间
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
// Expected pass time of this request.
//此请求的预期通过时间
long expectedTime = costTime + latestPassedTime.get();
if (expectedTime <= currentTime) {//说明本应该发生的时间比这个当前时间小,那么说明可以通过
// Contention may exist here, but it's okay.
//这里可能存在竞争,但没关系
latestPassedTime.set(currentTime);
return true;
} else {
// Calculate the time to wait.
//计算需要等待的时间
long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
//如果需要等待的时间超过了最大等待时间则限流
if (waitTime > maxQueueingTimeMs) {
return false;
} else {
//本该执行的时间
long oldTime = latestPassedTime.addAndGet(costTime);
try {
//等待的时间
waitTime = oldTime - TimeUtil.currentTimeMillis();
if (waitTime > maxQueueingTimeMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
// in race condition waitTime may <= 0
if (waitTime > 0) {//线程阻塞等待
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}
这里首先计算两个请求之间的间隔时间costTime,通过costTime计算下次请求可以通过的时间expectedTime,如果expectedTime <= currentTime说明当前时间已经到达expectedTime ,则请求可以正常通过,否则说明当前时间请求需要等待,计算等待时间waitTime,如果waitTime比设置的最大等待时间大则直接不允许通过,如果没有达到最大排队时间则等待waitTime后通过。
WarmUpController
关注控制每秒传入请求的计数,类似于令牌桶算法
几个属性
protected double count;//可以通过的总数
private int coldFactor;//最小的冷加载因子
protected int warningToken = 0;//起始预热token数
private int maxToken;//最大token数
protected double slope;//斜率
//令牌桶
protected AtomicLong storedTokens = new AtomicLong(0);
//上一次填充时间
protected AtomicLong lastFilledTime = new AtomicLong(0);
构造函数
public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {
//count:预热流量控制阈值计数
//warmUpPeriodInSec:预热时间默认10s
//coldFactor:冷加热因子默认为3
construct(count, warmUpPeriodInSec, coldFactor);
}
private void construct(double count, int warmUpPeriodInSec, int coldFactor) {
if (coldFactor <= 1) {
throw new IllegalArgumentException("Cold factor should be larger than 1");
}
this.count = count;
this.coldFactor = coldFactor;
// warningToken = 50;
//起始预热token数量
warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
// maxToken = 100
//最大的token数
maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
//增长
slope = (coldFactor - 1.0) / count / (maxToken - warningToken);//2/10/50=0.004
}
检查是否可以通过
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long passQps = (long) node.passQps();
//最后一个窗口的QPS
long previousQps = (long) node.previousPassQps();
syncToken(previousQps);
// 开始计算它的斜率
// 如果进入了警戒线,开始调整他的qps
long restToken = storedTokens.get();
if (restToken >= warningToken) {//QPS不是很高
long aboveToken = restToken - warningToken;
// 消耗的速度要比warning快,但是要比慢
// current interval = restToken*slope+1/count
//计算当前通过的最大QPS
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
if (passQps + acquireCount <= warningQps) {//没有达到最大QPS,则允许通过
return true;
}
} else {
//当前系统处理高QPS状态
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
}
填充令牌桶数量
protected void syncToken(long passQps) {
//当前时间
long currentTime = TimeUtil.currentTimeMillis();
currentTime = currentTime - currentTime % 1000;
//上次填入的时间
long oldLastFillTime = lastFilledTime.get();
if (currentTime <= oldLastFillTime) {//间隔时间太小,不能添加token
return;
}
//原来的总token数
long oldValue = storedTokens.get();
//计算新的token数
long newValue = coolDownTokens(currentTime, passQps);
//修改总的token数
if (storedTokens.compareAndSet(oldValue, newValue)) {
//需要减去已经通过的QPS个数
long currentValue = storedTokens.addAndGet(0 - passQps);
if (currentValue < 0) {
storedTokens.set(0L);
}
//设置上面装填token的时间
lastFilledTime.set(currentTime);
}
}
private long coolDownTokens(long currentTime, long passQps) {
//原来的token数
long oldValue = storedTokens.get();
long newValue = oldValue;
// 添加令牌的判断前提条件:
// 当令牌的消耗程度远远低于警戒线的时候
if (oldValue < warningToken) {//说明系统的QPS很高
//新的token数
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
} else if (oldValue > warningToken) {
if (passQps < (int)count / coldFactor) {//说明系统的QPS很低,正在预热
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
}
}
//新的token数不能超过最大token数
return Math.min(newValue, maxToken);
}
这里我们假设count=10,warmUpPeriodInSec,coldFactor默认为3,可以计算出来warningToken=50而maxToken=100,首先计算一下距离上次放入token的时间,如果还没达到再次放入的时间则直接返回。获取剩余token的数量,如果比warningToken低,说明当前负载高则放入token,如果oldValue > warningToken说明系统负载低,正在预热,判断前一个时间窗通过的qps是否小于count / coldFactor,如果是则也放入令牌。计算warningQps这里我们假设aboveToken 为10,Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count))约等于7.14,当passQps + acquireCount <= warningQps时已经通过QPS和本地通过数量小于越热值才能通过。
WarmUpRateLimiterController
还有一个流量整形控制器是WarmUpRateLimiterController,融合了WarmUpController和RateLimiterController
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long previousQps = (long) node.previousPassQps();
//更新令牌数量
syncToken(previousQps);
long currentTime = TimeUtil.currentTimeMillis();
//令牌数量
long restToken = storedTokens.get();
long costTime = 0;
long expectedTime = 0;
//预热数量已经达到
if (restToken >= warningToken) {
//超过预热数量
long aboveToken = restToken - warningToken;
// current interval = restToken*slope+1/count
//计算预热可以通过的qps
double warmingQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
//请求通过间隔时间
costTime = Math.round(1.0 * (acquireCount) / warmingQps * 1000);
} else {
costTime = Math.round(1.0 * (acquireCount) / count * 1000);
}
//请求可以通过时间
expectedTime = costTime + latestPassedTime.get();
//时间已经到达,可以通过
if (expectedTime <= currentTime) {
latestPassedTime.set(currentTime);
return true;
} else {
//需要等待一段时间通过
long waitTime = costTime + latestPassedTime.get() - currentTime;
if (waitTime > timeoutInMs) {
return false;
} else {
long oldTime = latestPassedTime.addAndGet(costTime);
try {
waitTime = oldTime - TimeUtil.currentTimeMillis();
if (waitTime > timeoutInMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
//等待
if (waitTime > 0) {
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}