本文主要来解析Sentinel限流核心源码,基于当前最新的release版本1.8.0
1、常见限流算法
1)、计数器算法
在指定周期内累加访问次数,当访问次数达到设定的阈值时,触发限流策略,当进入下一个时间周期清零访问次数
如上图所示,限定了每一分钟能够处理的总请求数为100,在第一个一分钟内,一共请求了60次。接着到第二个一分钟,counter又从0开始计数,在一分半钟时,已经达到了最大限流的阈值,这个时候后续的所有请求都会被拒绝
2)、滑动窗口算法
滑动窗口算法的原理是在固定窗口中分割出多个小时间窗口,分别在每个小时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内的所有小时间窗口总的计数即可
如上图所示,将一分钟拆分成4个小时间窗口,每个小时间窗口最多能够处理25个请求。并且通过虚线框表示滑动窗口的大小(当前窗口的大小是2,也就是在这个窗口内最多能够处理50个请求)。同时滑动窗口会随着时间往前移动,比如前面1.5s结束之后,窗口会滑动到1.5~4.5s这个范围,然后在新的窗口中重新统计数据
Sentinel就是采用滑动窗口来实现限流的
3)、漏斗算法
漏斗算法主要作用是控制数据流入网络的速度,平滑网络上的突发流量
在漏斗算法内部维护一个容器,这个容器会以恒定速度出水,不管上面的水流速度多快,漏斗流出水的速度始终保持不变
在漏斗算法中,存在以下几种可能的情况:
- 请求速度大于漏斗流出水的速度,也就是请求数超出当前服务所能处理的极限,将会触发限流策略
- 请求速度小于或者等于漏斗流出水的速度,也就是服务端的处理能力满足客户端的请求量,将正常执行
漏斗算法是一种恒定速度的限流算法,无法处理短时间内的突发流量
4)、令牌桶算法
令牌桶算法对于每一个请求,都需要从令牌桶中获得一个令牌,如果没有获得令牌,则触发限流策略
系统会以一个恒定速度往固定容量的令牌桶中放入令牌,如果此时有客户端请求过来,则需要先从令牌桶中拿到令牌以获得访问资格
假设令牌桶生成速度是每秒10个,此时在请求获取令牌的时候,会存在以下三种情况:
- 请求速度大于令牌生成速度,那么令牌会很快地取完,后续再进来的请求会被限流
- 请求速度等于令牌生成速度,此时流量处于平稳状态
- 请求速度小于令牌生成速度,说明此时系统的并发数并不高,请求能被正常处理
由于令牌桶有固定的大小,当请求速度小于令牌生成速度时,令牌桶会被填满。所以令牌桶能够处理突发流量,也就是在短时间内新增的流量系统能够正常处理
2、源码解析
Sentinel的熔断是由责任链中的FlowSlot来实现的
@SpiOrder(-2000)
public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
private final FlowRuleChecker checker;
public FlowSlot() {
this(new FlowRuleChecker());
}
FlowSlot(FlowRuleChecker checker) {
AssertUtil.notNull(checker, "flow checker should not be null");
this.checker = checker;
}
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
//检查是否能够限流通过
checkFlow(resourceWrapper, context, node, count, prioritized);
//调用责任链下游的Slot的entry
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
throws BlockException {
checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}
FlowSlot调用了FlowRuleChecker的checkFlow()
方法:
public class FlowRuleChecker {
public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
if (ruleProvider == null || resource == null) {
return;
}
//获取调用的resource对应的所有限流规则
Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
if (rules != null) {
//逐个规则判断是否触发限流操作 触发限流,直接抛出FlowException
for (FlowRule rule : rules) {
if (!canPassCheck(rule, context, node, count, prioritized)) {
throw new FlowException(rule.getLimitApp(), rule);
}
}
}
}
public boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node, int acquireCount,
boolean prioritized) {
String limitApp = rule.getLimitApp();
if (limitApp == null) {
return true;
}
//集群流控
if (rule.isClusterMode()) {
return passClusterCheck(rule, context, node, acquireCount, prioritized);
}
//单机流控
return passLocalCheck(rule, context, node, acquireCount, prioritized);
}
private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
boolean prioritized) {
//根据请求选择节点
Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
if (selectedNode == null) {
return true;
}
//根据配置FlowRule配置的controlBehavior(流控效果:直接拒绝、排队等待、慢启动模式)选择不同的Controller,判断是否通过
return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
rule.getRater()
返回的类型是TrafficShapingController,TrafficShapingController继承关系图如下:
下面主要来解析两种相对简单的策略:DefaultController和RateLimiterController
1)、DefaultController
在DefaultController中,首先获取当前的线程数或者QPS数,如果当前的线程数或者QPS+申请的数量>配置的总数,则不通过,如果当前线程数或者QPS+申请的数量<=配置的总数,则直接通过
public class DefaultController implements TrafficShapingController {
private static final int DEFAULT_AVG_USED_TOKENS = 0;
private double count;
private int grade;
public DefaultController(double count, int grade) {
this.count = count;
this.grade = grade;
}
@Override
public boolean canPass(Node node, int acquireCount) {
return canPass(node, acquireCount, false);
}
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
//获取当前node节点的线程数或者请求的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);
throw new PriorityWaitException(waitInMs);
}
}
return false;
}
return true;
}
//获取当前node节点的线程数或者请求的qps总数
private int avgUsedTokens(Node node) {
if (node == null) {
return DEFAULT_AVG_USED_TOKENS;
}
return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
}
2)、RateLimiterController
RateLimiterControlle使用latestPassedTime属性来记录最后一次通过的时间,然后根据规则中QPS的限制,计算当前请求是否可以通过。它在Sentinel中的流控效果定义为排队等待
举个例子:设置QPS为10,那么每100毫秒允许通过一个,通过计算当前时间是否已经过了上一个请求的通过时间latestPassedTime之后的100毫秒,来判断是否可以通过。假设才过了50ms,那么需要当前线程再sleep 50ms,然后才可以通过。如果同时有另一个请求,那需要sleep 150ms
public class RateLimiterController implements TrafficShapingController {
//排队最大时长,默认500ms
private final int maxQueueingTimeMs;
//QPS设置的值
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);
}
//通常acquireCount为1,prioritized为false
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
if (acquireCount <= 0) {
return true;
}
if (count <= 0) {
return false;
}
long currentTime = TimeUtil.currentTimeMillis();
//计算每2个请求之间的间隔,比如QPS限制为10,那么间隔就是100ms
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
//预期本次请求的时间=时间间隔+上一个请求的通过时间
long expectedTime = costTime + latestPassedTime.get();
//可以通过,设置latestPassedTime然后就返回true了
if (expectedTime <= currentTime) {
//设置上一个请求的通过时间 这里可能存在并发问题
//Contention may exist here, but it's okay.
latestPassedTime.set(currentTime);
return true;
} else {
//不可以通过,需要等待
long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
//等待时长大于最大值,返回false
if (waitTime > maxQueueingTimeMs) {
return false;
} else {
//将latestPassedTime往前推
long oldTime = latestPassedTime.addAndGet(costTime);
try {
//需要sleep的时间
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;
}
参考: