1 为什么需要限流
限流的目的非常简单就是对时间内的请求进行限制,从而保护系统不会应为过多量的请求使系统过载从而保证服务可用,一般限流的阈值由压测来进行统计。
2 常用限流算法
2.1 滑动窗口算法
一般会关注和思考限流算法的读者我觉得不用再科普为什么要使用滑动窗口算法来代固定窗口算法,因为滑动窗口算法对流量平滑的流入,在实际设计中基本上就只会使用滑动窗口算法。
以下图举例子来说,目前我们需要限制60s内,通过的QPS不能超过100。
假设我们的窗口拆分成了3个小窗口,小窗口都是20s,同样基于上面的例子,当在第三个20s的时候来了100个请求,可以通过。
当到了90秒内,在紫色窗口想流入100个请求,可见前两个窗口总共有100个请求,此时在窗口内有200个请求,因此新的100个请求会发限流。
在我要介绍Sentinel的框架中,滑动窗口思想会作为所有限流模式的基础,Sentinel会使用滑动窗口进行请求统计。
2.2 令牌桶算法
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶内添加令牌:
- 固定速率为令牌桶添加令牌
- 当桶满时候不再添加新的令牌
- 当请求有令牌的时候可以往后通过
- 当桶内没有令牌的时候则请求则被限流,无法通过
2.3 漏桶算法
漏桶算法拥有一个固定容量的漏桶,按照固定速率通过请求:
- 流入端的请求流入速率在算法本身没有限制
- 如果超出桶的容量的请求会被丢弃,限流
- 请求按照既定速率下潜通过
3 使用Sentinel的限流
本篇博文是关于限流,作为服务保障框架Hystrix只有熔断降级功能,因此不会进行讨论,我这次要对比的是Resilience4j
Sentinel | Resilience4j | |
流量整形 | 简单限流(基于滑动窗口)、令牌桶、漏桶 | Rate Limiter(常用令牌桶) |
单机限流 | 基于 QPS,支持基于调用关系的限流 | Rate Limiter |
多语言支持 | Java/Go/C++ | Java |
整合难度 | 基于注解 | 基于注解 |
动态规则配置 | 多动态数据源 | 有限支持 |
控制台 | 提供较为完整的原生控制台开源代码,可以定制化用于生产 | 没有原生控制台 |
目前在Java中提供比较完整的限流,目前除了有阿里开源的Sentinel外,还有Spring官方推荐的Resilience4j,单从使用角度来看Sentinel拆箱可用,更适合快速投入生产的框架,后面有时间我会出根据对Resilience4j仔细的使用进行更细致的对比。
4 结合Sentinel讨论限流算法在实际使用的思考
我这里放一下Sentinel中Warm Up和排队等待功能的源码
Warm Up-WarmUpController
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long passQps = (long) node.passQps();
long previousQps = (long) node.previousPassQps();
syncToken(previousQps);//令牌生产
long restToken = storedTokens.get();
if (restToken >= warningToken) {//在预热环境下当前剩余令牌作出如何选择
long aboveToken = restToken - warningToken;
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
if (passQps + acquireCount <= warningQps) {
return true;
}
} else {
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
}
Sentinel官方对Warm Up的描述
Warm Up(
RuleConstant.CONTROL_BEHAVIOR_WARM_UP
)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
详细的代码分析我会在后续限流博客进行解读,这本篇博文我们只要知道Sentinel在预热功能基于令牌桶算法进行开发。
排队等待-RateLimiterController
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
if (acquireCount <= 0) {
return true;
}
if (count <= 0) {
return false;
}
long currentTime = TimeUtil.currentTimeMillis();
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
long expectedTime = costTime + latestPassedTime.get();
if (expectedTime <= currentTime) {
//理想的情况是每个请求在队列中排队通过,那么每个请求都在固定的不重叠的时间通过。
latestPassedTime.set(currentTime);
return true;
} else {
//预期通过时间如果超过当前时间那就休眠等待,需要等待的时间等于预期通过时间减去当前时间
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;
}
//休眠等待,实现请求按指定速率流出
if (waitTime > 0) {
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}
Sentinel官方对匀速排队的描述
匀速排队(
RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
)方式会严格控制请求通过的间隔时间。该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
详细的代码分析我会在后续限流博客进行解读,这本篇博文我们只要知道Sentinel在预热功能基于漏桶算法进行开发即刻。
从一个大的时间统计刻度来看漏桶算法和命令牌算法通过的请求总数可能会是一样的。但是从毫秒的视距来看,漏斗算法通过漏斗可以缓存一部分请求在漏斗当中保证请求匀速进行消费,而令牌桶算法则会让令牌桶量的请求通过,再根据令牌消费速率进行消费。再从丢弃的角度来看,只要用足够大的漏斗容量我们就可以承接足够的多的请求,而一般情况下令牌桶生成令牌的速率是比较慢的,因此丢失的请求会比较多。因此我们得到漏斗算法的本质是总量控制,我们关注的是漏斗算法在遇到突发请求的时候通过漏斗最大限度的缓存请求。令牌桶的本质是速率控制,我们关注是令牌桶在请求消费中,对请求的的消费控制。因此比较合适法设置方式在流量总入口使用漏桶算法进行总量控制保证最多的接受请求,再在服务与服务之间使用命令牌算法进行速率控制。
5 结语
Sentinel作为目前比较完善的服务保障框架,从源码书写到框架设计都有很多值得分享的东西,后续我会通过更多系列博文来记录我对它的理解。