Sentinel源码解析之默认限流逻辑

要学习一个框架最主要的部分就是两块,一个是系统启动时如何初始化(参考: 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的设计扩展性极强

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值