要学习一个框架最主要的部分就是两块,一个是系统启动时如何初始化(参考: Sentinel源码解析之链式初始化),另外一个就是核心runtime流程,本篇文章重点讲runtime流程中的限流部分
核心流程
基于对初始化和官方快速开始文档的学习,我们知道需要通过这句话SphU.entry(KEY)来发起Sentinel调用,调用流程中会经过多个slot,其中限流相关的主要就是FlowSlot,先后又经过了FlowRuleChecker、DefaultController(默认情况,根据)、StatisticNode
核心数据结构
与理解限流逻辑请牢记下图环形滑动窗口及如下概念
1、一个环表示统计时间周期环即intervalInMs,默认1s
2、一个窗口是把一个时间周期环切成sampleCount份,每份代表一个时间窗口
FlowSlot核心方法解析
这个是做限流控制的槽,通过FlowRuleChecker的checkflow实现限流
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);
}
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;
}
// 找到对应资源的限流规则,判断是否能够通过限流控制
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) {
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;
}
return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
DefaultController核心方法解析
1、当前周期请求数+申请数,如果未超过限流值,返回true
2、当前周期请求数+申请数,如果超过限流值,判断优先级和限流规则
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// 获取当前周期(默认1s)请求数
int curCount = avgUsedTokens(node);
// 1、当前周期请求数+申请数,如果未超过限流值,返回true
// 2、当前周期请求数+申请数,如果超过限流值,判断优先级和限流规则
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;
}
StatisticNode核心方法解析
tryOccupyNext方法的核心思想
就是提前借用下一个时间窗口的buffer来用,那么如何借用呢?
这里设计非常巧妙,利用环形滑动窗口的特点,当一整个时间周期qps达到限流值的时候,到下一个窗口去借,那么可以借多少呢? 答案是上一次这个窗口承接了多少请求数,就可以借多少,如果还不够就再往下一个窗口借,这样能够保证整个时间周期(整个环)请求数不超过预设限流值
关键代码就是等待时间和超限判断的配合
等待时间计算
long waitInMs = idx * windowLength + windowLength - currentTime % windowLength;
idx为0,则代表需等待当前时间所在窗口的剩余时间windowLength - currentTime % windowLength;
,为什么是这样呢??
意思就是到当前窗口结束时间正是下一个窗口的开始时间,那么时间周期超QPS限制了,那么就在下一个窗口一开始时,就把要申请的acquireCount全部申请完
超限判断
if (currentPass + currentBorrow + acquireCount - windowPass <= maxCount) { return waitInMs; }
超限判断也很好理解,当前通过数+当前借用的数+申请数-下一个窗口在上一个周期的数 < 限流值
下一个窗口在上一个周期的数怎么取呢?
我们知道数据结构上是环形的,所以可以往前找,也可以向后找,只要找到对应的索引就行,这里作者是往前找的,即 先找到当前窗口的结束时间(下一个窗口的开始时间)
:currentTime - currentTime % windowLength + windowLength,再减去一个时间周期IntervalProperty.INTERVAL
long earliestTime = currentTime - currentTime % windowLength + windowLength - IntervalProperty.INTERVAL;
tryOccupyNext全部代码如下:
public long tryOccupyNext(long currentTime, int acquireCount, double threshold) {
double maxCount = threshold * IntervalProperty.INTERVAL / 1000;
long currentBorrow = rollingCounterInSecond.waiting();
if (currentBorrow >= maxCount) {
return OccupyTimeoutProperty.getOccupyTimeout();
}
int windowLength = IntervalProperty.INTERVAL / SampleCountProperty.SAMPLE_COUNT;
long earliestTime = currentTime - currentTime % windowLength + windowLength - IntervalProperty.INTERVAL;
int idx = 0;
/*
* Note: here {@code currentPass} may be less than it really is NOW, because time difference
* since call rollingCounterInSecond.pass(). So in high concurrency, the following code may
* lead more tokens be borrowed.
*/
long currentPass = rollingCounterInSecond.pass();
while (earliestTime < currentTime) {
long waitInMs = idx * windowLength + windowLength - currentTime % windowLength;
if (waitInMs >= OccupyTimeoutProperty.getOccupyTimeout()) {
break;
}
long windowPass = rollingCounterInSecond.getWindowPass(earliestTime);
if (currentPass + currentBorrow + acquireCount - windowPass <= maxCount) {
return waitInMs;
}
earliestTime += windowLength;
currentPass -= windowPass;
idx++;
}
return OccupyTimeoutProperty.getOccupyTimeout();
}
最后再补充一下控制器选择
FlowPropertyListener
规则变化监听器,用来监听规则配置的变化,具体如何触发更新或加载改天再讲吧,只要知道规则变化和初始化都会调用FlowRuleUtil.buildFlowRuleMap(value),用来创建新的FlowRule规则及rule对应的控制器TrafficShapingController
private static final class FlowPropertyListener implements PropertyListener<List<FlowRule>> {
@Override
public synchronized void configUpdate(List<FlowRule> value) {
Map<String, List<FlowRule>> rules = FlowRuleUtil.buildFlowRuleMap(value);
flowRules.updateRules(rules);
RecordLog.info("[FlowRuleManager] Flow rules received: {}", rules);
}
@Override
public synchronized void configLoad(List<FlowRule> conf) {
Map<String, List<FlowRule>> rules = FlowRuleUtil.buildFlowRuleMap(conf);
flowRules.updateRules(rules);
RecordLog.info("[FlowRuleManager] Flow rules loaded: {}", rules);
}
}
public static final int CONTROL_BEHAVIOR_DEFAULT = 0;
public static final int CONTROL_BEHAVIOR_WARM_UP = 1;
public static final int CONTROL_BEHAVIOR_RATE_LIMITER = 2;
public static final int CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER = 3;
FlowRuleUtil
默认是使用的我们文中主要讲解的DefaultController,继承自TrafficShapingController,作为rule的一个属性通过setRater做关联
// 生成限流控制器
private static TrafficShapingController generateRater(/*@Valid*/ FlowRule rule) {
if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
switch (rule.getControlBehavior()) {
case RuleConstant.CONTROL_BEHAVIOR_WARM_UP:
return new WarmUpController(rule.getCount(), rule.getWarmUpPeriodSec(),
ColdFactorProperty.coldFactor);
case RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER:
return new ThrottlingController(rule.getMaxQueueingTimeMs(), rule.getCount());
case RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER:
return new WarmUpRateLimiterController(rule.getCount(), rule.getWarmUpPeriodSec(),
rule.getMaxQueueingTimeMs(), ColdFactorProperty.coldFactor);
case RuleConstant.CONTROL_BEHAVIOR_DEFAULT:
default:
// Default mode or unknown mode: default traffic shaping controller (fast-reject).
}
}
return new DefaultController(rule.getCount(), rule.getGrade());
}
// 根据FlowRule构造加载规则,并设置限流控制器
public static <K> Map<K, List<FlowRule>> buildFlowRuleMap(List<FlowRule> list, Function<FlowRule, K> groupFunction,
Predicate<FlowRule> filter, boolean shouldSort) {
Map<K, List<FlowRule>> newRuleMap = new ConcurrentHashMap<>();
if (list == null || list.isEmpty()) {
return newRuleMap;
}
Map<K, Set<FlowRule>> tmpMap = new ConcurrentHashMap<>();
for (FlowRule rule : list) {
if (!isValidRule(rule)) {
RecordLog.warn("[FlowRuleManager] Ignoring invalid flow rule when loading new flow rules: " + rule);
continue;
}
if (filter != null && !filter.test(rule)) {
continue;
}
if (StringUtil.isBlank(rule.getLimitApp())) {
rule.setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);
}
TrafficShapingController rater = generateRater(rule);
rule.setRater(rater);
K key = groupFunction.apply(rule);
if (key == null) {
continue;
}
Set<FlowRule> flowRules = tmpMap.get(key);
if (flowRules == null) {
// Use hash set here to remove duplicate rules.
flowRules = new HashSet<>();
tmpMap.put(key, flowRules);
}
flowRules.add(rule);
}
Comparator<FlowRule> comparator = new FlowRuleComparator();
for (Entry<K, Set<FlowRule>> entries : tmpMap.entrySet()) {
List<FlowRule> rules = new ArrayList<>(entries.getValue());
if (shouldSort) {
// Sort the rules.
Collections.sort(rules, comparator);
}
newRuleMap.put(entries.getKey(), rules);
}
return newRuleMap;
}
总结
Sentinel通过精妙的环形滑动窗口数据结构设计,把限流控制的相对比较精确,使用SlotChain的设计扩展性极强