限流降级与流量控制(上篇):FlowSlot核心原理

图片[1]-限流降级与流量控制(上篇):FlowSlot核心原理 - 速优课-速优课

限流降级与流量控制(上篇):FlowSlot核心原理

前言

前面我们花了三篇文章,把Sentinel的指标统计体系完整地讲了一遍。从这篇开始,我们正式进入功能模块的分析。

第一个要讲的,就是最核心、最常用的限流降级——也就是FlowSlot。

限流是Sentinel最基础也是最重要的功能之一。这篇我们先从整体架构入手,看看限流规则是怎么管理的、FlowSlot是怎么工作的、以及限流检查的完整流程。

一、限流功能的整体架构

在看具体代码之前,我们先搞清楚Sentinel限流功能的整体设计。

Sentinel的各种降级功能(限流、熔断、系统保护等),基本上都是由这几个角色配合完成的:

角色作用限流对应的类
ProcessorSlot责任链切入点,调用Checker做检查FlowSlot
Checker规则检查器,负责具体的判断逻辑FlowRuleChecker
Rule规则配置类,保存阈值、策略等配置FlowRule
RuleManager规则管理器,缓存和加载规则FlowRuleManager

整体流程可以总结为三步:

  1. FlowSlot.entry() 被调用,拿到当前资源的DefaultNode
  2. FlowRuleChecker 从FlowRuleManager取出该资源的所有限流规则
  3. 逐条规则检查,用ClusterNode的实时指标和规则阈值对比,达到阈值就抛出FlowException

就这么简单。下面我们一个个角色来看。

二、限流规则:FlowRule

2.1 Rule接口

先从最顶层的Rule接口说起。

Sentinel最初设计的时候,把”判断请求是否能通过”这个行为交给了Rule自己,所以定义了Rule接口:

public interface Rule {
    boolean passCheck(Context context, DefaultNode node, int count, Object... args);
}

参数说明:

  • context:调用链路上下文
  • node:当前资源的DefaultNode
  • count:申请数量(一般是1,令牌桶算法里就是申请几个令牌)
  • args:方法调用参数(热点参数限流会用到)

2.2 AbstractRule抽象类

因为规则都是围绕资源配置的,所以有个抽象类AbstractRule:

public abstract class AbstractRule implements Rule {
    private String resource;   // 资源名称
    private String limitApp;  // 对哪个调用来源生效
}
  • resource:规则作用在哪个资源上
  • limitApp:只对某个调用来源生效,default表示不区分来源

各种规则的继承关系:

图片[2]-限流降级与流量控制(上篇):FlowSlot核心原理 - 速优课-速优课

2.3 FlowRule限流规则

FlowRule是限流规则的配置类,字段比较多,我们一个个来看:

public class FlowRule extends AbstractRule {
    // 限流阈值类型:QPS或线程数
    private int grade = RuleConstant.FLOW_GRADE_QPS;
    // 限流阈值
    private double count;
    // 基于调用关系的限流策略
    private int strategy = RuleConstant.STRATEGY_DIRECT;
    // 引用资源名称(配合strategy使用)
    private String refResource;
    // 流量控制效果:直接拒绝、Warm Up、匀速排队
    private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;
    // 冷启动时长(秒)
    private int warmUpPeriodSec = 10;
    // 最大排队时间(毫秒)
    private int maxQueueingTimeMs = 500;
    // 流量控制器
    private TrafficShapingController controller;
    
    @Override
    public boolean passCheck(Context context, DefaultNode node, 
                             int acquireCount, Object... args) {
        return true;
    }
}

注意一个细节:FlowRule的passCheck方法直接返回true

也就是说,Sentinel自己都没遵守最初的设计约定——passCheck的逻辑并不在Rule里实现,而是放到了Checker里。这应该是框架演进过程中的历史遗留问题。

2.4 FlowRuleManager规则管理器

规则的加载和缓存由FlowRuleManager负责:

public class FlowRuleManager {
    // 缓存规则:key=资源名称,value=该资源的所有限流规则
    private static final Map<String, List<FlowRule>> flowRules = 
        new ConcurrentHashMap<String, List<FlowRule>>();
    
    static Map<String, List<FlowRule>> getFlowRuleMap() {
        return flowRules;
    }
    
    // 加载/更新规则
    public static void loadRules(List<FlowRule> rules) {
        // 更新静态字段flowRules
    }
}

几个关键点:

  • 用ConcurrentHashMap缓存:key是资源名,value是规则列表
  • 一个资源可以有多条规则:只要有一条规则触发限流,就拒绝请求
  • loadRules方法更新规则:先清空再写入,动态生效

三、FlowSlot:限流切入点

FlowSlot是限流功能在责任链中的切入点。代码很简单:

public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    private final FlowRuleChecker checker;
    
    public FlowSlot() {
        this(new FlowRuleChecker());
    }
    
    // 规则提供者:根据资源名获取规则列表
    private final Function<String, Collection<FlowRule>> ruleProvider = 
        new Function<String, Collection<FlowRule>>() {
            @Override
            public Collection<FlowRule> apply(String resource) {
                Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();
                return flowRules.get(resource);
            }
        };
​
    @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
        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);
    }
}

FlowSlot做的事情很简单:

  1. 构造方法里创建FlowRuleChecker
  2. entry方法里调用checker.checkFlow()做限流检查
  3. 检查通过就继续往下传,不通过就抛异常

注意这里的ruleProvider——它是一个Function接口,封装了”根据资源名获取规则列表”这个行为。这样FlowRuleChecker就不需要直接依赖FlowRuleManager了,耦合度更低。

四、FlowRuleChecker:限流检查器

FlowRuleChecker是真正做限流判断的地方。我们顺着调用链一步步往下看。

4.1 checkFlow:入口方法

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;
    }
    // 1. 获取当前资源的所有限流规则
    Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
    if (rules != null) {
        // 2. 遍历每条规则
        for (FlowRule rule : rules) {
            // 3. 检查是否能通过
            if (!canPassCheck(rule, context, node, count, prioritized)) {
                // 4. 通不过就抛FlowException
                throw new FlowException(rule.getLimitApp(), rule);
            }
        }
    }
}

逻辑很清晰:

  1. 拿到该资源的所有限流规则
  2. 逐条检查
  3. 只要有一条规则不通过,就抛FlowException(BlockException的子类)

为什么一个资源可以有多条规则?因为你可以同时配置多种限流策略,比如:QPS不超过100,同时并发线程数不超过20。两条规则同时生效,哪个先到就触发哪个。

4.2 canPassCheck:判断是否通过

public boolean canPassCheck(FlowRule rule, Context context, DefaultNode node, 
                            int acquireCount, boolean prioritized) {
    // 1. limitApp为空直接通过
    String limitApp = rule.getLimitApp();
    if (limitApp == null) {
        return true;
    }
    // 2. 集群限流模式
    if (rule.isClusterMode()) {
        return passClusterCheck(rule, context, node, acquireCount, prioritized);
    }
    // 3. 单机限流模式
    return passLocalCheck(rule, context, node, acquireCount, prioritized);
}

三种情况:

  • limitApp为空 → 直接通过(一般不会出现,默认是default)
  • 集群模式 → 走集群限流检查(后面再讲)
  • 单机模式 → 走单机限流检查

4.3 passLocalCheck:单机限流检查

private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node,
                                      int acquireCount, boolean prioritized) {
    // 1. 根据策略选择要检查的Node
    Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
    if (selectedNode == null) {
        return true;
    }
    // 2. 拿到流量控制器,调用canPass
    return rule.getRater()
               .canPass(selectedNode, acquireCount, prioritized);
}

两步走:

  1. 选Node:根据limitApp和strategy,选择用哪个Node的统计数据来判断
  2. 调用控制器:拿到TrafficShapingController(流量效果控制器),调用它的canPass方法

TrafficShapingController就是我们常说的”流量效果控制器”——直接拒绝、冷启动、匀速排队,都是由它来实现的。这个我们后面几篇会详细讲。

重点来看第一步:怎么选Node?


五、选择统计节点:selectNodeByRequesterAndStrategy

这个方法是限流检查里最复杂的一个方法,因为它要处理多种组合情况。

static Node selectNodeByRequesterAndStrategy(FlowRule rule, Context context, DefaultNode node) {
    String limitApp = rule.getLimitApp();    // 限制哪个来源
    int strategy = rule.getStrategy();       // 限流策略
    String origin = context.getOrigin();     // 当前调用来源
    
    // 情况1:当前来源正好是规则指定的来源
    if (limitApp.equals(origin) && filterOrigin(origin)) {
        if (strategy == RuleConstant.STRATEGY_DIRECT) {
            return context.getOriginNode();          // (1) 直接限流:用来源Node
        }
        return selectReferenceNode(rule, context, node); // (2) 关联/链路限流
    }
    // 情况2:规则是default(不区分来源)
    else if (RuleConstant.LIMIT_APP_DEFAULT.equals(limitApp)) {
        if (strategy == RuleConstant.STRATEGY_DIRECT) {
            return node.getClusterNode();             // (3) 直接限流:用ClusterNode
        }
        return selectReferenceNode(rule, context, node); // (4) 关联/链路限流
    }
    // 情况3:规则是other(其他来源)
    else if (RuleConstant.LIMIT_APP_OTHER.equals(limitApp)
        && FlowRuleManager.isOtherOrigin(origin, rule.getResource())) {
        if (strategy == RuleConstant.STRATEGY_DIRECT) {
            return context.getOriginNode();           // (5) 直接限流:用来源Node
        }
        return selectReferenceNode(rule, context, node); // (6) 关联/链路限流
    }
    
    return null;
}

看起来有点晕,我们来梳理一下。

5.1 三个limitApp选项

limitApp有三种可能的值:

limitApp值含义
default不区分来源,对所有调用者生效
具体的来源名(如service-a)只对这个来源生效
other对其他来源生效(除了已经单独配置的)

5.2 三个限流策略

strategy也有三种:

策略含义说明
STRATEGY_DIRECT直接限流用当前资源的指标数据判断
STRATEGY_RELATE关联限流用另一个资源(refResource)的指标数据判断
STRATEGY_CHAIN链路限流用调用链路上的DefaultNode判断

3 × 3 = 9种组合?其实不是,因为有些组合最终走的是同一个逻辑。简化一下,核心是这6种情况:

序号limitAppstrategy使用的Node说明
1指定来源DIRECTorigin的StatisticNode只限制某个来源的QPS
2指定来源RELATE/CHAIN引用资源/链路Node对某个来源,按关联资源限流
3defaultDIRECTClusterNode最常用:全局限流
4defaultRELATE/CHAIN引用资源/链路Node全局按关联资源限流
5otherDIRECTorigin的StatisticNode对”其他来源”限流
6otherRELATE/CHAIN引用资源/链路Node对”其他来源”按关联资源限流

5.3 为什么需要这么多策略?

你可能会问:搞这么复杂,有必要吗?

我们举几个实际场景,你就明白了。

场景1:按调用来源限流

你的服务同时被服务A和服务B调用。服务A是核心业务,服务B是非核心业务。你想限制服务B的QPS,但不限制服务A。

这时候就可以配置两条规则:

  • limitApp = service-b,count = 50(限制服务B最多50 QPS)
  • limitApp = default,count = 500(总共最多500 QPS)

场景2:关联限流(写优先)

你的服务有两个接口:

  • 读接口:queryOrder
  • 写接口:createOrder

两个接口都操作同一张表,写操作优先级更高。你希望写操作太多的时候,限制读操作。

这时候给读接口配置:

  • strategy = STRATEGY_RELATE
  • refResource = createOrder
  • count = 100

意思是:当写接口的QPS超过100时,读接口就限流。实现写优先。

图片[3]-限流降级与流量控制(上篇):FlowSlot核心原理 - 速优课-速优课

场景3:链路限流

同一个接口可能从不同的入口进来。比如:

  • 从Web入口进来调用的 → 链路A
  • 从Dubbo入口进来调用的 → 链路B

你只想限制从Web入口进来的流量,不想限制Dubbo的。

这时候用链路限流(STRATEGY_CHAIN),它用的是DefaultNode的数据——不同链路的DefaultNode是分开的。

这也解释了为什么Sentinel要为同一个资源创建多个DefaultNode——就是为了支持按链路限流。


六、整体流程总结

用一张流程图来总结一下限流检查的完整流程:

请求进入FlowSlot
  │
  ▼
checkFlow()
  │
  ├─→ 获取该资源的所有限流规则
  │
  └─→ 遍历每条规则
        │
        ▼
     canPassCheck()
        │
        ├─→ 集群模式?→ passClusterCheck()
        │
        └─→ 单机模式 → passLocalCheck()
              │
              ├─→ selectNodeByRequesterAndStrategy() 选择统计Node
              │     ├─→ limitApp判断(指定/default/other)
              │     └─→ strategy判断(直接/关联/链路)
              │
              └─→ TrafficShapingController.canPass()
                    ├─→ 直接拒绝模式
                    ├─→ Warm Up冷启动模式
                    └─→ 匀速排队模式

总结与思考

这篇文章我们从整体架构入手,讲了Sentinel限流功能的四个核心角色:FlowSlot、FlowRuleChecker、FlowRule、FlowRuleManager,以及它们之间的协作流程。

重点回顾:

  1. 四个角色分工明确:Slot是入口、Checker做判断、Rule存配置、Manager管规则
  2. 一个资源可以有多条规则:只要有一条触发就限流
  3. limitApp支持三种模式:指定来源、default(全部)、other(其他)
  4. strategy支持三种策略:直接限流、关联限流、链路限流
  5. 3×3的组合:最终通过selectNodeByRequesterAndStrategy选择不同的统计Node
  6. 真正的限流判断在TrafficShapingController里:直接拒绝、冷启动、匀速排队都由它实现

思考一下:

  • 为什么limitApp要有other这个选项?和直接配置default有什么区别?(other是”除了已经单独配置的来源之外的”,可以实现”单独限制A,其他的统一限制B”的需求)
  • 关联限流的实际应用场景多吗?(写优先是一个典型场景,总体来说用得不算多,但关键时刻很有用)
  • 链路限流为什么需要DefaultNode而不是ClusterNode?(因为ClusterNode是全局的,不分链路;DefaultNode是按Context分的,不同链路的DefaultNode不同)

这篇我们只讲了限流检查的”外壳”,真正的核心——TrafficShapingController(流量效果控制器)还没展开。下一篇,我们就来深入分析三种流量控制效果的实现原理。

© 版权声明
THE END
喜欢就支持一下吧
点赞11
相关推荐
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!