一、流量控制( flow control)
简单来讲,就是监控应用服务流量的QPS或者并发线程数一些指标,当达到指定的阈值时对流量进行控制,以免被瞬时的流量高峰冲垮,从而达到应用的高可用。
1.1 概念解释:
1 、快速失败 :当QPS超过了指定的阈值时,请求会立即拒绝,拒绝的方式是抛出FlowException。
2 、Warm Up (预热/冷启动方式):当系统长期的处于低水平的情况下,流量突然增加时,直接吧系统拉升 到高水位可能瞬间把系统压垮。通过“冷启动”,让请求的流量缓慢增加,在一定时间内增加到阈值的上限,给冷系统一个预热的时间。避免令系统被压垮。通常的设置为这个模式的系统请求的QPS曲线是这样的:
3 、匀速排队:请求是以均匀的速度通过,对应的是漏斗算法。类似于火车站检票一样,不管有多少人,都是匀速的通过安检:
1.2 源码解读
在Sentinel中,定义了一个Rule (规则) 的接口,FlowRule (流量规则) 实现了这个接口,这个接口只有一个方法,检查统计当前的指标是否超过设置的阈值,true表示不超过,我们可以看到这些接口当中就定义了以上的概念:
public interface Rule {
boolean passCheck(Context context, DefaultNode node, int count, Object... args);
}
AbstractRule
public abstract class AbstractRule implements Rule {
//资源名称,接口的调用路径
private String resource;
//调用来源,默认是default,不区分
private String limitApp;
}
FlowRule 继承 AbstractRule 抽象类间接的实现了Rule的接口
public class FlowRule extends AbstractRule {
//阈值类型,QPS 线程数
private int grade = 1;
//数量,限流的阈值
private double count;
//流控规则:0:直接, 1:关联 , 2:链路
private int strategy = 0;
private String refResource;
//流量控制效果 ,0:直接拒绝(默认) 1 预热/冷启动方() 2 速率限制 3 预热+速率限制
private int controlBehavior = 0;
private int warmUpPeriodSec = 10;
//最大排队时间
private int maxQueueingTimeMs = 500;
private boolean clusterMode;
//集群模式下的流量控制规则
private ClusterFlowConfig clusterConfig;
private TrafficShapingController controller;
....
}
二、基于QPS的流量控制
经过上面的分析,流量控制主要有两种类型,一种是统计并发线程数,另一种是统计QPS,由FlowRule 中的 grade 字段控制
在上一章 Spring Cloud Alibaba教程:Sentinel 实现限流 当中,我们通过Sentinel dashboard 页面配置了QPS的,在单机模式下,我们设置了流量的控制模式为直接,失败的效果是直接快速的失败。当然我们也可以设置通过页面设置其他的类型。
当然我们也可以用代码实现:
public class FlowRuleForQPS {
private static void init() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
//定义以QPS类型
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//定义资源名
rule.setResource("hello");
//定义每秒的请求QPS的阈值
rule.setCount(5);
//这里我们设置为匀速的模式
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
public static void main(String[] args) {
init();
for (int j = 0; j < 10; j++) {
Entry entry = null;
try {
entry = SphU.entry("hello");
System.out.println("操作成功!");
} catch (BlockException ex) {
System.out.println("当前访问人数过多,请刷新后重试!");
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
基于QPS的限流,我们是可以设置不同的流量控制效果的。
二、基于并发线程数的流量控制
并发线程数限流主要是保护业务线程数不被耗尽。当应用依赖的下游应用由于某种原因不稳定、延迟增加。对于调用者来说,意味着吞吐量下降和更多的线程数占用。为了应对太多线程占用的情况,业内有使用的隔离方案,比如Hystrix通过线程池的方式隔离的。这种方式虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大。特别是对低延时的调用比较大的。Sentinel并发线程数限流不负责创建和管理线程池,而是简单的统计上下文的线程数,如果超出阈值,请求会被立即的拒绝,类似于信号量隔离。
我们通过代码实现以下:
public class FlowRuleForThreadNum {
private static void init() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
//定义以线程数控制
rule.setGrade(RuleConstant.FLOW_GRADE_THREAD);
//定义资源名
rule.setResource("hello");
//定义并发线程数阈值
rule.setCount(5);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
public static void main(String[] args) {
init();
for (int i = 0; i < 10; i++) {
new Thread() {
@Override
public void run() {
for (int j = 0; j < 10; j++) {
Entry entry = null;
try {
entry = SphU.entry("hello");
System.out.println("操作成功!");
} catch (BlockException ex) {
System.out.println("当前访问人数过多,请刷新后重试!");
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
}.start();
}
}
}
这里的init()方法,在Sentinel dashboard页面中的配置如下:
我们运行代码,就可以知道,当请求的线程数达到了指定的阈值,就直接的拒绝了,在并发线程限流模式下,流量控制的效果只有一个,当查过设定的阈值,就会立即拒绝。
四、基于调用关系的流量控制
什么是调用关系?关系是包含了调用方和被调用方,一个方法有可能会调用其他方法,形成一个调用链路的层次关系。Sentinel通过 NodeSelectorSlot 建立不同资源的调用关系, 并且通过 ClusterNodeBuilderSlot 记录每个资源的实时统计信息。
4.1 根据调用方限流
在流控规则中,limitApp 字段用于根据调用方来源进行流量控制,当前的字段有三个选项:
同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default
我们可以通过 ContextUtil.enter(resourceName, origin) 方法中的 origin 参数标明了调用方身份。这些信息会在 ClusterBuilderSlot 中被统计,先看实例代码:
在使用基于调用关系的流量控制时候,我们对资源/hello接口,设置了QPS=2,受限制的应用为caller,设置了流控模式为直接拒绝。当我们调用该资源时,我们通过 ContextUtil.enter(“contextname”, “caller”) 方法声明了当前的调用者就是caller,如下代码:
public class FlowRuleForCaller {
private static void init() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
//定义以QPS类型
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//定义资源名
rule.setResource("hello");
//定义每秒的请求QPS的阈值
rule.setCount(2);
//设置受限制的应用名称
rule.setLimitApp("caller");
//这里我们设置直接拒绝模式
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
rules.add(rule);
//我们可以设置多个规则,针对不同的应用
//针对除了应用名称为caller的其他应用
FlowRule rule1 = new FlowRule();
rule1.setResource("hello");
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("other");
rule1.setCount(3);
rules.add(rule1);
//统一添加到规则管理器中
FlowRuleManager.loadRules(rules);
}
public static void main(String[] args) {
init();
for (int i = 0; i < 5; i++) {
//声明当前的调用方的应用名称, 通过origin参数
ContextUtil.enter("contextname", "caller");
Entry entry = null;
try {
entry = SphU.entry("hello");
System.out.println("访问成功");
} catch (BlockException e) {
System.out.println("网络异常,请刷新!");
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
}
当我们运行代码时,其效果就发挥了作用,如果我们将caller换成其他的调用方,那么规则就不一样了:
当然我们可以通过web页面设置:
4.2 根据调用链路入口限流:链路限流
在Sentinel中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root 的虚拟节点,调用链的入口都是这个虚节点的子节点。
一棵典型的调用树如下图所示:
machine-root
/
/
Entrance1 Entrance2
/
/
DefaultNode(nodeA) DefaultNode(nodeA)
ContextUtil.enter(name,origin) 有2个参数,第一个参数表示上下文名称,绑定了调用链入口,第二个表示调用方,我们可以通过设置上下文名称设置调用链路入口:
public class FlowRuleForEntrance {
private static void init() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
//设置阈值类型 qps
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
//設置流控模式为 调用链路入口
rule.setStrategy(RuleConstant.STRATEGY_CHAIN);
//定义资源名
rule.setResource("hello");
//定义每秒的请求QPS的阈值
rule.setCount(2);
//设置入口
rule.setRefResource("Entrance1");
//这里我们设置直接拒绝模式
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
public static void main(String[] args) {
init();
for (int i = 0; i < 5; i++) {
ContextUtil.enter("Entrance1");
Entry entry = null;
try {
entry = SphU.entry("hello");
System.out.println("访问成功");
} catch (BlockException e) {
System.out.println("网络异常,请刷新!");
} finally {
if (entry != null) {
entry.exit();
}
}
}
}
}
4.3 、具有关系的资源流量控制:关联流量控制
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 FlowRule.strategy 为 RuleConstant.RELATE 同时设置 FlowRule.ref_identity 为 write_db。这样当写库操作过于频繁时,读数据的请求会被限流。
4.4 其他
关于更多的问题我们可以参考: https://github.com/alibaba/Sentinel/wiki/FAQ