11. 流控FlowSlot源码解析

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;
    }

}

流控流程图

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值