Sentinel源码(四)ProcessorSlot(中)

前言

Sentinel1.8总共有8个重要的Slot,都是通过SPI机制加载的。

slots.gif

上一章学习了前三个ProcessorSlot:

  1. NodeSelectorSlot:构建资源(Resource)的路径(DefaultNode),用树的结构存储
  2. ClusterBuilderSlot:构建ClusterNode,用于记录资源维度的统计信息
  3. StatisticSlot:使用Node记录指标信息,如RT、Pass/Block Count,为后续规则校验提供数据支撑

本章学习后3个ProcessorSlot,分别对应不同的规则校验:

  1. AuthoritySlot:授权规则校验
  2. SystemSlot:系统规则校验
  3. FlowSlot:流控规则校验

1、AuthoritySlot

AuthoritySlot是第一个规则校验Slot,校验来源应用是否能够有权限访问这个资源。

控制台-授权规则.png

AuthorityRule需要配置三个字段:

  1. 资源名;
  2. 流控应用:逗号分割;
  3. 授权类型:白名单/黑名单,默认白名单;
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方法分为两步:

  1. 从AuthorityRuleManager获取Resource对应的AuthorityRule集合;
  2. 循环每个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是否需要被拒绝:

  1. 如果授权配置是黑名单,且origin在黑名单内,则拒绝;
  2. 如果授权配置是白名单,且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系统规则校验。

控制台-系统规则.png

控制台中,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方法分为三步:

  1. 放行所有EntryType.OUT的出口资源流量;
  2. 根据全局ClusterNode统计的入口流量,校验QPS、ThreadNum、RT,这些数据是在StatisticSlot中统计的;
  3. 系统负载和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流控规则,是使用最多的规则之一。

控制台-流控规则.png

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;
}
复制代码

这里暂时只看单机流控,成员变量之间有如下关系:

单机FlowRule.png

接下来通过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的逻辑层层嵌套,非常复杂,其维度分为两层:

  1. origin/limitApp:根据来源app不同,选择不同
  2. 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选择节点(1).png

情况二:如果FlowRule定义的来源是default(默认情况)

  • 当流控模式为直接时,返回Context中的ClusterNode,统计的是当前Resource对应的流量(默认情况)
  • 当流控模式为关联和链路时,逻辑与上面情况一一致;

FlowRule选择节点(2).png

情况三:如果FlowRule定义的来源是other,且当前上下文的来源 + Resource不存在对应流控规则

  • 当流控模式为直接时,逻辑与上面情况一一致;
  • 当流控模式为关联和链路时,逻辑与上面情况一一致;

FlowRule选择节点(3).png

整理成表格如下,默认情况下配置一个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阈值。

如果小于,直接放行,否则分两种情况:

  1. 调用Sph.entry方法时,入参prioritized为true(代表业务逻辑很重要),且阈值类型为QPS。支持让当前线程睡眠到下一个时间窗口,让业务代码再执行,起到流量整形的作用。这里PriorityWaitException会阻断后续规则校验,被外层的StatisticSlot吃掉(见上一章),进而可以正常执行业务代码;
  2. 不满足上述1中条件的,拒绝通过;

WarmUpController

WarmUpController在业务低峰期时(特例:应用刚启动),控制QPS上升速率

WarmUp.png

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方法逻辑如下:

  1. 根据上一个时间窗口的QPS,调整令牌数量

  2. 判断当前剩余令牌数量与warningToken的大小关系;

    1. 如果令牌充足,即storedTokens剩余令牌 >= warningToken,代表系统处于业务低峰期,需要执行warm up,控制QPS缓慢上升。warm up时,通过token数量+QPS阈值+slope斜率,计算得到QPS警戒阈值warningQps,要求获取令牌数量 + 当前时间窗口qps 不能超过QPS警戒阈值warningQps。当aboveToken越来越小,会导致warningQps慢慢变大,表示对QPS的限制越来越松,即warm up,让QPS限制缓慢放开;
    2. 如果令牌不充足,即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

RateLimiter.png

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. 如果资源长时间没被访问(1/QPS+上次访问时间 < 当前时间),通过
  2. 如果等待时间 = 1/QPS+上次访问时间 - 当前时间 大于 配置排队时间,拒绝
  3. 尝试累加上次访问时间 = 上次访问时间 + 1/QPS,二次确认这个新的上次访问时间 - 当前时间 大于 配置排队时间,如果是,拒绝并回滚 上次访问时间 = 上次访问时间 - 1/QPS;
  4. 经过上面三步之后,确认当前请求的响应时间在可控范围内(上次访问时间 + 1/QPS < 当前时间 且 差值不超过配置排队时间),睡眠一段时间(当前时间 - 上次访问时间 + 1/QPS),通过

RateLimiterController.png

总结

本章学习了3个ProcessorSlot:

  1. AuthoritySlot:授权规则校验。规则可配置为黑名单或白名单,对名单中的来源系统放行或拒绝。如果授权配置是黑名单,且上下文的origin在黑名单内,则拒绝;如果授权配置是白名单,且上下文的origin不在白名单内,则拒绝;

  2. SystemSlot:系统规则校验。根据 系统负载cpu使用率平均响应时间qps并发线程数五个指标,校验系统所有EntryType.IN的入口资源是否超过配置阈值。其中系统负载cpu使用率是定时通过JDK的MXBean获取的。平均响应时间qps并发线程数三个指标,是通过全局入口流量Constants.ENTRY_NODE节点获取的,Constants.ENTRY_NODE在StatisticSlot中记录了每个请求的RT、Pass/Block Count;

  3. FlowSlot:流控规则校验。流控规则可以根据两种阈值类型qps线程数进行流量控制。

    单机FlowRule.png

    根据流控模式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

    总结来说:

    1. 直接:代表根据配置Resource维度数据进行流控,如果有来源系统配置,取来源系统的Resource维度数据;
    2. 关联:引用其他Resource对应流量数据,做流控规则校验;
    3. 链路:在前面两者的基础上,还关心链路上下文,取Context+Resource维度数据做流控规则校验;
    4. 来源:default代表匹配任意来源;other的含义是针对某个资源,如果没有配置来源系统相关规则,兜底的流控规则;

    另外,可以通过配置controlBehavior参数,设置流控效果

    controlBehavior=0,默认快速失败,支持阈值类型包含qps和线程数。当请求速率超过阈值,直接拒绝。DefaultController实现;

    controlBehavior=1,预热,仅支持阈值类型qps。当系统处于冷状态时(qps较低,如系统刚启动),通过配置预热时长,控制qps缓慢爬升。WarmUpController使用令牌桶算法,将QPS和预热时长转换为令牌实现流控;

    controlBehavior=2,排队等待,仅支持阈值类型qps。通过配置QPS阈值和排队超时时间,控制访问资源的速率。RateLimiterController使用漏桶算法,将配置的QPS阈值,转换为请求耗时(1/qps),根据上次通过时间和预计耗时,计算期望通过时间。如果这个时间大于当前时间,表示请求速率过快,可能需要等待,如果等待时间超过阈值,则会拒绝;

猜你喜欢

转载自juejin.im/post/7049210511065350174