sentinel中也使用了漏桶算法作为对资源流控的一种手段,其对应的就是sentinel的dashcoard中的“等待排队”这种流控模式,这里我们就重点看一下sentinel源码中对于漏桶算法的实现。
一.什么是漏桶算法
漏桶算法是限流算法的一种。从图中我们可以看到,整个算法其实十分简单。首先,我们有一个固定容量的桶,有水流进来,也有水流出去。对于流进来的水来说,我们无法预计一共有多少水会流进来,也无法预计水流的速度。但是对于流出去的水来说,这个桶可以固定水流出的速率。而且,当桶满了之后,多余的水将会溢出。我们将算法中的水换成实际应用中的请求,我们可以看到漏桶算法天生就限制了请求的速度。当使用了漏桶算法,我们可以保证接口会以一个常速速率来处理请求。
优点:漏桶算法能够限制一个接口以固定的频率被请求,超过此频率的请求会被流控,利用这种特性我们可以在调用某些第三方的api的时候使用它,因为第三方的系统我们是不能够去修改到它们的限流规则的,所以只能够由我们在调用的时候去进行限流。所以漏桶算法能够保护第三方系统不被打垮。
缺点:漏桶算法不能够处理突发的流量,因为它只能够以固定的频率去处理,超过这个频率的请求将会被抛弃,所以一般对于突发流量的我们都会使用令牌桶算法去对接口进行保护。
二.Sentinel对漏桶算法的实现
com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
/**
* 基于漏桶算法
*
* @author jialiang.linjl
*/
public class RateLimiterController implements TrafficShapingController {
/**
* 最大等待超时时间,默认500ms
*/
private final int maxQueueingTimeMs;
/**
* 限流数量
*/
private final double count;
/**
* 上一次请求的通过时间
*/
private final AtomicLong latestPassedTime = new AtomicLong(-1);
public RateLimiterController(int timeOut, double count) {
this.maxQueueingTimeMs = timeOut;
this.count = count;
}
@Override
public boolean canPass(Node node, int acquireCount) {
return canPass(node, acquireCount, false);
}
@Override
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.
// 根据设置的qps计算出每个请求固定的处理速率
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
// Expected pass time of this request.
// 计算出消费当前请求的时间点
long expectedTime = costTime + latestPassedTime.get();
// 条件成立:说明当前已经到达了要处理当前请求的时间点了,这时候可以直接返回true表示可以处理当前请求
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 {
// 每一个请求都可以获取到自己可以被处理的时间点(想象一下这里如果有大量的请求进来,通过CAS每一个请求都会得到一个正确的处理时间点,相当于在排队等待...)
long oldTime = latestPassedTime.addAndGet(costTime);
try {
// 再次计算出当前请求需要等待的时间
waitTime = oldTime - TimeUtil.currentTimeMillis();
// 条件成立:超过了最大等待时间了,这里为什么还要再次判断一下?上面不是判断过了吗?
// 这是因为上面的判断只是适用于当前已经有线程在排好队的场景,都排好队了就可以算出当前线程还要排多久了(类似于你看到一个很长的队伍你就可以预估排队要多久了)
// 但是如果此时是一瞬间的流量突发进来,此时这些请求都是没有进行排好队的,所以上面的判断就派不上用场了,需要这些请求排好队之后再进行一次判断才行了
if (waitTime > maxQueueingTimeMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
// in race condition waitTime may <= 0
// 在上面CAS中有可能排队的时间耗时比较长,排好队时早就已经到达请求自身可处理的时间点了,所以这里有可能waitTime <= 0
if (waitTime > 0) {
// 等待
Thread.sleep(waitTime);
}
// 代码执行到这里说明线程等待完了,或者waitTime <= 0
// waitTime <= 0了就不用等待了(因为此时早就已经到请求自身的处理时间点了)
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}
}
- 根据设置的qps计算出每个请求的固定处理频率,也就算是每个请求最多处理时长
- 从lastestPassedTime取出上一次请求的通过时间,然后加上上面计算出的请求处理时长,如果小于当前时间,就说明这个请求能够在处理频率内,此时就把当前时间设置到lastestPassedTime中,然后请求正常通过,反之则不能通过
- 计算出当前请求要等待的时间,与规定的最大等待时间相比,如果超过最大等待时间(可以理解为超过桶的最大容量了),则不予通过,反之执行第4点
- 把请求需要等待的时间通过cas加到lastestPassedTime中,获取到累加的结果,再与最大等待时间比较,如果超过最大等待时间了(这一次比较与上面一次的比较作用的场景不一样,详细看上面代码的注释),则不予通过并且同时对lastestPassedTime减回等待的时间,反之执行第5点
- 通过Thread.sleep()给当前请求线程进行睡眠,睡眠的时间是上面计算的等待时间
- 当线程睡眠醒来之后直接返回true,使得请求正常执行