Sentinel 之所以针对每个资源统计访问来源的指标数据,也是为了实现对丰富的限流策略的支持。
因为每个调用来源服务对同一个资源的访问频率都是不同的,针对调用来源限流可限制并发量较高的来源服务的请求,而对并发量低的来源服务的请求可不限流,或者是对一些并没有那么重要的来源服务限流。
当两个资源之间具有资源争抢关系的时候,使用 STRATEGY_RELATE 调用关系限流策略可避免多个资源之间过度的对同一资源争抢。例如查询订单信息和用户下单两个分别读和写数据库订单表的资源。
限流处理器插槽:FlowSlot
FlowSlot 是实现限流功能的切入点,它作为 ProcessorSlot 插入到 ProcessorSlotChain 链表中,在 entry 方法中调用 Checker 去判断是否需要拒绝当前请求,如果需要拒绝请求则抛出 Block 异常。FlowSlot 的源码如下:
public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> { private final FlowRuleChecker checker; public FlowSlot() { this(new FlowRuleChecker()); } // 规则生产者,一个 Function 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); fireEntry(context, resourceWrapper, node, count, prioritized, args); } // check 是否限流 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); } }
限流规则检查器: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; } // (1) Collection<FlowRule> rules = ruleProvider.apply(resource.getName()); if (rules != null) { // (2) for (FlowRule rule : rules) { // (3) if (!canPassCheck(rule, context, node, count, prioritized)) { throw new FlowException(rule.getLimitApp(), rule); } } } }
//canPassCheck 方法返回 true 说明允许请求通过,反之则不允许通过,
//其中check分为local check和 clusterCheck.
public boolean canPassCheck(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); }
其中 passLocalCheck包含三个关键步骤
-
根据调用来源和“调用关系限流策略”选择 DefaultNode;
-
获取限流规则配置的流量效果控制器(TrafficShapingController);
-
调用 TrafficShapingController#canPass 方法完成 canPassCheck。
passLocalCheck 方法源码如下:
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); } //根据流控策略找到对应的实时统计信息(Node) static Node selectNodeByRequesterAndStrategy(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node) { // 限流规则针对的来源 // 如果当前限流规则的 limitApp 为 default,则说明该限流规则对任何调用来源都生效,针对所有调用来源限流,否则只针对指定调用来源限流。 String limitApp = rule.getLimitApp(); //基于调用关系的限流策略 int strategy = rule.getStrategy(); // 调用来源, ContextUtil.enter(res,origin) 时传入 String origin = context.getOrigin(); //如果限流规则配置的针对的调用方与当前请求实际调用来源匹配(并且不是 default、other)时的处理逻辑 if (limitApp.equals(origin) && filterOrigin(origin)) { // (origin==limitApp),直接使用OriginNode, 实现针对该origin来源限流。 if (strategy == RuleConstant.STRATEGY_DIRECT) { // Matches limit origin, return origin statistic node. return context.getOriginNode(); } return selectReferenceNode(rule, context, node); //如果流控规则针对的调用方(limitApp) 配置的为 default,表示对所有的调用源都生效. } else if (RuleConstant.LIMIT_APP_DEFAULT.equals(limitApp)) { if (strategy == RuleConstant.STRATEGY_DIRECT) { // 直接返回 cluster node. return node.getClusterNode(); } return selectReferenceNode(rule, context, node); //如果流控规则针对的调用方为(other),此时需要判断是否有针对当前的流控规则,只要存在,则这条规则对当前资源“失效”, //如果 limitApp 为 other,且该资源的所有限流规则都没有针对当前的Origin限流, 这使用本originNode //如果针对该资源没有配置其他额外的流控规则. } else if (RuleConstant.LIMIT_APP_OTHER.equals(limitApp) && FlowRuleManager.isOtherOrigin(origin, rule.getResource())) { if (strategy == RuleConstant.STRATEGY_DIRECT) { return context.getOriginNode(); } return selectReferenceNode(rule, context, node); } //都未匹配,表示没有找到Statistics Node,则直接通过. return null; } static Node selectReferenceNode(FlowRule rule, Context context, DefaultNode node) { String refResource = rule.getRefResource(); int strategy = rule.getStrategy(); if (StringUtil.isEmpty(refResource)) { return null; } //关联模式, 从集群环境中获取对应关联资源所代表的 Node //通俗点说就是使用其它资源的指标数据(statisticsNode)匹配当前的rule, //如果你的并发量高,到了我的规则值,我就限流,等你并发量降低到我的rule以下,我就不限流了; if (strategy == RuleConstant.STRATEGY_RELATE) { return ClusterBuilderSlot.getClusterNode(refResource); } // 判断当前调用上下文的入口资源与规则配置的是否一样, // 如果是,则返回入口资源对应的Node,即当前DefaultNode // 否则返回 null,表示该条流控规则,不参与流控判断. if (strategy == RuleConstant.STRATEGY_CHAIN) { if (!refResource.equals(context.getName())) { return null; } return node; } // No node. return null; } private static boolean filterOrigin(String origin) { //origin 不能为 default 和 other return !RuleConstant.LIMIT_APP_DEFAULT.equals(origin) && !RuleConstant.LIMIT_APP_OTHER.equals(origin); }
简而言之:
验证流程:就是逐个遍历Resource对应的Rule,拿Rule_limitApp去匹配来源origin(来源不能为default和other)
- 如果匹配上(limit_app==Origin),验证流控模式
- 直接流控:取origin_statistics_node
- 关联流控:通俗点说就是使用其它res的指标数据(statisticsNode)匹配当前的rule, 如果你的并发量高,到了我的规则值,我就限流,等你并发量降低到我的rule以下,我就不限流了;
- 链路模式:判断当前调用上下文的入口资源与规则配置的是否一样,如果是,则返回入口资源对应的Node,即当前DefaultNode,否则直接pass
- 如果匹配不上,验证流控模式
- 如果Rule_limitApp为Deafult,
- 直接流控:取Cluster_statistics_node
- 关联流控:通俗点说就是使用其它res的指标数据(statisticsNode)匹配当前的rule, 如果你的并发量高,到了我的规则值,我就限流,等你并发量降低到我的rule以下,我就不限流了;
- 链路模式:判断当前调用上下文的入口资源与规则配置的是否一样,如果是,则返回入口资源对应的Node,即当前DefaultNode,否则直接pass
- 如果Rule_limitApp为Other,并且该Res下没有对origin做单独规则,
- 直接流控:取origin_statistics_node
- 关联流控:通俗点说就是使用其它res的指标数据(statisticsNode)匹配当前的rule, 如果你的并发量高,到了我的规则值,我就限流,等你并发量降低到我的rule以下,我就不限流了;
- 链路模式:判断当前调用上下文的入口资源与规则配置的是否一样,如果是,则返回入口资源对应的Node,即当前DefaultNode,否则直接pass
- 标识规则没匹配上,直接pass
从 selectNodeByRequesterAndStrategy 方法可以看出,Sentinel 之所以针对每个Resource统计访问来源的指标数据,也是为了实现对丰富的限流策略的支持。
因为每个调用来源服务对同一个资源的访问频率都是不同的,针对调用来源限流可限制并发量较高的来源服务的请求,而对并发量低的来源服务的请求可不限流,或者是对一些并没有那么重要的来源服务限流。
当两个资源之间具有资源争抢关系的时候,使用 STRATEGY_RELATE 调用关系限流策略可避免多个资源之间过度的对同一资源争抢。
流量效果控制器:TrafficShapingController
Sentinel 支持对超出限流阈值的流量采取效果控制器控制这些流量,流量效果控制支持:直接拒绝、Warm Up(冷启动)、匀速排队。
对应 FlowRule 中的 controlBehavior 字段。在调用 FlowRuleManager#loadRules 方法时,FlowRuleManager 会将限流规则配置的 controlBehavior 转为对应的 TrafficShapingController。
controlBehavior 的取值与使用的 TrafficShapingController 对应关系如下表格所示:
DefaultController
DefaultController 是默认使用的流量效果控制器,直接拒绝超出阈值的请求。当 QPS 超过限流规则配置的阈值,新的请求就会被立即拒绝,抛出 FlowException。
@Override public boolean canPass(Node node, int acquireCount, boolean prioritized) { // (1) int curCount = avgUsedTokens(node); // (2) if (curCount + acquireCount > count) { // (3) if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) { long currentTime; long waitInMs; currentTime = TimeUtil.currentTimeMillis(); // (4) waitInMs = node.tryOccupyNext(currentTime, acquireCount, count); // (5) if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) { // 将休眠之后对应的时间窗口的 pass(通过)这项指标数据的值加上 acquireCount node.addWaitingRequest(currentTime + waitInMs, acquireCount); // 添加占用未来的 pass 指标的数量 node.addOccupiedPass(acquireCount); // 休眠等待,当前线程阻塞 sleep(waitInMs); // 抛出 PriorityWait 异常,表示当前请求是等待了 waitInMs 之后通过的 throw new PriorityWaitException(waitInMs); } } return false; } return true; }
- avgUsedTokens 方法:返回 node 当前时间窗口统计的QPS/ThreadCount,注意如果是ClusterNode的话,需要汇总所有Childer的QPS;
- 如果将当前请求放行会超过限流阈值,且不满足(3),则直接拒绝当前请求。
- 如果限流阈值类型为 QPS,表示具有优先级的请求可以占用未来时间窗口的统计指标。
- 如果可以占用未来时间窗口的统计指标,则 tryOccupyNext 返回当前请求需要等待的时间,单位毫秒。
- 如果休眠时间在限制可占用的最大时间范围内,则挂起当前请求,当前线程休眠 waitInMs 毫秒。休眠结束后抛出 PriorityWait 异常,表示当前请求是等待了 waitInMs 之后通过的。
RateLimiterController
Sentinel 匀速流控效果是漏桶算法结合虚拟队列等待机制实现的,可理解为存在一个虚拟的队列,请求在队列中排队通过,每(count/1000)毫秒可通过一个请求,
要配置限流规则使用匀速通过效果控制器 RateLimiterController,则必须配置限流阈值类型为 GRADE_QPS,并且阈值要少于等于 1000
匀速流控适合用于请求突发性增长后剧降的场景。例如用在有定时任务调用的接口,在定时任务执行时请求量一下子飙高,但随后又没有请求的情况,这个时候我们不希望一下子让所有请求都通过,避免把系统压垮,但也不想直接拒绝超出阈值的请求,这种场景下使用匀速流控可以将突增的请求排队到低峰时执行,起到“削峰填谷”的效果。
WarmUpController
Warm Up,冷启动。在应用升级重启时,应用自身需要一个预热的过程,预热之后才能到达一个稳定的性能状态。Sentinel 冷启动限流算法参考了 Guava 的 SmoothRateLimiter 实现的冷启动限流算法。具体实现不做过多分析。