Sentinel源码解析四(流控策略和流控效果)
引言
在分析Sentinel的上一篇文章中,我们知道了它是基于滑动窗口做的流量统计,那么在当我们能够根据流量统计算法拿到流量的实时数据后,下一步要做的事情自然就是基于这些数据做流控。在介绍Sentinel
的流控模型之前,我们先来简单看下 Sentinel 后台是如何去定义一个流控规则的
对于上图的配置Sentinel
把它抽象成一个FlowRule
类,与其属性一一对应
- resource 资源名
- limitApp 限流来源,默认为default不区分来源
- grade 限流类型,有QPS和并发线程数两种类型
- count 限流阈值
- strategy 流控策略 1. 直接 2. 关联 3.链路
- controlBehavior 流控效果 1.快速失败 2.预热启动 3.排队等待 4. 预热启动排队等待
- warmUpPeriodSec 流控效果为预热启动时的预热时长(秒)
- maxQueueingTimeMs 流控效果为排队等待时的等待时长 (毫秒)
下面我们来看下选择流控策略和流控效果的核心代码
private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,boolean prioritized) {
// 根据流控策略选择需要流控的Node维度节点
Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
if (selectedNode == null) {
return true;
}
// 获取配置的流控效果 控制器 (1. 直接拒绝 2. 预热启动 3. 排队 4. 预热启动排队等待)
return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
上面的代码比较简单流程也很清晰,首先根据我们配置的流控策略获取到合适维度的 Node 节点(Node节点是Sentinel做流量统计的基本单位),然后再获取到规则中配置的流控效果控制器(1. 直接拒绝 2. 预热启动 3. 排队等待 4.预热启动排队等待)。
流控策略
下面我们来看下选择流控策略的源码分析
static Node selectNodeByRequesterAndStrategy(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node) {
// 获取限流来源 limitApp
String limitApp = rule.getLimitApp();
// 获取限流策略
int strategy = rule.getStrategy();
// 获取当前 上下文的 来源
String origin = context.getOrigin();
// 如果规则配置的限流来源 limitApp 等于 当前上下文来源
if (limitApp.equals(origin) && filterOrigin(origin)) {
// 且配置的流控策略是 直接关联策略
if (strategy == RuleConstant.STRATEGY_DIRECT) {
// 直接返回当前来源 origin 节点
return context.getOriginNode();
}
// 配置的策略为关联或则链路
return selectReferenceNode(rule, context, node);
// 如果规则配置的限流来源 limitApp 等于 default
} else if (RuleConstant.LIMIT_APP_DEFAULT.equals(limitApp)) {
// 且配置的流控策略是 直接关联策略
if (strategy == RuleConstant.STRATEGY_DIRECT) {
// 直接返回当前资源的 clusterNode
return node.getClusterNode();
}
// 配置的策略为关联或则链路
return selectReferenceNode(rule, context, node);
// 如果规则配置的限流来源 limitApp 等于 other,且当前上下文origin不在流控规则策略中
} 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);
}
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;
}
// 策略是关联
if (strategy == RuleConstant.STRATEGY_RELATE) {
// 返回关联的资源ClusterNode
return ClusterBuilderSlot.getClusterNode(refResource);
}
// 策略是链路
if (strategy == RuleConstant.STRATEGY_CHAIN) {
// 当前上下文名称不是规则配置的name 直接返回null
if (!refResource.equals(context.getName())) {
return null;
}
return node;
}
// No node.
return null;
}
这段代码的逻辑判断比较多,我们稍微理一下整个过程
LimitApp
的作用域只在配置的流控策略为RuleConstant.STRATEGY_DIRECT
(直接关联)时起作用。其有三种配置,分别为default
,origin_name
,other
- default 如果配置为default,表示统计不区分来源,当前资源的任何来源流量都会被统计(其实就是选择 Node 为 clusterNode 维度)
- origin_name 如果配置为指定名称的 origin_name,则只会对当前配置的来源流量做统计
- other 如果配置为other 则会对其他全部来源生效但不包括第二条配置的来源
- 当策略配置为 RuleConstant.STRATEGY_RELATE 或 RuleConstant.STRATEGY_CHAIN 时
- STRATEGY_RELATE 关联其他的指定资源,如资源A想以资源B的流量状况来决定是否需要限流,这时资源A规则配置可以使用 STRATEGY_RELATE 策略
- STRATEGY_CHAIN 对指定入口的流量限流,因为流量可以有多个不同的入口(EntranceNode)
- 对于上面几个节点之间的关系不清楚的可以去看我这篇文章开头的总览图 https://www.cnblogs.com/taromilk/p/11750962.html
流控效果
关于流控效果的配置有四种,我们来看下它们的初始化代码
/**
* class com.alibaba.csp.sentinel.slots.block.flow.FlowRuleUtil
*/
private static TrafficShapingController generateRater(/*@Valid*/ FlowRule rule) {
// 只有Grade为统计 QPS时 才可以选择除默认流控效果外的 其他流控效果控制器
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 RateLimiterController(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());
}
可以比较清晰的看到总共对应有四种流控器的初始化
直接拒绝
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// 获取当前qps
int curCount = avgUsedTokens(node);
// 判断是否已经大于阈值
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;
}
此种策略比较简单粗暴,超过流量阈值的会直接拒绝。不过这里有一个小细节,如果入口流量prioritized为true,也就是优先级比较高,则会通过占用未来时间窗口的名额来实现。这个在上一篇文章有介绍到
预热启动
WarmUpController
主要是用来防止流量的突然上升,使系统本在稳定状态下能处理的,但是由于许多资源没有预热,导致处理不了了。注意这里的预热并不是指系统启动之后的一次性预热,而是指系统在运行的任何时候流量从低峰到突增的预热阶段。
下面我们来看下WarmUpController
的具体实现类
/**
* WarmUpController 构造方法
* @param count 当前qps阈值
* @param warmUpPeriodInSec 预热时长 秒
* @param coldFactor 冷启动系数 默认为3
*/
private void construct(double count, int warmUpPeriodInSec, int coldFactor) {
if (coldFactor <= 1) {
throw new IllegalArgumentException("Cold factor should be larger than 1");
}
this.count = count;
this.coldFactor = coldFactor;
// 剩余Token的警戒值,小于警戒值系统就进入正常运行期
warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
// 系统最冷时候的剩余Token数
maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
// 系统预热的速率(斜率)
slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
}
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long passQps = (long) node.passQps();
long previousQps = (long) node.previousPassQps();
// 计算当前的 剩余 token 数
syncToken(previousQps);
// 如果进入了警戒线,开始调整他的qps
long restToken = storedTokens.get();
if (restToken >= warningToken) {
// 计算剩余token超出警戒值的值
long aboveToken = restToken - warningToken;
// 计算当前允许通过的最大 qps
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
if (passQps + acquireCount <= warningQps) {
return true;
}
} else {
// 不在预热阶段,则直接判断当前qps是否大于阈值
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
}
首先是构造方法,主要关注2个重要参数
- warningToken 剩余token的警戒值
- maxToken 剩余的最大token数,如果剩余token数等于maxToken,则说明系统处于最冷阶段
要理解这两个参数的含义,可以参考令牌桶算法,每通过一个请求,就会从令牌桶中取走一个令牌。那么试想一下,当令牌桶中的令牌达到最大值是,是不是意味着系统目前处于最冷阶段,因为桶里的令牌始终处于一个非常饱和的状态。这里的令牌最大值对应的就是maxToken
,而warningToken
,则是对应了一个警戒值,当桶中的令牌数减少到一个指定的值时,说明系统已经度过了预热阶段
当一个请求进来时,首先需要计算当前桶中剩余的token数,具体逻辑在syncToken
方法中
当系统剩余Token大于warningToken时,说明系统仍处于预热阶段,故需要调整当前所能通过的最大qps阈值
protected void syncToken(long passQps) {
long currentTime = TimeUtil.currentTimeMillis();
// 获取秒级别时间(去除毫秒)
currentTime = currentTime - currentTime % 1000;
long oldLastFillTime = lastFilledTime.get();
if (currentTime <= oldLastFillTime) {
return;
}
long oldValue = storedTokens.get();
// 判断是否需要往桶中添加令牌
long newValue = coolDownTokens(currentTime, passQps);
// 设置新的token数
if (storedTokens.compareAndSet(oldValue, newValue)) {
// 如果设置成功的话则减去上次通过的qps数量,就得到当前的实际token数
long currentValue = storedTokens.addAndGet(0 - passQps);
if (currentValue < 0) {
storedTokens.set(0L);
}
lastFilledTime.set(currentTime);
}
}
- 获取当前时间
- coolDownTokens 方法会判断是否需要往桶中放 token,并返回最新的token数
- 如果返回了最新的token数,则将当前剩余的token数减去已经通过的qps,得到最新的剩余token数
private long coolDownTokens(long currentTime, long passQps) {
long oldValue = storedTokens.get();
long newValue = oldValue;
// 添加令牌的几种情况
// 1. 系统初始启动阶段,oldvalue = 0,lastFilledTime也等于0,此时得到一个非常大的newValue,会取maxToken为当前token数量值
// 2. 系统处于预热阶段 且 当前qps小于 count / coldFactor
// 3. 系统处于完成预热阶段
if (oldValue < warningToken) {
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
} else if (oldValue > warningToken) {
if (passQps < (int)count / coldFactor) {
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
}
}
return Math.min(newValue, maxToken);
}
这里看一下会添加令牌的几种情况
- 系统初始启动阶段,oldvalue = 0,lastFilledTime也等于0,此时得到一个非常大的newValue,会取maxToken为当前token数量值
- 系统处于完成预热阶段,需要补充 token 使其稳定在一个范围内
- 系统处于预热阶段 且 当前qps小于 count / coldFactor
前2种情况比较好理解,这里主要解释一下第三种情况,为何 当前qps
小于count / coldFactor
时,需要往桶中添加Token?试想一下如果没有这一步会怎么样,如果没有这一步在比较低的qps情况下补充Token,系统最终也会慢慢度过预热阶段,但实际上这么低的qps(小于 count / coldFactor时
)不应该完成预热。所以这里才会在 qps低于count / coldFactor
时补充剩余token数,来让系统在低qps情况下始终处于预热状态下
排队等待
排队等待的实现相对预热启动实现比较简单
首先会通过我们的配置,计算出相邻两个请求允许通过的最小时间,然后会记录最近一个通过的时间。两者相加即是下一次请求允许通过的最小时间。
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
if (acquireCount <= 0) {
return true;
}
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;
}
// in race condition waitTime may <= 0
// 占用等待时间成功,直接sleep costTime
if (waitTime > 0) {
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}
排队等待控制器的核心策略其实就是围绕了latestPassedTime
进行的,latestPassedTime
指的是上一次请求通过的时间,通过latestPassedTime
+ costTime
来与当前时间做比较,来判断当前请求是否可以通过,无法通过的请求则会优先占用latestPassedTime
时间,直到sleep到可以通过的时间。当然我们也可以配置排队等待的最大时间,来限制目前排队等待通过的请求数量。
预热启动排队等待
预热排队等待,WarmUpRateLimiterController
实现类我们发现其继承了WarmUpController
,这是Sentinel在1.4版本后新加的一种控制器,其实就是预热启动和排队等待的结合体,具体源码我们就不做分析。
尾言
Sentinel
的流控策略和流控效果的相结合使用还是非常巧妙的,当中的一些设计思想还是非常有借鉴意义的
Sentinel源码解析四(流控策略和流控效果)的更多相关文章
- Sentinel源码解析二(Slot总览)
写在前面 本文继续来分析Sentinel的源码,上篇文章对Sentinel的调用过程做了深入分析,主要涉及到了两个概念:插槽链和Node节点.那么接下来我们就根据插槽链的调用关系来依次分析每个插槽(s ...
- Sentinel源码解析一(流程总览)
引言 Sentinel作为ali开源的一款轻量级流控框架,主要以流量为切入点,从流量控制.熔断降级.系统负载保护等多个维度来帮助用户保护服务的稳定性.相比于Hystrix,Sentinel的设计更加简 ...
- Mybatis源码解析(四) —— SqlSession是如何实现数据库操作的?
Mybatis源码解析(四) -- SqlSession是如何实现数据库操作的? 如果拿一次数据库请求操作做比喻,那么前面3篇文章就是在做请求准备,真正执行操作的是本篇文章要讲述的内容.正如标题一 ...
- Dubbo 源码解析四 —— 负载均衡LoadBalance
欢迎来我的 Star Followers 后期后继续更新Dubbo别的文章 Dubbo 源码分析系列之一环境搭建 Dubbo 入门之二 --- 项目结构解析 Dubbo 源码分析系列之三 -- 架构原 ...
- iOS即时通讯之CocoaAsyncSocket源码解析四
原文 前言: 本文为CocoaAsyncSocket源码系列中第二篇:Read篇,将重点涉及该框架是如何利用缓冲区对数据进行读取.以及各种情况下的数据包处理,其中还包括普通的.和基于TLS的不同读取操 ...
- React的React.createContext()源码解析(四)
一.产生context原因 从父组件直接传值到孙子组件,而不必一层一层的通过props进行传值,相比较以前的那种传值更加的方便.简介. 二.context的两种实现方式 1.老版本(React16.x ...
- Sentinel源码解析三(滑动窗口流量统计)
前言 Sentinel的核心功能之一是流量统计,例如我们常用的指标QPS,当前线程数等.上一篇文章中我们已经大致提到了提供数据统计功能的Slot(StatisticSlot),StatisticSlo ...
- AFNetworking2.0源码解析<四>
结构 AFURLResponseSerialization负责解析网络返回数据,检查数据是否合法,把NSData数据转成相应的对象,内置的转换器有json,xml,plist,image,用户可以很方 ...
- Celery 源码解析四: 定时任务的实现
在系列中的第二篇我们已经看过了 Celery 中的执行引擎是如何执行任务的,并且在第三篇中也介绍了任务的对象,但是,目前我们看到的都是被动的任务执行,也就是说目前执行的任务都是第三方调用发送过来的.可 ...
随机推荐
- 全网最全最细的appium自动化测试环境搭建教程以及appium工作原理
一.前言 对于appium自动化测试环境的搭建我相信90%的自学者都是在痛苦中挣扎,在挣扎中放弃,在放弃后又重新开始,只有10%的人,人品比较好,能够很快并顺利的搭建成功.appium 自动化测试 ...
- 五分钟学会Python装饰器,看完面试不再慌
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是Python专题的第12篇文章,我们来看看Python装饰器. 一段囧事 差不多五年前面试的时候,我就领教过它的重要性.那时候我Pyt ...
- nginx 配置大吞吐量
ng做反向代理服务是如果没有这两行配置吞吐量到8000-10000就上不去. proxy_http_version 1.1; # 后端配置支持HTTP1.1,必须配 proxy_set_header ...
- OpenCV学习(1)——初步接触
一.介绍OpenCV OpenCV的全称是Open Source Computer Vision Library,是一个跨平台的计算机视觉库.OpenCV是由英特尔公司发起并参与开 ...
- NumPy学习指南(第2版)
第一章 NumPy快速入门 首先,我们将介绍如何在不同的操作系统中安装NumPy和相关软件,并给出使用NumPy的简单示例代码. 然后,我们将简单介绍IPython(一种交互式shell工具). 如前 ...
- Silverlight Tools Beta2更新了中文语言支持
1,似乎是微软偷偷摸摸更新的......刚才无意间发现,已经下载安装并测试,已在中文版的VS2008安装成功.注意下载页面的安装说明: http://www.microsoft.com/downloa ...
- 在服务器上发布第一个.net项目
作为一名前端开发者,对后端一窍不通可是不行的.公司后端所用的恰好是.net技术,日常开发常见MVC架构,然而还是对MVC不甚了解,前端开发也多有掣肘.本人很想摸索清楚如何构建一个asp.net的项目, ...
- CodeForces - 1058D D. Vasya and Triangle
D. Vasya and Triangle time limit per test1 second memory limit per test256 megabytes inputstandard i ...
- 洛谷 P 4180 次小生成树
题目描述 小C最近学了很多最小生成树的算法,Prim算法.Kurskal算法.消圈算法等等.正当小C洋洋得意之时,小P又来泼小C冷水了.小P说,让小C求出一个无向图的次小生成树,而且这个次小生成树还得 ...
- jmeter的正则表达式编辑器
位置:在后置处理器里面,表示在请求结束或者返回响应结果时发挥作用. 作用:允许用户从服务器的响应中通过使用perl的正则表达式提取值.该元素会作用在指定范围取样器,用正则表达式提取所需值,生成模板字符 ...