前言
Sentinel1.8总共有8个重要的Slot,都是通过SPI机制加载的。
上一章学习了前三个ProcessorSlot:
- NodeSelectorSlot:构建资源(Resource)的路径(DefaultNode),用树的结构存储
- ClusterBuilderSlot:构建ClusterNode,用于记录资源维度的统计信息
- StatisticSlot:使用Node记录指标信息,如RT、Pass/Block Count,为后续规则校验提供数据支撑
本章学习后3个ProcessorSlot,分别对应不同的规则校验:
- AuthoritySlot:授权规则校验
- SystemSlot:系统规则校验
- FlowSlot:流控规则校验
1、AuthoritySlot
AuthoritySlot是第一个规则校验Slot,校验来源应用是否能够有权限访问这个资源。
AuthorityRule需要配置三个字段:
- 资源名;
- 流控应用:逗号分割;
- 授权类型:白名单/黑名单,默认白名单;
public abstract class AbstractRule implements Rule {
private String resource;
private String limitApp;
}
public class AuthorityRule extends AbstractRule {
// 0-白名单规则 1-黑名单规则
private int strategy = RuleConstant.AUTHORITY_WHITE;
}
复制代码
用户代码使用AuthorityRuleManager保存AuthorityRule。
private static void initWhiteRules() {
AuthorityRule rule = new AuthorityRule();
rule.setResource(RESOURCE_NAME);
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appE");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));
}
private static void initBlackRules() {
AuthorityRule rule = new AuthorityRule();
rule.setResource(RESOURCE_NAME);
rule.setStrategy(RuleConstant.AUTHORITY_BLACK);
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));
}
复制代码
AuthoritySlot的entry方法分为两步:
- 从AuthorityRuleManager获取Resource对应的AuthorityRule集合;
- 循环每个AuthorityRule,执行AuthorityRuleChecker.passCheck校验来源应用是否有权限访问资源;
@Spi(order = Constants.ORDER_AUTHORITY_SLOT)
public class AuthoritySlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
checkBlackWhiteAuthority(resourceWrapper, context);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
Map<String, Set<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();
if (authorityRules == null) {
return;
}
Set<AuthorityRule> rules = authorityRules.get(resource.getName());
if (rules == null) {
return;
}
for (AuthorityRule rule : rules) {
if (!AuthorityRuleChecker.passCheck(rule, context)) {
throw new AuthorityException(context.getOrigin(), rule);
}
}
}
}
复制代码
AuthorityRuleChecker的passCheck方法判断Context中的origin是否需要被拒绝:
- 如果授权配置是黑名单,且origin在黑名单内,则拒绝;
- 如果授权配置是白名单,且origin不在白名单内,则拒绝;
final class AuthorityRuleChecker {
static boolean passCheck(AuthorityRule rule, Context context) {
String requester = context.getOrigin();
if (StringUtil.isEmpty(requester) || StringUtil.isEmpty(rule.getLimitApp())) {
return true;
}
// 1. 判断规则是否适用于当前上下文中的app
int pos = rule.getLimitApp().indexOf(requester);
boolean contain = pos > -1;
if (contain) {
boolean exactlyMatch = false;
String[] appArray = rule.getLimitApp().split(",");
for (String app : appArray) {
if (requester.equals(app)) {
exactlyMatch = true;
break;
}
}
contain = exactlyMatch;
}
// 2. 如果规则配置是黑名单,来源应用在黑名单内,则拒绝
int strategy = rule.getStrategy();
if (strategy == RuleConstant.AUTHORITY_BLACK && contain) {
return false;
}
// 3. 如果规则配置是白名单,来源应用在白名单外,则拒绝
if (strategy == RuleConstant.AUTHORITY_WHITE && !contain) {
return false;
}
return true;
}
}
复制代码
2、SystemSlot
SystemSlot系统规则校验。
控制台中,SystemRule如果设置load,则highestSystemLoad!=-1,表示这个系统规则只针对系统负载。控制台只支持SystemRule中的单个属性设置,如果要配置多个属性,需要在控制台创建多个SystemRule。
public class SystemRule extends AbstractRule {
// 最大系统负载(不等同于Linux load)
private double highestSystemLoad = -1;
// 最大cpu使用率
private double highestCpuUsage = -1;
// 最大qps
private double qps = -1;
// 最大平均响应时间 单位ms
private long avgRt = -1;
// 最大并发线程数
private long maxThread = -1;
}
复制代码
使用编码方式,通过SystemRuleManager加载SystemRule,可以在一个规则上设置多个属性。
private static void initSystemRule() {
List<SystemRule> rules = new ArrayList<SystemRule>();
SystemRule rule = new SystemRule();
// max load is 3
rule.setHighestSystemLoad(3.0);
// max cpu usage is 60%
rule.setHighestCpuUsage(0.6);
// max avg rt of all request is 10 ms
rule.setAvgRt(10);
// max total qps is 20
rule.setQps(20);
// max parallel working thread is 10
rule.setMaxThread(10);
rules.add(rule);
SystemRuleManager.loadRules(Collections.singletonList(rule));
}
复制代码
SystemSlot调用SystemRuleManager,校验系统规则。
@Spi(order = Constants.ORDER_SYSTEM_SLOT)
public class SystemSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
SystemRuleManager.checkSystem(resourceWrapper);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
}
复制代码
SystemRuleManager的checkSystem方法分为三步:
- 放行所有EntryType.OUT的出口资源流量;
- 根据全局ClusterNode统计的入口流量,校验QPS、ThreadNum、RT,这些数据是在StatisticSlot中统计的;
- 系统负载和CPU使用率校验;
// SystemRuleManager.java
public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
if (resourceWrapper == null) {
return;
}
// Ensure the checking switch is on.
if (!checkSystemStatus.get()) {
return;
}
// 1. 入口流量才执行系统规则校验
// for inbound traffic only
if (resourceWrapper.getEntryType() != EntryType.IN) {
return;
}
// 2. 通过全局ClusterNode(__total_inbound_traffic__)【StatisticSlot】统计的入口流量,校验QPS、ThreadNum、RT
// total qps
double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps();
if (currentQps > qps) {
throw new SystemBlockException(resourceWrapper.getName(), "qps");
}
// total thread
int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum();
if (currentThread > maxThread) {
throw new SystemBlockException(resourceWrapper.getName(), "thread");
}
double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt();
if (rt > maxRt) {
throw new SystemBlockException(resourceWrapper.getName(), "rt");
}
// 3. 系统负载和cpu使用率校验
// load. BBR algorithm.
if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
if (!checkBbr(currentThread)) {
throw new SystemBlockException(resourceWrapper.getName(), "load");
}
}
// cpu usage
if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
throw new SystemBlockException(resourceWrapper.getName(), "cpu");
}
}
public static double getCurrentSystemAvgLoad() {
return statusListener.getSystemAverageLoad();
}
public static double getCurrentCpuUsage() {
return statusListener.getCpuUsage();
}
复制代码
系统负载和CPU使用率的数据,并非来源于Node,而是SystemRuleManager的一个定时任务。SystemRuleManager在初始化时,启动一个定时任务SystemStatusListener,每秒统计系统负载和CPU使用率。
public final class SystemRuleManager {
private static AtomicBoolean checkSystemStatus = new AtomicBoolean(false);
private static SystemStatusListener statusListener = null;
private final static ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
new NamedThreadFactory("sentinel-system-status-record-task", true));
static {
checkSystemStatus.set(false);
statusListener = new SystemStatusListener();
scheduler.scheduleAtFixedRate(statusListener, 0, 1, TimeUnit.SECONDS);
}
}
复制代码
SystemStatusListener使用JDK的MXBean获取系统负载和CPU使用率。
public class SystemStatusListener implements Runnable {
volatile double currentLoad = -1;
volatile double currentCpuUsage = -1;
volatile long processCpuTime = 0;
volatile long processUpTime = 0;
@Override
public void run() {
try {
OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
// 1. 系统负载
currentLoad = osBean.getSystemLoadAverage();
// 2. cpu使用率
// 2-1. 普通linux环境下cpu使用率
double systemCpuUsage = osBean.getSystemCpuLoad();
// 2-2. 运行于容器内的应用cpu使用率
RuntimeMXBean runtimeBean = ManagementFactory.getPlatformMXBean(RuntimeMXBean.class);
long newProcessCpuTime = osBean.getProcessCpuTime();
long newProcessUpTime = runtimeBean.getUptime();
int cpuCores = osBean.getAvailableProcessors();
long processCpuTimeDiffInMs = TimeUnit.NANOSECONDS
.toMillis(newProcessCpuTime - processCpuTime);
long processUpTimeDiffInMs = newProcessUpTime - processUpTime;
double processCpuUsage = (double) processCpuTimeDiffInMs / processUpTimeDiffInMs / cpuCores;
processCpuTime = newProcessCpuTime;
processUpTime = newProcessUpTime;
// 2-3. max(2-1,2-2)
currentCpuUsage = Math.max(processCpuUsage, systemCpuUsage);
// 3. 如果系统负载大于规则中的阈值,打印日志
if (currentLoad > SystemRuleManager.getSystemLoadThreshold()) {
writeSystemStatusLog();
}
} catch (Throwable e) {
RecordLog.warn("[SystemStatusListener] Failed to get system metrics from JMX", e);
}
}
}
复制代码
此外,SystemManager校验系统负载时,没有直接比较真实的系统负载,而是额外加了一层判断。
只有当系统负载大于规则阈值,且每秒并发线程数 > 总qps * 最小响应时间(秒)时(参考BBR算法),才会抛出SystemBlockException。也就是说针对于load的配置,系统负载高不能直接决定是否拒绝请求,还取决于qps * 最小rt 和 每秒并发线程数的关系。
// SystemRuleManager.java#systemCheck
// load. BBR algorithm.
if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
if (!checkBbr(currentThread)) {
throw new SystemBlockException(resourceWrapper.getName(), "load");
}
}
private static boolean checkBbr(int currentThread) {
if (currentThread > 1 &&
// 每秒并发线程数 > 总qps * 最小响应时间(秒)
currentThread > Constants.ENTRY_NODE.maxSuccessQps() * Constants.ENTRY_NODE.minRt() / 1000) {
return false;
}
return true;
}
复制代码
3、FlowSlot
FlowSlot流控规则,是使用最多的规则之一。
FlowRule的配置项比较多,如下:
public class FlowRule extends AbstractRule {
// 阈值类型 0-线程数 1-QPS(默认)
private int grade = RuleConstant.FLOW_GRADE_QPS;
// 阈值
private double count;
// 流控模式 0-直接 1-关联 2-链路
private int strategy = RuleConstant.STRATEGY_DIRECT;
// 引用资源
private String refResource;
// 流控效果 0-快速失败 1-Warm up 2-排队等待
private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;
// 预热时长(s)
private int warmUpPeriodSec = 10;
// 排队等待时长(ms)
private int maxQueueingTimeMs = 500;
// 是否集群流控
private boolean clusterMode;
// 集群流控配置
private ClusterFlowConfig clusterConfig;
// 流量整形控制器 与controlBehavior相关
private TrafficShapingController controller;
}
复制代码
这里暂时只看单机流控,成员变量之间有如下关系:
接下来通过FlowSlot这个入口,看看上述配置的作用。
FlowSlot的entry方法,调用FlowRuleChecker的checkFlow方法,传入了Resource对应的FlowRule集合。
特别注意这里传入的DefaultNode,是当前Context中的curNode。
@Spi(order = Constants.ORDER_FLOW_SLOT)
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);
}
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);
}
};
}
复制代码
FlowRuleChecker循环FlowRule执行canPassCheck方法,针对集群流控和单机流控有不同的逻辑,这里重点看单机流控。
public class 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;
}
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(FlowRule rule, Context context, DefaultNode node,
int acquireCount) {
return canPassCheck(rule, context, node, acquireCount, false);
}
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);
}
}
复制代码
单机流控就分为两步:选择节点、执行校验。
// FlowRuleChecker.java
private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
boolean prioritized) {
// 1. 选择节点
Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
if (selectedNode == null) {
return true;
}
// 2. 执行校验
return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
复制代码
选择节点
首先是选择节点,选择不同的节点,会导致后续执行规则校验的目标不同。这里选择Node的逻辑层层嵌套,非常复杂,其维度分为两层:
- origin/limitApp:根据来源app不同,选择不同
- strategy:根据流控模式不同,选择不同
// FlowRuleChecker.java
static Node selectNodeByRequesterAndStrategy(FlowRule rule, Context context, DefaultNode node) {
String limitApp = rule.getLimitApp();
int strategy = rule.getStrategy();
String origin = context.getOrigin();
if (limitApp.equals(origin) && filterOrigin(origin)) {
// 1. context.origin = rule.limitApp = xxx(非default和other)
if (strategy == RuleConstant.STRATEGY_DIRECT) {
// STRATEGY_DIRECT --- 取context.curEntry.originNode
return context.getOriginNode();
}
// 非STRATEGY_DIRECT,获取引用的Node返回
return selectReferenceNode(rule, context, node);
} else if (RuleConstant.LIMIT_APP_DEFAULT.equals(limitApp)) {
// 2. context.origin = rule.limitApp = default(默认)
if (strategy == RuleConstant.STRATEGY_DIRECT) {
// STRATEGY_DIRECT --- 取Node对应ClusterNode(Resource)维度指标返回
return node.getClusterNode();
}
// 非STRATEGY_DIRECT,获取引用的Node返回
return selectReferenceNode(rule, context, node);
} else if (RuleConstant.LIMIT_APP_OTHER.equals(limitApp)
&& FlowRuleManager.isOtherOrigin(origin, rule.getResource())) {
// 3. rule.limitApp = other && RuleManager中找不到origin+resource维度的Rule
if (strategy == RuleConstant.STRATEGY_DIRECT) {
// STRATEGY_DIRECT --- 取context.curEntry.originNode
return context.getOriginNode();
}
// 非STRATEGY_DIRECT,获取引用的Node返回
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;
}
// 流控模式 = 关联,返回引用资源的ClusterNode(Context维度)
if (strategy == RuleConstant.STRATEGY_RELATE) {
return ClusterBuilderSlot.getClusterNode(refResource);
}
// 流控模式 = 链路,如果引用资源与当前上下文(EntranceNode对应资源名称)一致,返回context.curEntry.curNode,否则返回空
// 意思是,当前Rule针对某个上下文链路(EntranceNode对应链路)才生效,返回当前Node节点(Context+Resource维度)
if (strategy == RuleConstant.STRATEGY_CHAIN) {
if (!refResource.equals(context.getName())) {
return null;
}
return node;
}
return null;
}
复制代码
分析一下,这段代码到底说的是什么。
情况一:如果FlowRule定义的来源与当前上下文的来源一致,且非default也非other:
- 当流控模式为直接时,返回Context中的originNode,这是origin+Resource维度的StatisticNode。这个Node由Resource对应ClusterNode维护,由ClusterBuilderSlot构建;
- 当流控模式为关联时,返回关联资源refResource对应ClusterNode(Resource维度);
- 当流控模式为链路时,判断关联资源refResource是否与当前上下文入口资源一致(context.name),如果一致代表当前链路匹配当前FlowRule,是关心的链路入口(ContextUtil#enter)带来的流量。返回Context中的当前DefaultNode;
情况二:如果FlowRule定义的来源是default(默认情况) :
- 当流控模式为直接时,返回Context中的ClusterNode,统计的是当前Resource对应的流量(默认情况) ;
- 当流控模式为关联和链路时,逻辑与上面情况一一致;
情况三:如果FlowRule定义的来源是other,且当前上下文的来源 + Resource不存在对应流控规则:
- 当流控模式为直接时,逻辑与上面情况一一致;
- 当流控模式为关联和链路时,逻辑与上面情况一一致;
整理成表格如下,默认情况下配置一个FlowRule,选择的都是ClusterNode,即Resource维度的流量。
流控模式 | 规则中系统来源 | 上下文中系统来源 | 选择节点 | 维度 |
---|---|---|---|---|
直接 | serviceA | serviceA | Context.originNode(StatisticNode) | Origin + Resource |
直接 | default | 任意 | Context.clusterNode(ClusterNode) | Resource(默认情况) |
直接 | other | 上下文的来源 + Resource不存在对应流控规则 | Context.originNode(StatisticNode) | Origin + Resource |
关联 | serviceA | serviceA | rule.refResource对应ClusterNode | Resource |
关联 | default | 任意 | rule.refResource对应ClusterNode | Resource |
关联 | other | 上下文的来源 + Resource不存在对应流控规则 | rule.refResource对应ClusterNode | Resource |
链路 | serviceA | serviceA | rule.refResource == context.name则返回Context.curNode(DefaultNode) | Context+Resource |
链路 | default | 任意 | rule.refResource == context.name则返回Context.curNode(DefaultNode) | Context+Resource |
链路 | other | 上下文的来源 + Resource不存在对应流控规则 | rule.refResource == context.name则返回Context.curNode(DefaultNode) | Context+Resource |
执行校验
执行校验的第一步,是要获取一个TrafficShapingController流量整形控制器。
// FlowRule.java
// 流量整形控制器
private TrafficShapingController controller;
TrafficShapingController getRater() {
return controller;
}
复制代码
TrafficShapingController接口有两个方法,都是为了根据某个Node里记录的流量,是否允许通过。
public interface TrafficShapingController {
boolean canPass(Node node, int acquireCount, boolean prioritized);
boolean canPass(Node node, int acquireCount);
}
复制代码
通过设置FlowRule的controlBehavior属性决定流控效果,从而决定选择流量整形控制器的实现。
但根据FlowRuleUtil.generateRater方法可以看到,grade属性(阈值类型)也与流控效果有关系。
// FlowRuleUtil.java
private static TrafficShapingController generateRater(FlowRule rule) {
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:
}
}
return new DefaultController(rule.getCount(), rule.getGrade());
}
复制代码
虽然控制台能看到很多种配置可能性,但是实际上当阈值类型为线程数(grade=0)时,只能支持快速失败;只有当阈值类型为QPS(grade=1)时,支持三种流控效果。
grade | controlBehavior | TrafficShapingController | 效果 |
---|---|---|---|
FLOW_GRADE_QPS(1) | CONTROL_BEHAVIOR_DEFAULT(0) | DefaultController | 快速失败 |
FLOW_GRADE_QPS(1) | CONTROL_BEHAVIOR_WARM_UP(1) | WarmUpController | warm up |
FLOW_GRADE_QPS(1) | CONTROL_BEHAVIOR_RATE_LIMITER(2) | RateLimiterController | 排队 |
FLOW_GRADE_THREAD(0) | - | DefaultController | 快速失败 |
DefaultController
DefaultController快速失败,一但QPS或并发线程数超过阈值,返回失败。
public class DefaultController implements TrafficShapingController {
// FlowRule.count
private double count;
// FlowRule.grade
private int grade;
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// 1. 根据阈值类型,从node获取不同指标curCount
int curCount = avgUsedTokens(node);
// 2. 如果超过阈值
if (curCount + acquireCount > count) {
// 3. 如果prioritized=true,且是阈值类型为QPS,支持睡眠到下一个时间窗口,让业务代码再执行
// PriorityWaitException会被外层的StatisticSlot吃掉
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);
throw new PriorityWaitException(waitInMs);
}
}
// 4. 不满足上述条件,只要超过阈值则不通过
return false;
}
// 5. 没超过阈值,正常返回
return true;
}
private int avgUsedTokens(Node node) {
// 阈值类型为线程数 返回当前并发线程数
// 阈值类型为QPS 返回当前通过的QPS
return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
}
}
复制代码
首先avgUsedTokens通过FlowRule.grade区分阈值类型,获取Node中不同的属性值,用于判断是否大于FlowRule阈值。
如果小于,直接放行,否则分两种情况:
- 调用Sph.entry方法时,入参prioritized为true(代表业务逻辑很重要),且阈值类型为QPS。支持让当前线程睡眠到下一个时间窗口,让业务代码再执行,起到流量整形的作用。这里PriorityWaitException会阻断后续规则校验,被外层的StatisticSlot吃掉(见上一章),进而可以正常执行业务代码;
- 不满足上述1中条件的,拒绝通过;
WarmUpController
WarmUpController在业务低峰期时(特例:应用刚启动),控制QPS上升速率。
WarmUpController用令牌桶算法实现,其成员变量如下:
public class WarmUpController implements TrafficShapingController {
// FlowRule.count QPS阈值
protected double count;
// 默认3,冷却因子
private int coldFactor;
// 警戒令牌数量,区分系统冷热状态
// 小于warningToken,热状态,走正常逻辑,允许QPS最大为阈值count;大于warningToken,冷状态,允许QPS不超过阈值count
protected int warningToken = 0;
// 令牌最大数量
private int maxToken;
// 斜率 固定等于 (coldFactor - 1.0) / count / (maxToken - warningToken),冷状态时的爬升QPS速度
protected double slope;
// 令牌桶
protected AtomicLong storedTokens = new AtomicLong(0);
// 上次投放令牌的时间,用于计算本次需要新增多少令牌
protected AtomicLong lastFilledTime = new AtomicLong(0);
}
复制代码
- count:FlowRule配置的QPS阈值;
- coldFactor:冷却因子,默认为3,用于后续计算使用,通过csp.sentinel.flow.cold.factor设置;
- warningToken:警戒令牌数量。当剩余令牌数量小于该值,表示系统处于热状态;当剩余令牌数量大于该值,代表系统处于冷状态,业务低峰期;
- maxToken:令牌桶最大容量;
- slope:斜率。用于控制冷状态时爬升QPS的速率;
- storedTokens:AtomicLong类型,表示当前令牌数量;
- lastFilledTime:上次投放令牌时间;
WarmUpController在构造时,会执行construct方法,使用QPS和warm up时长转换计算得到FlowRule对应的warningToken、maxToken、slope。这三个参数在FlowRule生效期间,不会改变。
// WarmUpController.java
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;
warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
}
复制代码
WarmUpController的canPass方法逻辑如下:
-
根据上一个时间窗口的QPS,调整令牌数量;
-
判断当前剩余令牌数量与warningToken的大小关系;
- 如果令牌充足,即storedTokens剩余令牌 >= warningToken,代表系统处于业务低峰期,需要执行warm up,控制QPS缓慢上升。warm up时,通过token数量+QPS阈值+slope斜率,计算得到QPS警戒阈值warningQps,要求获取令牌数量 + 当前时间窗口qps 不能超过QPS警戒阈值warningQps。当aboveToken越来越小,会导致warningQps慢慢变大,表示对QPS的限制越来越松,即warm up,让QPS限制缓慢放开;
- 如果令牌不充足,即storedTokens剩余令牌 < warningToken,代表系统处于非业务低峰期,需要执行严格的QPS控制。比较逻辑等同于DefaultController(prioritized=false);
// WarmUpController.java
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long passQps = (long) node.passQps();
long previousQps = (long) node.previousPassQps();
// 1. 根据上一个时间窗口的QPS,调整令牌数量
syncToken(previousQps);
long restToken = storedTokens.get();
if (restToken >= warningToken) {
// 2. 如果剩余token相对比较充足,大于警戒线,代表系统处于业务低峰期,需要warm up,动态计算QPS阈值
long aboveToken = restToken - warningToken;
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
if (passQps + acquireCount <= warningQps) {
return true;
}
} else {
// 3. 如果剩余token不是很充足,小于警戒线,代表系统处于非业务低峰期,要严格控制QPS
// 逻辑同DefaultController
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
}
复制代码
令牌数量调整逻辑如下,当剩余令牌数量小于warningToken时 或 QPS < FlowRule.countQPS阈值 / coldFactor(3)时,加入新的令牌,否则不改变令牌数量。
// WarmUpController.java
protected void syncToken(long passQps) {
// 1. 判断当前时间是否小于上次发放令牌时间
long currentTime = TimeUtil.currentTimeMillis();
currentTime = currentTime - currentTime % 1000;
long oldLastFillTime = lastFilledTime.get();
if (currentTime <= oldLastFillTime) {
return;
}
// 2. 计算令牌数量
long oldValue = storedTokens.get();
long newValue = coolDownTokens(currentTime, passQps);
// 3. 设置令牌数量
if (storedTokens.compareAndSet(oldValue, newValue)) {
long currentValue = storedTokens.addAndGet(0 - passQps);
if (currentValue < 0) {
storedTokens.set(0L);
}
lastFilledTime.set(currentTime);
}
}
private long coolDownTokens(long currentTime, long passQps) {
long oldValue = storedTokens.get();
long newValue = oldValue;
// 添加令牌的判断前提条件:
// 当令牌的消耗程度远远低于警戒线的时候
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);
}
复制代码
那么预热时长warmUpPeriodInSec配置的长短,到底实际效果是什么呢?
假设阈值QPS(count)不变,warmUpPeriodInSec越大,warningToken越大,maxToken越大(令牌桶越大),slope越小(maxToken = warningToken + k * warmUpPeriodInSec)。
warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
复制代码
又maxToken随warmUpPeriodInSec增长快过warningToken,所以系统从冷状态到热状态,需要经过的时间就越长(maxToken-warningToken需要获取的token数量越多)。
当剩余token大于warningToken,通过slope控制qps的增长速度,让剩余token缓慢低于warningToken,让系统进入热状态。
long restToken = storedTokens.get();
if (restToken >= warningToken) { // 冷状态
// 2. 如果剩余token相对比较充足,大于警戒线,代表系统处于业务低峰期,需要warm up,动态计算QPS阈值
long aboveToken = restToken - warningToken;
// 计算warningQps,要求qps不能超过这个阈值
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
if (passQps + acquireCount <= warningQps) {
return true;
}
// ...
}
return false;
复制代码
RateLimiterController
RateLimiterController使用漏桶算法,通过配置QPS阈值和超时时间,控制访问资源的速率。
public class RateLimiterController implements TrafficShapingController {
// 超时时间
private final int maxQueueingTimeMs;
// qps阈值
private final double count;
// 上次通过该资源的时间戳
private final AtomicLong latestPassedTime = new AtomicLong(-1);
}
复制代码
RateLimiterController的checkPass方法逻辑如下。
// RateLimiterController.java
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
if (acquireCount <= 0) {
return true;
}
if (count <= 0) {
return false;
}
long currentTime = TimeUtil.currentTimeMillis();
// RT = 1 / qps
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
// 期望通过时间 = RT + 上次通过时间
long expectedTime = costTime + latestPassedTime.get();
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;
}
复制代码
RateLimiterController将配置的QPS阈值,转换为请求耗时(1/qps),根据上次通过时间和预计耗时,计算期望通过时间。如果这个时间大于当前时间,表示请求速率过快,可能需要等待;否则直接放行。
总结如下:
- 如果资源长时间没被访问(1/QPS+上次访问时间 < 当前时间),通过;
- 如果等待时间 = 1/QPS+上次访问时间 - 当前时间 大于 配置排队时间,拒绝;
- 尝试累加上次访问时间 = 上次访问时间 + 1/QPS,二次确认这个新的上次访问时间 - 当前时间 大于 配置排队时间,如果是,拒绝并回滚 上次访问时间 = 上次访问时间 - 1/QPS;
- 经过上面三步之后,确认当前请求的响应时间在可控范围内(上次访问时间 + 1/QPS < 当前时间 且 差值不超过配置排队时间),睡眠一段时间(当前时间 - 上次访问时间 + 1/QPS),通过;
总结
本章学习了3个ProcessorSlot:
-
AuthoritySlot:授权规则校验。规则可配置为黑名单或白名单,对名单中的来源系统放行或拒绝。如果授权配置是黑名单,且上下文的origin在黑名单内,则拒绝;如果授权配置是白名单,且上下文的origin不在白名单内,则拒绝;
-
SystemSlot:系统规则校验。根据
系统负载
、cpu使用率
、平均响应时间
、qps
、并发线程数
五个指标,校验系统所有EntryType.IN的入口资源是否超过配置阈值。其中系统负载
和cpu使用率
是定时通过JDK的MXBean获取的。平均响应时间
、qps
、并发线程数
三个指标,是通过全局入口流量Constants.ENTRY_NODE节点获取的,Constants.ENTRY_NODE在StatisticSlot中记录了每个请求的RT、Pass/Block Count; -
FlowSlot:流控规则校验。流控规则可以根据两种阈值类型qps或线程数进行流量控制。
根据流控模式strategy的不同和来源系统的不同,流控规则校验的维度也不同,如下表。
默认情况下配置一个FlowRule,选择的都是ClusterNode,即Resource维度的流量。
流控模式 规则中系统来源 上下文中系统来源 选择节点 维度 直接 serviceA serviceA Context.originNode(StatisticNode) Origin + Resource 直接 default 任意 Context.clusterNode(ClusterNode) Resource(默认情况) 直接 other 上下文的来源 + Resource不存在对应流控规则 Context.originNode(StatisticNode) Origin + Resource 关联 serviceA serviceA rule.refResource对应ClusterNode Resource 关联 default 任意 rule.refResource对应ClusterNode Resource 关联 other 上下文的来源 + Resource不存在对应流控规则 rule.refResource对应ClusterNode Resource 链路 serviceA serviceA rule.refResource == context.name则返回Context.curNode(DefaultNode) Context+Resource 链路 default 任意 rule.refResource == context.name则返回Context.curNode(DefaultNode) Context+Resource 链路 other 上下文的来源 + Resource不存在对应流控规则 rule.refResource == context.name则返回Context.curNode(DefaultNode) Context+Resource 总结来说:
- 直接:代表根据配置Resource维度数据进行流控,如果有来源系统配置,取来源系统的Resource维度数据;
- 关联:引用其他Resource对应流量数据,做流控规则校验;
- 链路:在前面两者的基础上,还关心链路上下文,取Context+Resource维度数据做流控规则校验;
- 来源:default代表匹配任意来源;other的含义是针对某个资源,如果没有配置来源系统相关规则,兜底的流控规则;
另外,可以通过配置controlBehavior参数,设置流控效果。
controlBehavior=0,默认快速失败,支持阈值类型包含qps和线程数。当请求速率超过阈值,直接拒绝。DefaultController实现;
controlBehavior=1,预热,仅支持阈值类型qps。当系统处于冷状态时(qps较低,如系统刚启动),通过配置预热时长,控制qps缓慢爬升。WarmUpController使用令牌桶算法,将QPS和预热时长转换为令牌实现流控;
controlBehavior=2,排队等待,仅支持阈值类型qps。通过配置QPS阈值和排队超时时间,控制访问资源的速率。RateLimiterController使用漏桶算法,将配置的QPS阈值,转换为请求耗时(1/qps),根据上次通过时间和预计耗时,计算期望通过时间。如果这个时间大于当前时间,表示请求速率过快,可能需要等待,如果等待时间超过阈值,则会拒绝;