面试老被问限流算法在Sentinel中的应用和实现细节,似懂非懂回答不上来的痛苦,家人们谁懂啊!之前笔者浅谈了限流算法,今天,笔者就从Sentinel源码入手看看这神秘的限流算法是如何实现的
使用过Sentinel的同学可能都知道,Sentinel有三种流控效果:直接拒绝、Warm Up和排队等待
这三种流控效果在源码中的实现如下,相应的有四个实现类,接下来就一一看看它们是如何实现的
直接拒绝
直接拒绝的实现类DefaultController,核心代码如下
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// 以qps为例,获取当前的秒级qps
// Sentinel是如何统计当前的秒级qps的?当然是滑动时间窗限流算法
int curCount = avgUsedTokens(node);
// 看有没有超过设定的限流阈值
if (curCount + acquireCount > count) {
// 这里有种特殊情况,允许占用下一秒的请求资源
// 也就是说当前秒的请求数已用完,可以将当前请求阻塞到下一秒,以减少采用拒绝访问这种不太友好的处理方式
if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
long currentTime;
long waitInMs;
currentTime = TimeUtil.currentTimeMillis();
waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
node.addWaitingRequest(currentTime + waitInMs, acquireCount);
node.addOccupiedPass(acquireCount);
sleep(waitInMs);
// PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
throw new PriorityWaitException(waitInMs);
}
}
// 超过限流阈值则不放行
return false;
}
// 未超过限流阈值则放行
return true;
}
Warm Up
这里要先提提令牌桶算法借鉴了Google Guava的轻量级实现,里面蕴含了一个预热模型,笔者本着拿来主义从这篇博客中拿了个图供大家参考,如果侵权了请联系笔者删除,带着这个模型往下看,要来然会对下面的计算逻辑云里雾里,当然也可以忽略,只考虑算法的大致运行情况
记住图中第8条,这应该是预热模型设定的条件,关系不是很大挖个坑后续再查阅下资料
Warm Up的实现类是WarmUpController,也就是令牌桶限流算法的实现
其构造方法如下
private void construct(double count, int warmUpPeriodInSec, int coldFactor) {
if (coldFactor <= 1) {
throw new IllegalArgumentException("Cold factor should be larger than 1");
}
// 这里的count和stableInterval是有关系的,count*stableInterval=1s
this.count = count;
this.coldFactor = coldFactor;
// thresholdPermits = 0.5 * warmupPeriod / stableInterval.
// warningToken = 100;
// 利用1的面积来算
warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
// / maxPermits = thresholdPermits + 2 * warmupPeriod /
// (stableInterval + coldInterval)
// maxToken = 200
// 利用2的面积来算
maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
// slope
// slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);
slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
}
其核心代码如下
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// 获取当前秒的qps
long passQps = (long) node.passQps();
// 获取前一秒的qps
long previousQps = (long) node.previousPassQps();
// 生成令牌,这里实现也是很轻量级的,下面会详细看看它的实现
syncToken(previousQps);
// 要不要放行
long restToken = storedTokens.get();
if (restToken >= warningToken) {
long aboveToken = restToken - warningToken;
// current interval = restToken*slope+1/count
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
if (passQps + acquireCount <= warningQps) {
// 令牌数超出预警值,当时qps还未超出警告qps,放行
return true;
}
} else {
if (passQps + acquireCount <= count) {
// 令牌数未超出预警值,当时qps小于阈值,放行
return true;
}
}
// 除了以上两种情况外不放行
return false;
}
生成令牌的详细实现
protected void syncToken(long passQps) {
// 计算当前的时间
long currentTime = TimeUtil.currentTimeMillis();
currentTime = currentTime - currentTime % 1000;
// 上次生成令牌的时间
long oldLastFillTime = lastFilledTime.get();
if (currentTime <= oldLastFillTime) {
return;
}
// 桶中现存的令牌数
long oldValue = storedTokens.get();
// 根据当前时间和上次生成令牌的时间计算新的令牌数
long newValue = coolDownTokens(currentTime, passQps);
// 更改令牌桶的令牌数
if (storedTokens.compareAndSet(oldValue, newValue)) {
// 扣减上次时间窗的所需的令牌数
long currentValue = storedTokens.addAndGet(-passQps);
if (currentValue < 0) {
storedTokens.set(0L);
}
lastFilledTime.set(currentTime);
}
}
private long coolDownTokens(long currentTime, long passQps) {
// 当前令牌数
long oldValue = storedTokens.get();
long newValue = oldValue;
// 添加令牌的前提条件
if (oldValue < warningToken) {
// 当令牌数低于警戒线的时候
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);
}
}
return Math.min(newValue, maxToken);
}
可以想象当系统平时以低流量运行时,令牌数会填满令牌桶,当有突发流量进来,qps会慢慢增长,直到令牌生成速率达到最大
排队等待
排队等待的实现类是RateLimiterController,它实际上就是漏斗限流算法的实现,其核心代码如下
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// Pass when acquire count is less or equal than 0.
if (acquireCount <= 0) {
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) {
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;
}
这个代码逻辑比较简单,就不详细分析了,大致就是根据固定速率算出一个请求所需的时间,然后按照先来后到进行排序依次等待请求
还有一个就不分析了,就是Warm Up和排队等待的结合体,有兴趣的宝子们可以自行研究
如果写的有误欢迎批评指正,也欢迎和我沟通,一块学习进步
创作不易,感兴趣的兄弟集美可以点赞关注给兄弟点支持
如需转发,请注明出处