11. 流控FlowSlot源码解析
介绍
这个slot 对应我们在Dashboard中设定的流控规则, 主要根据预设的规则资料信息来进行流控,按照固定的次序,依次生效。如果一个资源对应两条或者多条流控规则,则会根据如以下次序依次检验,直到全部通过或者有一个规则生效为止:
- 指定应用生效的规则,即针对调用方限流的;
- 调用方为 other 的规则;
- 调用方为 default 的规则。
代码解析
public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
private final FlowRuleChecker 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);
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);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
private final Function<String, Collection<FlowRule>> ruleProvider = new Function<String, Collection<FlowRule>>() {
@Override
public Collection<FlowRule> apply(String resource) {
// Flow rule map should not be null.
Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();
return flowRules.get(resource);
}
};
}
从代码可以看出主要是通过FlowRuleChecker.checkFlow来执行的流控逻辑, 通过ruleProvider获取流控规则, 流控规则是通过Dashboard控制台配置的。
关于是如何获取流控规则,这块我们后面在解析,本章不做分析
FlowRuleChecker
我们进到FlowRuleChecker进行解析,通过以下代码,我们可以看出逻辑是根据资源获取到流控规则,然后逐个便利流控规则进行canPassCheck检查,如果检查失败,则抛出FlowException,而FlowException是继承自BlockedException,所以如果抛出FlowException则代表流控生效
public class FlowRuleChecker {
public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
...
Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
if (rules != null) {
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) {
// limitApp也就是配置的针对来源,默认为:default
String limitApp = rule.getLimitApp();
...
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);
...
//通过获取rule.getRater调用其canPass来检查
//这里的rule.getRater方法返回的对象代表的是流控的效果,有快速失败,WarmUp和排队等待
//这里我们通过默认的快速失败来解析
return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
...
}
流控效果代码解析
快速失败DefaultController
从上面的分析来看,快速失败是通过rule.getRater()方法获取到的流控效果控制器类DefaultController,也是默认的流控效果。 我们把对应的类拉出来看看
public class DefaultController implements TrafficShapingController {
...
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// 根据设置的规则获取当前已经通过的QPS或并发线程数
// 这里获取的数据是涉及到了滑动窗口中的当前时间窗口数据
int curCount = avgUsedTokens(node);
// 【核心逻辑】如果当前的数量+待申请的数量 > 设置的单机阈值 则需要进行流控
if (curCount + acquireCount > count) {
...
return false;
}
// 不需要流控
return true;
}
private int avgUsedTokens(Node node) {
if (node == null) {
return DEFAULT_AVG_USED_TOKENS;
}
//如果设置为并发线程数,则返回当前的并发线程数,否则返回通过的Qps
return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
}
...
}
预热WarmUpController
概念:Warm Up方式,即预热/冷启动方式。该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况。
预热公式:初始阈值= 设定阈值/coldFactor(默认值为3),经过预热一段时间后才会达到设定的阈值。
预热的访问过程系统允许通过的QPS曲线如下图:
public class WarmUpController implements TrafficShapingController {
protected double count;
private int coldFactor;
protected int warningToken = 0;
private int maxToken;
protected double slope;
...
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// 获取当前1s的QPS
long passQps = (long) node.passQps();
// 获取上一窗口通过的qps
long previousQps = (long) node.previousPassQps();
// 生成和滑落token
syncToken(previousQps);
long restToken = storedTokens.get();
// 如果令牌桶中的token数量大于警戒值,说明还未预热结束,需要判断token的生成速度和消费速度
if (restToken >= warningToken) {
long aboveToken = restToken - warningToken;
// 计算此时1s内能够生成token的数量
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
// 判断token消费速度是否小于生成速度,如果是则正常请求,否则限流
if (passQps + acquireCount <= warningQps) {
return true;
}
} else {
// 预热结束,直接判断是否超过设置的阈值
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
}
}
推荐参考文章:Sentinel中冷启动限流原理WarmUpController_@Kong的博客-CSDN博客_sentinel warmup
排队等待RateLimiterController
排队等待(匀速器):匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
官方文档:flow-control
概念:匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求(削峰填谷)。
它的中心思想是,以固定的间隔时间让请求通过。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过。否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过(排队等待处理);若预期的通过时间超出最大排队时长,则直接拒接这个请求。
例图:
-public class RateLimiterController implements TrafficShapingController {
private final int maxQueueingTimeMs;
private final double count;
private final AtomicLong latestPassedTime = new AtomicLong(-1);
...
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// 如果没有申请请求数量则放过
if (acquireCount <= 0) {
return true;
}
// 如果设置的单机阈值小于等于0 等于是都不能访问,所以直接限流
if (count <= 0) {
return false;
}
long currentTime = TimeUtil.currentTimeMillis();
// 计算本次流控中一次请求需消耗的时间的毫秒数
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
// 计算请求预期通过时间毫秒数
long expectedTime = costTime + latestPassedTime.get();
// 如果请求预期通过时间小于当前时间
// 说明系统已经闲置了一段时间了,需要多放过一些请求,才能达到设定的QPS
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;
}
}