Sentinel源码(五)ProcessorSlot(下)

1,135 阅读19分钟

前言

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

slots.gif

上两章学习了6个ProcessorSlot:

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

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

  1. ParamFlowSlot:热点参数流控规则校验
  2. DegradeSlot:降级规则校验

1、ParamFlowSlot

ParamFlowSlot处理热点参数流控规则校验,在FlowSlot流控规则校验的基础上,增加了参数例外项。资源除了基础的阈值限制以外,可以控制不同入参的阈值限制。如下图所示,对于资源hot2,默认QPS阈值是3,但是对于方法第一个参数为B时,QPS阈值是30。

控制台-热点规则.png

编码方式配置热点规则:

private static void initParamFlowRules() {
    // QPS mode, threshold is 5 for every frequent "hot spot" parameter in index 0 (the first arg).
    ParamFlowRule rule = new ParamFlowRule(RESOURCE_KEY)
        .setParamIdx(0)
        .setGrade(RuleConstant.FLOW_GRADE_QPS)
        .setCount(5);
    // We can set threshold count for specific parameter value individually.
    // Here we add an exception item. That means: QPS threshold of entries with parameter `PARAM_B` (type: int)
    // in index 0 will be 10, rather than the global threshold (5).
    ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B))
        .setClassType(int.class.getName())
        .setCount(10);
    rule.setParamFlowItemList(Collections.singletonList(item));
    ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
}

ParamFlowRule热点规则的成员变量如下:

public class ParamFlowRule extends AbstractRule {
    // 阈值类型
    private int grade = RuleConstant.FLOW_GRADE_QPS;
    // 参数下标
    private Integer paramIdx;
    // 阈值
    private double count;
    // 流控效果 0-快速失败 1-warm up(热点规则不支持,效果同0) 2-排队等待
    private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;
    // 排队等待超时时间
    private int maxQueueingTimeMs = 0;
    // 膨胀数量 阈值类型为QPS时,决定令牌桶的最大数量
    private int burstCount = 0;
    // 时间窗口
    private long durationInSec = 1;
    // 参数例外项
    private List<ParamFlowItem> paramFlowItemList = new ArrayList<ParamFlowItem>();
    // Sentinel内部使用,用于包装例外项
    private Map<Object, Integer> hotItems = new HashMap<Object, Integer>();
}
// 例外项
public class ParamFlowItem {
    // 例外项入参值,String类型
    private String object;
    // 阈值
    private Integer count;
    // Class
    private String classType;
}

ParamFlowSlot的entry方法逻辑如下:

  1. 校验热点规则索引下标是否小于0,如果小于0做相应调整,忽略这段逻辑,因为正常配置,不会配置小于0的参数下标;
  2. 初始化资源对应ParameterMetric,用于统计热点参数频率;
  3. ParamFlowChecker.passCheck执行热点规则校验;
@Spi(order = -3000)
public class ParamFlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
            fireEntry(context, resourceWrapper, node, count, prioritized, args);
            return;
        }
        checkFlow(resourceWrapper, count, args);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

     void checkFlow(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
        if (args == null) {
            return;
        }
        if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
            return;
        }
        List<ParamFlowRule> rules = ParamFlowRuleManager.getRulesOfResource(resourceWrapper.getName());
        for (ParamFlowRule rule : rules) {
            // 1 如果规则索引下标小于0,调整为正数 忽略
            applyRealParamIdx(rule, args.length);
            // 2 初始化ParameterMetric,用于统计热点参数频率
            ParameterMetricStorage.initParamMetricsFor(resourceWrapper, rule);
            // 3 校验热点规则
            if (!ParamFlowChecker.passCheck(resourceWrapper, rule, count, args)) {
                String triggeredParam = "";
                if (args.length > rule.getParamIdx()) {
                    Object value = args[rule.getParamIdx()];
                    triggeredParam = String.valueOf(value);
                }
                throw new ParamFlowException(resourceWrapper.getName(), triggeredParam, rule);
            }
        }
    }
}

初始化ParameterMetric

ParameterMetricStorage.initParamMetricsFor方法,初始化ParameterMetric,从全局metricsMap中获取资源对应ParameterMetric。

public final class ParameterMetricStorage {
    // k=资源名称 v=ParameterMetric
    private static final Map<String, ParameterMetric> metricsMap = new ConcurrentHashMap<>();
    private static final Object LOCK = new Object();
    public static void initParamMetricsFor(ResourceWrapper resourceWrapper, ParamFlowRule rule) {
        if (resourceWrapper == null || resourceWrapper.getName() == null) {
            return;
        }
        String resourceName = resourceWrapper.getName();
        ParameterMetric metric;
        if ((metric = metricsMap.get(resourceName)) == null) {
            synchronized (LOCK) {
                if ((metric = metricsMap.get(resourceName)) == null) {
                    metric = new ParameterMetric();
                    metricsMap.put(resourceWrapper.getName(), metric);
                    RecordLog.info("[ParameterMetricStorage] Creating parameter metric for: {}", resourceWrapper.getName());
                }
            }
        }
        // ParameterMetric的初始化方法
        metric.initialize(rule);
    }
}

ParameterMetric初始化三个维度的计数器。

特点是这三个ConcurrentLinkedHashMapWrapper底层都是用的google包下的基于LRU算法的LinkedHashMap。超过最大容量后,会淘汰最近最少使用的kv对,以防止内存泄露。

public class ParameterMetric {
    private static final int THREAD_COUNT_MAX_CAPACITY = 4000;
    private static final int BASE_PARAM_MAX_CAPACITY = 4000;
    private static final int TOTAL_MAX_CAPACITY = 20_0000;
    private final Object lock = new Object();

    // 热点规则 - 入参 - 上次投递令牌时间 阈值类型为QPS时使用
    private final Map<ParamFlowRule, CacheMap<Object, AtomicLong>> ruleTimeCounters = new HashMap<>();
    // 热点规则 - 入参 - 令牌桶 阈值类型为QPS时使用
    private final Map<ParamFlowRule, CacheMap<Object, AtomicLong>> ruleTokenCounter = new HashMap<>();

    // 下标索引 - 索引位置对应入参 - 并发线程数 阈值类型为并发线程数时使用
    private final Map<Integer, CacheMap<Object, AtomicInteger>> threadCountMap = new HashMap<>();

     public void initialize(ParamFlowRule rule) {
        if (!ruleTimeCounters.containsKey(rule)) {
            synchronized (lock) {
                if (ruleTimeCounters.get(rule) == null) {
                    long size = Math.min(BASE_PARAM_MAX_CAPACITY * rule.getDurationInSec(), TOTAL_MAX_CAPACITY);
                    ruleTimeCounters.put(rule, new ConcurrentLinkedHashMapWrapper<Object, AtomicLong>(size));
                }
            }
        }

        if (!ruleTokenCounter.containsKey(rule)) {
            synchronized (lock) {
                if (ruleTokenCounter.get(rule) == null) {
                    long size = Math.min(BASE_PARAM_MAX_CAPACITY * rule.getDurationInSec(), TOTAL_MAX_CAPACITY);
                    ruleTokenCounter.put(rule, new ConcurrentLinkedHashMapWrapper<Object, AtomicLong>(size));
                }
            }
        }

        if (!threadCountMap.containsKey(rule.getParamIdx())) {
            synchronized (lock) {
                if (threadCountMap.get(rule.getParamIdx()) == null) {
                    threadCountMap.put(rule.getParamIdx(),
                        new ConcurrentLinkedHashMapWrapper<Object, AtomicInteger>(THREAD_COUNT_MAX_CAPACITY));
                }
            }
        }
    }
}

选择入参

ParamFlowChecker的passCheck方法,首先根据规则配置下标,选择入参。

注意虽然Sentinel只支持基础数据类型和String等类型的热点规则配置,但是通过编码方式可以实现任意类型参数的热点规则。这里看到passCheck对于ParamFlowArgument类型的入参做了特殊处理,将参与规则校验的入参,替换为了ParamFlowArgument.paramFlowKey方法返回的参数。

public final class ParamFlowChecker {

    public static boolean passCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count, Object... args) {
        if (args == null) {
            return true;
        }

        // 1. 选择value入参
        int paramIdx = rule.getParamIdx();
        if (args.length <= paramIdx) {
            return true;
        }
        Object value = args[paramIdx];
        // 入参实现ParamFlowArgument,可以实现任意object类型的入参配置热点规则
        if (value instanceof ParamFlowArgument) {
            value = ((ParamFlowArgument) value).paramFlowKey();
        }
        if (value == null) {
            return true;
        }

        // 2. 集群 or 单机规则校验
        if (rule.isClusterMode() && rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
            return passClusterCheck(resourceWrapper, rule, count, value);
        }

        return passLocalCheck(resourceWrapper, rule, count, value);
    }
}

这里暂时只看单机规则校验逻辑。

入参是否为集合

passLocalCheck区分了入参是否是集合或数组类型,如果是的话,循环集合或数组中每个元素,进行规则校验;如果是普通类型,直接进行规则校验。

// ParamFlowChecker.java
private static boolean passLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count,
                                      Object value) {
    try {
        // 集合类型,循环校验集合中每个参数
        if (Collection.class.isAssignableFrom(value.getClass())) {
            for (Object param : ((Collection)value)) {
                if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
                    return false;
                }
            }
        }
        // 数组类型,循环校验数组中每个参数
        else if (value.getClass().isArray()) {
            int length = Array.getLength(value);
            for (int i = 0; i < length; i++) {
                Object param = Array.get(value, i);
                if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
                    return false;
                }
            }
        }
        // 其他直接校验入参
        else {
            return passSingleValueCheck(resourceWrapper, rule, count, value);
        }
    } catch (Throwable e) {
        RecordLog.warn("[ParamFlowChecker] Unexpected error", e);
    }

    return true;
}

passSingleValueCheck单值规则校验。

当阈值类型为QPS时,区分流控效果,根据不同流控效果决定规则校验逻辑。注意在控制台上并不能设置流控效果,会采用默认流控效果;

当阈值类型为并发线程数时,通过ParameterMetric获取当前并发线程数,如果超过阈值,返回false。

// ParamFlowChecker.java
static boolean passSingleValueCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
                                    Object value) {
    // case1 : 阈值类型 QPS
    if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
        // case1-1 : 流控效果 排队等待
        if (rule.getControlBehavior() == RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) {
            return passThrottleLocalCheck(resourceWrapper, rule, acquireCount, value);
        } else {
            // case1-2 : 流控效果 默认
            return passDefaultLocalCheck(resourceWrapper, rule, acquireCount, value);
        }
    }
    // case2 : 阈值类型 并发线程数
    else if (rule.getGrade() == RuleConstant.FLOW_GRADE_THREAD) {
        // 例外项入参集合
        Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
        // 热点参数目前并发线程数
        long threadCount = getParameterMetric(resourceWrapper).getThreadCount(rule.getParamIdx(), value);
        // 如果热点参数在例外项中,取例外项中的阈值做校验,否则取热点规则默认阈值做校验
        if (exclusionItems.contains(value)) {
            int itemThreshold = rule.getParsedHotItems().get(value);
            return ++threadCount <= itemThreshold;
        }
        long threshold = (long)rule.getCount();
        return ++threadCount <= threshold;
    }
    return true;
}

阈值类型=QPS,流控效果=默认

当流控效果不为RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER排队等待时,即为默认流控效果逻辑。默认流控效果通过令牌桶算法实现,当请求进来后cas修改令牌桶,如果令牌数量小于0,则返回false。

  1. 确定入参是否匹配例外项,如果匹配例外项,阈值QPS取例外项的,否则取规则级别的阈值QPS;
  2. 假设配置时间窗口为3s,则每3秒对应一个令牌桶,令牌初始数量=配置阈值QPS + 配置burstCount,这个burstCount默认是0,比如配置阈值QPS是10,则令牌桶默认情况下初始容量为10;
  3. 如果距离上次访问超过3s时间窗口,按照时间比率 * 阈值QPS,填充令牌桶。比如当前距离上次访问为4s,阈值QPS为10,则新增token = (4 - 3) * 10 / 3 = 3,假设剩余token为2,本次申请token为1,令牌数量更新为Math.min(10, 3+ 2 - 1) = 10,不能超过令牌桶的最大容量;
  4. 如果距离上次访问不超过3s时间窗口,从原有令牌桶中获取令牌。如果令牌不足,返回false;
  5. 每次添加token后,都更新规则对应的时间计数器,用于后续时间窗口判断;
// ParamFlowChecker.java
// 阈值类型QPS 流控效果默认
static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
                                     Object value) {
    // 规则对应 ParameterMetric
    ParameterMetric metric = getParameterMetric(resourceWrapper);
    // 规则对应 令牌桶
    CacheMap<Object, AtomicLong> tokenCounters = metric == null ? null : metric.getRuleTokenCounter(rule);
    // 规则对应 上次添加令牌时间
    CacheMap<Object, AtomicLong> timeCounters = metric == null ? null : metric.getRuleTimeCounter(rule);

    if (tokenCounters == null || timeCounters == null) {
        return true;
    }

    // 将QPS阈值转换为令牌概念 令牌数量 = QPS
    Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
    long tokenCount = (long)rule.getCount();
    if (exclusionItems.contains(value)) {
        tokenCount = rule.getParsedHotItems().get(value);
    }

    if (tokenCount == 0) {
        return false;
    }

    // 获取token的上限数量 = 阈值 + rule.burstCount(0)
    long maxCount = tokenCount + rule.getBurstCount();
    if (acquireCount > maxCount) {
        return false;
    }

    while (true) {
        long currentTime = TimeUtil.currentTimeMillis();

        // case1 : 从来没有获取过token
        // 尝试更新上次添加token的时间
        AtomicLong lastAddTokenTime = timeCounters.putIfAbsent(value, new AtomicLong(currentTime));
        if (lastAddTokenTime == null) {
            // 添加token = 上限 - 本次需要获取数量
            tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));
            return true;
        }

        long passTime = currentTime - lastAddTokenTime.get();
        if (passTime > rule.getDurationInSec() * 1000) {
            // case2 : 当规则配置时间窗口过去后,计算令牌桶token数量
            AtomicLong oldQps = tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));
            if (oldQps == null) {
                lastAddTokenTime.set(currentTime);
                return true;
            } else {
                // 剩余token
                long restQps = oldQps.get();
                // 新增token = 阈值qps * 过去x秒没添加过token / 时间窗口大小
                long toAddCount = (passTime * tokenCount) / (rule.getDurationInSec() * 1000);
                // 确保新增token不超过令牌桶容量
                long newQps = toAddCount + restQps > maxCount ? (maxCount - acquireCount)
                    : (restQps + toAddCount - acquireCount);

                // 如果增加之后 token数量仍然小于0,则不通过
                if (newQps < 0) {
                    return false;
                }
                // cas修改令牌桶
                if (oldQps.compareAndSet(restQps, newQps)) {
                    lastAddTokenTime.set(currentTime);
                    return true;
                }
                Thread.yield();
            }
        } else {
            // case3 : 仍然处于当前时间窗口,令牌数量 -= 资源请求数量
            AtomicLong oldQps = tokenCounters.get(value);
            if (oldQps != null) {
                long oldQpsValue = oldQps.get();
                if (oldQpsValue - acquireCount >= 0) {
                    if (oldQps.compareAndSet(oldQpsValue, oldQpsValue - acquireCount)) {
                        return true;
                    }
                } else {
                    return false;
                }
            }
            Thread.yield();
        }
    }
}

阈值类型=QPS,流控效果=排队等待

Sentinel控制台上没有配置排队等待流控效果的热点规则入口,所以这类配置只能通过编码方式实现

当流控效果为CONTROL_BEHAVIOR_RATE_LIMITER排队等待时,使用漏桶算法,控制请求速率。实现逻辑类似普通流控规则的RateLimiterController。不同点在于RateLimiterController没有时间窗口配置项,以QPS代表的秒作为窗口,而热点规则这里有时间窗口概念。此外,RateLimiterController没有使用cas,而是尝试addAndGet,如果add之后超出阈值,回滚,并返回失败;而热点规则这里是通过cas+循环的方式更新上次访问时间戳。

// ParamFlowChecker.java
// 阈值类型=QPS,流控效果=排队等待
static boolean passThrottleLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
                                      Object value) {
    ParameterMetric metric = getParameterMetric(resourceWrapper);
    CacheMap<Object, AtomicLong> timeRecorderMap = metric == null ? null : metric.getRuleTimeCounter(rule);
    if (timeRecorderMap == null) {
        return true;
    }
    // 1. 根据入参是否在例外项,选择配置阈值tokenCount
    Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
    long tokenCount = (long)rule.getCount();
    if (exclusionItems.contains(value)) {
        tokenCount = rule.getParsedHotItems().get(value);
    }

    if (tokenCount == 0) {
        return false;
    }

    // 2. 计算当前时间窗口预计耗时 = 1 / qps * 时间窗口大小 = 单次请求RT * 时间窗口大小
    long costTime = Math.round(1.0 * 1000 * acquireCount * rule.getDurationInSec() / tokenCount);
    while (true) {
        long currentTime = TimeUtil.currentTimeMillis();
        AtomicLong timeRecorder = timeRecorderMap.putIfAbsent(value, new AtomicLong(currentTime));
        if (timeRecorder == null) {
            return true;
        }
        long lastPassTime = timeRecorder.get();
        // 期望本次窗口结束时间 = 上次通过时间 + 当前时间窗口预计耗时
        long expectedTime = lastPassTime + costTime;

        // 如果期望时间小于当前时间 或 期望时间 - 当前时间 小于 排队超时时间,
        if (expectedTime <= currentTime || expectedTime - currentTime < rule.getMaxQueueingTimeMs()) {
            AtomicLong lastPastTimeRef = timeRecorderMap.get(value);
            if (lastPastTimeRef.compareAndSet(lastPassTime, currentTime)) {
                // 如果期望时间需要排队,则睡眠
                long waitTime = expectedTime - currentTime;
                if (waitTime > 0) {
                    lastPastTimeRef.set(expectedTime);
                    try {
                        TimeUnit.MILLISECONDS.sleep(waitTime);
                    } catch (InterruptedException e) {
                        RecordLog.warn("passThrottleLocalCheck: wait interrupted", e);
                    }
                }
                return true;
            } else {
                Thread.yield();
            }
        } else {
            return false;
        }
    }
}

从上述逻辑还可以得知,QPS阈值类型的热点规则中,它使用的流量指标都是在ParamFlowSlot中自己统计的,并非在统一的StatisticSlot中通过Node统计的。

阈值类型=线程数

ParamFlowChecker.passSingleValueCheck对于并发线程数的校验很简单:

  1. 获取ParamterMetric中记录的当前下标参数的并发线程数threadCount;
  2. 判断并发线程数是否超过阈值。如果下标参数在例外项中,阈值取例外项配置阈值,否则取热点规则级别阈值。
// ParamFlowChecker.java
static boolean passSingleValueCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
                                    Object value) {
    // case1 : 阈值类型 QPS
    if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
        // ...
    }
    // case2 : 阈值类型 并发线程数
    else if (rule.getGrade() == RuleConstant.FLOW_GRADE_THREAD) {
        // 例外项入参集合
        Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
        // 热点参数目前并发线程数
        long threadCount = getParameterMetric(resourceWrapper).getThreadCount(rule.getParamIdx(), value);
        // 如果热点参数在例外项中,取例外项中的阈值做校验,否则取热点规则默认阈值做校验
        if (exclusionItems.contains(value)) {
            int itemThreshold = rule.getParsedHotItems().get(value);
            return ++threadCount <= itemThreshold;
        }
        long threshold = (long)rule.getCount();
        return ++threadCount <= threshold;
    }
    return true;
}

那么这个并发线程数是在哪里统计的呢?

StatisticSlotentry方法中,调用了ProcessorSlotEntryCallbackonPass方法;

StatisticSlotexit方法中,调用了ProcessorSlotExitCallbackonExit方法;

public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        try {
            // 1. 先执行后续Slot的entry
            fireEntry(context, resourceWrapper, node, count, prioritized, args);
            // 2. 统计数据ThreadNum++ passRequest++
            // ...
            // 3. 给用户的扩展点,可以通过SPI加载
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
                handler.onPass(context, resourceWrapper, node, count, args);
            }
        } catch (PriorityWaitException ex) {
            // ...
        } catch (BlockException e) {
            // 1. 设置BlockError到上下文中
            context.getCurEntry().setBlockError(e);
            // 2. 统计数据 BlockQps++
            // ...
            // 3. 给用户的扩展点,可以通过SPI加载
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
                handler.onBlocked(e, context, resourceWrapper, node, count, args);
            }
            throw e;
        } catch (Throwable e) {
            context.getCurEntry().setError(e);
            throw e;
        }
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        Node node = context.getCurNode();
        // 1. 如果没有发生BlockException,统计响应时间RT和错误率exceptionQps
        if (context.getCurEntry().getBlockError() == null) {
            // ...
        }
        // 2. 给用户的扩展点,可以通过SPI加载
        Collection<ProcessorSlotExitCallback> exitCallbacks = StatisticSlotCallbackRegistry.getExitCallbacks();
        for (ProcessorSlotExitCallback handler : exitCallbacks) {
            handler.onExit(context, resourceWrapper, count, args);
        }

        // 3. 执行后续Slot的exit方法
        fireExit(context, resourceWrapper, count);
    }
}

这两个回调函数,都是通过SPI加载的。

首先SPI会加载InitFunc=ParamFlowStatisticSlotCallbackInit,执行init方法时,注册了两个回调函数。

public class ParamFlowStatisticSlotCallbackInit implements InitFunc {

    @Override
    public void init() {
        StatisticSlotCallbackRegistry.addEntryCallback(ParamFlowStatisticEntryCallback.class.getName(),
            new ParamFlowStatisticEntryCallback());
        StatisticSlotCallbackRegistry.addExitCallback(ParamFlowStatisticExitCallback.class.getName(),
            new ParamFlowStatisticExitCallback());
    }
}
public class ParamFlowStatisticEntryCallback implements ProcessorSlotEntryCallback<DefaultNode> {
    @Override
    public void onPass(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) {
        ParameterMetric parameterMetric = ParameterMetricStorage.getParamMetric(resourceWrapper);
        if (parameterMetric != null) {
            parameterMetric.addThreadCount(args);
        }
    }
}
public class ParamFlowStatisticExitCallback implements ProcessorSlotExitCallback {
    @Override
    public void onExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        if (context.getCurEntry().getBlockError() == null) {
            ParameterMetric parameterMetric = ParameterMetricStorage.getParamMetric(resourceWrapper);
            if (parameterMetric != null) {
                parameterMetric.decreaseThreadCount(args);
            }
        }
    }
}

看一下ParameterMetric的addThreadCount方法实现。ParameterMetric的addThreadCount方法,遍历所有args参数列表,对于每个参数都进行threadCount++。

但是这里发现没有判断入参是否为ParamFlowArgument类型,也就是说如果入参为自定义Object类型,无法支持阈值类型为线程数的热点参数规则。这个特性我已经提交了PR。

// ParameterMetric.java
public void addThreadCount(Object... args) {
    if (args == null) {
        return;
    }

    try {
        // 循环所有参数
        for (int index = 0; index < args.length; index++) {
            CacheMap<Object, AtomicInteger> threadCount = threadCountMap.get(index);
            if (threadCount == null) {
                continue;
            }
            Object arg = args[index];
            if (arg == null) {
                continue;
            }
                        // 1-集合
            if (Collection.class.isAssignableFrom(arg.getClass())) {
                for (Object value : ((Collection)arg)) {
                    // ... 处理同普通类型
                }
            }
              // 2-数组
            else if (arg.getClass().isArray()) {
                int length = Array.getLength(arg);
                for (int i = 0; i < length; i++) {
                    // ... 处理同普通类型
                }
            } else {
                AtomicInteger oldValue = threadCount.putIfAbsent(arg, new AtomicInteger());
                if (oldValue != null) {
                    oldValue.incrementAndGet();
                } else {
                    threadCount.put(arg, new AtomicInteger(1));
                }
            }
        }
    } catch (Throwable e) {
        RecordLog.warn("[ParameterMetric] Param exception", e);
    }
}

2、DegradeSlot

DegradeSlot负责处理降级规则校验。通过设置不同熔断策略,实现不同的熔断逻辑。Sentinel的降级和熔断是一个概念,而在Hystrix中熔断只是代表断路器打开,降级代表fallback方法。

控制台-降级规则.png

编码方式配置降级规则案例:

// 慢调用比率
private static void initDegradeRule() {
    List<DegradeRule> rules = new ArrayList<>();
    DegradeRule rule = new DegradeRule(KEY)
        .setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
        // Max allowed response time
        .setCount(50)
        // Retry timeout (in second)
        .setTimeWindow(10)
        // Circuit breaker opens when slow request ratio > 60%
        .setSlowRatioThreshold(0.6)
        .setMinRequestAmount(100)
        .setStatIntervalMs(20000);
    rules.add(rule);
    DegradeRuleManager.loadRules(rules);
}

// 异常率
private static void initDegradeRule() {
  List<DegradeRule> rules = new ArrayList<>();
  DegradeRule rule = new DegradeRule(KEY)
    .setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType())
    // Set ratio threshold to 50%.
    .setCount(0.5d)
    .setStatIntervalMs(30000)
    .setMinRequestAmount(50)
    // Retry timeout (in second)
    .setTimeWindow(10);
  rules.add(rule);
  DegradeRuleManager.loadRules(rules);
  System.out.println("Degrade rule loaded: " + rules);
}

DegradeRule成员变量如下:

public class DegradeRule extends AbstractRule {
    // 熔断策略 0-慢调用比率 1-异常率 2-异常数
    private int grade = RuleConstant.DEGRADE_GRADE_RT;
    // 阈值
    // 当grade = 慢调用比率 时,阈值=最大rt
    // 当grade = 异常率 时,阈值=异常率
    // 当grade = 异常数 时,阈值=异常数
    private double count;
    // 熔断时长(秒):断路器 从 开放 变为 半开 的时长
    private int timeWindow;
    // 最少请求数 ---> 断路器起效 默认5
    private int minRequestAmount = RuleConstant.DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT;
    // 使用grade=慢调用比率时,代表慢调用比率的阈值
    private double slowRatioThreshold = 1.0d;
    // 统计时长(毫秒)--- 时间窗口大小
    private int statIntervalMs = 1000;
}

CircuitBreaker

在DegradeRuleManager中,所有的降级规则,都会转换为CircuitBreaker断路器

根据规则的grade不同,分为两种断路器:

  1. 如果grade=0,慢调用比率,则创建ResponseTimeCircuitBreaker;
  2. 如果grade=1或2,异常率或异常数,则创建ExceptionCircuitBreaker;
public final class DegradeRuleManager {
    // 资源名称 - 断路器集合
    private static volatile Map<String, List<CircuitBreaker>> circuitBreakers = new HashMap<>();
    // 资源名称 - 降级规则集合
    private static volatile Map<String, Set<DegradeRule>> ruleMap = new HashMap<>();

    private static CircuitBreaker getExistingSameCbOrNew(DegradeRule rule) {
          List<CircuitBreaker> cbs = getCircuitBreakers(rule.getResource());
          if (cbs == null || cbs.isEmpty()) {
              return newCircuitBreakerFrom(rule);
          }
          for (CircuitBreaker cb : cbs) {
              if (rule.equals(cb.getRule())) {
                  return cb;
              }
          }
          return newCircuitBreakerFrom(rule);
      }
      // 根据降级规则创建断路器
      private static CircuitBreaker newCircuitBreakerFrom(DegradeRule rule) {
          switch (rule.getGrade()) {
              case RuleConstant.DEGRADE_GRADE_RT:
                  return new ResponseTimeCircuitBreaker(rule);
              case RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO:
              case RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT:
                  return new ExceptionCircuitBreaker(rule);
              default:
                  return null;
          }
      }
}

CircuitBreaker类图.png

CircuitBreaker接口,定义了Sentinel断路器需要具备的能力,定义了断路器三种状态:完全开放、半开、关闭

public interface CircuitBreaker {
    // 对应降级规则
    DegradeRule getRule();
    // 是否允许请求通过
    boolean tryPass(Context context);
    // 断路器状态
    State currentState();
    // 请求完成后的钩子方法
    void onRequestComplete(Context context);
    // 断路器状态
    enum State {
        OPEN,
        HALF_OPEN,
        CLOSED
    }
}

在DegradeSlot中,entry方法,调用每个断路器的tryPass方法,判断是否可以通过;exit方法,当没有发生BlockException的情况下,会调用断路器的onRequestComplete方法,统计数据并变更断路器状态

@Spi(order = Constants.ORDER_DEGRADE_SLOT)
public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        performChecking(context, resourceWrapper);

        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    void performChecking(Context context, ResourceWrapper r) throws BlockException {
        // 获取资源对应所有断路器
        List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
        if (circuitBreakers == null || circuitBreakers.isEmpty()) {
            return;
        }
        // 断路器决定是否放行请求
        for (CircuitBreaker cb : circuitBreakers) {
            if (!cb.tryPass(context)) {
                throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
            }
        }
    }

    @Override
    public void exit(Context context, ResourceWrapper r, int count, Object... args) {
        Entry curEntry = context.getCurEntry();
        if (curEntry.getBlockError() != null) {
            fireExit(context, r, count, args);
            return;
        }
        List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
        if (circuitBreakers == null || circuitBreakers.isEmpty()) {
            fireExit(context, r, count, args);
            return;
        }

        // 触发所有断路器的onRequestComplete方法
        if (curEntry.getBlockError() == null) {
            for (CircuitBreaker circuitBreaker : circuitBreakers) {
                circuitBreaker.onRequestComplete(context);
            }
        }

        fireExit(context, r, count, args);
    }
}

AbstractCircuitBreaker是断路器的抽象实现,实现了大部分断路器逻辑,包括CAS状态变更、tryPass等

public abstract class AbstractCircuitBreaker implements CircuitBreaker {
    // 降级规则
    protected final DegradeRule rule;
    // 恢复超时时间(ms)--- rule.timeWindow --- 熔断时间
    protected final int recoveryTimeoutMs;
    // 观察者注册中心,当断路器状态变更,会通知所有观察者 --- 目前只留给用户扩展
    private final EventObserverRegistry observerRegistry;
    // 断路器状态
    protected final AtomicReference<State> currentState = new AtomicReference<>(State.CLOSED);
    // 半开状态下,下次重试时间戳
    protected volatile long nextRetryTimestamp;

     AbstractCircuitBreaker(DegradeRule rule, EventObserverRegistry observerRegistry) {
        this.observerRegistry = observerRegistry;
        this.rule = rule;
        this.recoveryTimeoutMs = rule.getTimeWindow() * 1000;
    }
}

先来看一下tryPass方法的实现。

case1:如果断路器关闭,直接放行;

case2:如果断路器开放,需要判断当前时间是否已经超过熔断时间窗口nextRetryTimestamp,如果超过,尝试执行fromOpenToHalfOpen方法,将断路器变为半开状态;

case3:如果断路器半开,拒绝,半开状态,只允许一个请求通过(进入case2且fromOpenToHalfOpen执行成功的请求),其余请求通通拒绝;

// AbstractCircuitBreaker.java
// 半开状态下,下次重试时间戳
protected volatile long nextRetryTimestamp;
// 断路器状态
protected final AtomicReference<State> currentState = new AtomicReference<>(State.CLOSED);
@Override
public boolean tryPass(Context context) {
    if (currentState.get() == State.CLOSED) {
        return true;
    }
    if (currentState.get() == State.OPEN) {
        return retryTimeoutArrived() && fromOpenToHalfOpen(context);
    }
    return false;
}
protected boolean retryTimeoutArrived() {
  return TimeUtil.currentTimeMillis() >= nextRetryTimestamp;
}

重点关注fromOpenToHalfOpen方法,断路器从开放到半开的逻辑。

首先CAS将断路器状态从开,变为半开,如果CAS失败,返回false拒绝请求。这里保证半开状态断路器只允许放行一个探测请求

然后给当前Entry注册一个回调钩子,当Entry退出时会被调用,如果后续规则校验抛出了BlockException,也将当前断路器重新打开

这里注册回调方法是为了修复ISSUE#1638的BUG,由于当前降级规则之后的规则校验失败,抛出BlockException,导致当前降级规则对应断路器始终处于半开状态(DegradeSlot的exit方法,判断如果发生BlockException,不会调用CircuitBreaker的onRequestComplete钩子方法更新断路器状态)。

// AbstractCircuitBreaker.java
protected boolean fromOpenToHalfOpen(Context context) {
    if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
        notifyObservers(State.OPEN, State.HALF_OPEN, null);
        Entry entry = context.getCurEntry();
        // 注册一个回调方法,修复#1638
        entry.whenTerminate(new BiConsumer<Context, Entry>() {
            @Override
            public void accept(Context context, Entry entry) {
                if (entry.getBlockError() != null) {
                    currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
                    notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d);
                }
            }
        });
        return true;
    }
    return false;
}

此外AbstractCircuitBreaker为子类提供了状态变更的实现方法。

从半开到开,需要更新下次重试时间 = 当前时间 + 熔断时长:

// AbstractCircuitBreaker.java
protected boolean fromHalfOpenToOpen(double snapshotValue) {
    if (currentState.compareAndSet(State.HALF_OPEN, State.OPEN)) {
        // 根据熔断时间更新下次重试时间
        updateNextRetryTimestamp();
        notifyObservers(State.HALF_OPEN, State.OPEN, snapshotValue);
        return true;
    }
    return false;
}
// 下次重试时间 = 下次从开放变为半开状态的时间 = 当前时间 + 熔断时长
protected void updateNextRetryTimestamp() {
  this.nextRetryTimestamp = TimeUtil.currentTimeMillis() + recoveryTimeoutMs;
}

从关到开,和从半开到开逻辑一致:

// AbstractCircuitBreaker.java
protected boolean fromCloseToOpen(double snapshotValue) {
    State prev = State.CLOSED;
    if (currentState.compareAndSet(prev, State.OPEN)) {
        // 根据熔断时间更新下次重试时间
        updateNextRetryTimestamp();
        notifyObservers(prev, State.OPEN, snapshotValue);
        return true;
    }
    return false;
}

从半开到关,调用子类实现的resetStat方法,用于清除子类保存的统计数据:

// AbstractCircuitBreaker.java
protected boolean fromHalfOpenToClose() {
    if (currentState.compareAndSet(State.HALF_OPEN, State.CLOSED)) {
        // 子类实现 清除统计数据
        resetStat();
        notifyObservers(State.HALF_OPEN, State.CLOSED, null);
        return true;
    }
    return false;
}
abstract void resetStat();

根据熔断策略不同,不同断路器实现类的onRequestComplete方法逻辑不同,分为异常断路器ExceptionCircuitBreaker和慢调用断路器ResponseTimeCircuitBreaker

异常率和异常数

控制台-降级规则-异常率:

控制台-降级规则-异常率.png

控制台-降级规则-异常数:

控制台-降级规则-异常数.png

如果使用异常率和异常数的降级规则,在使用上与其他规则有些不同,需要手动记录发生的业务异常,否则统计不到错误请求

Entry entry = null;
try {
    entry = SphU.entry(KEY);
    //...
} catch (BlockException e) {
    // ...
} catch (Throwable t) {
    // 手动记录业务异常到当前Entry
    Tracer.traceEntry(t, entry);
} finally {
    if (entry != null) {
        // 这里才能统计到异常次数
        entry.exit();
    }
}

ExceptionCircuitBreaker实现了基于异常率和异常数的断路器。使用Sentinel自己的LeapArray来统计每个时间窗口的数据,每个窗口大小,取决于配置的统计时长statInterval。

public class ExceptionCircuitBreaker extends AbstractCircuitBreaker {
    // 熔断策略 1-异常率 2-异常数
    private final int strategy;
    // 最小请求数
    private final int minRequestAmount;
    // 阈值
    private final double threshold;
    // 窗口数据统计
    private final LeapArray<SimpleErrorCounter> stat;
     public ExceptionCircuitBreaker(DegradeRule rule) {
        this(rule, new SimpleErrorCounterLeapArray(1, rule.getStatIntervalMs()));
    }
    ExceptionCircuitBreaker(DegradeRule rule, LeapArray<SimpleErrorCounter> stat) {
        super(rule);
        this.strategy = rule.getGrade();
        boolean modeOk = strategy == DEGRADE_GRADE_EXCEPTION_RATIO || strategy == DEGRADE_GRADE_EXCEPTION_COUNT;
        AssertUtil.isTrue(modeOk, "rule strategy should be error-ratio or error-count");
        AssertUtil.notNull(stat, "stat cannot be null");
        this.minRequestAmount = rule.getMinRequestAmount();
        this.threshold = rule.getCount();
        this.stat = stat;
    }
}

SimpleErrorCounterLeapArray继承LeapArray只是负责时间窗口处理。

注意传入父类LeapArray构造方法的两个参数,第一个sampleCount决定了存储几个时间窗口,ExceptionCircuitBreaker传入了1,表示只存储一个窗口;第二个intervalInMs取自降级规则的统计时长决定每个窗口的大小。

// 窗口统计
static class SimpleErrorCounterLeapArray extends LeapArray<SimpleErrorCounter> {
    public SimpleErrorCounterLeapArray(int sampleCount, int intervalInMs) {
      super(sampleCount, intervalInMs);
    }

    @Override
    public SimpleErrorCounter newEmptyBucket(long timeMillis) {
      return new SimpleErrorCounter();
    }

    @Override
    protected WindowWrap<SimpleErrorCounter> resetWindowTo(WindowWrap<SimpleErrorCounter> w, long startTime) {
      w.resetTo(startTime);
      w.value().reset();
      return w;
    }
}

实际统计数据靠LeapArray泛型中的对象,如这里错误率和错误数统计,使用SimpleErrorCounter

static class SimpleErrorCounter {
  // 错误数
  private LongAdder errorCount;
  // 总数
  private LongAdder totalCount;
  public SimpleErrorCounter() {
      this.errorCount = new LongAdder();
      this.totalCount = new LongAdder();
  }
  public SimpleErrorCounter reset() {
      errorCount.reset();
      totalCount.reset();
      return this;
  }
}

ExceptionCircuitBreaker一共实现了两个方法,第一个是resetStat方法。

当断路器变为关闭,会调用子类resetStat方法,清除当前窗口统计数据,错误数和总数清零。

// ExceptionCircuitBreaker.java
// 窗口数据统计
private final LeapArray<SimpleErrorCounter> stat;
protected void resetStat() {
    stat.currentWindow().value().reset();
}

第二个方法是onRequestComplete方法,只有Entry正常通过或发生非BlockException时(见DegradeSlot.exit),才会进入这个方法。

// ExceptionCircuitBreaker.java
public void onRequestComplete(Context context) {
    Entry entry = context.getCurEntry();
    if (entry == null) {
        return;
    }
    // 有异常,记录异常数
    Throwable error = entry.getError();
    SimpleErrorCounter counter = stat.currentWindow().value();
    if (error != null) {
        counter.getErrorCount().add(1);
    }
    // 记录总数
    counter.getTotalCount().add(1);

    // 判断断路器状态是否要变更
    handleStateChangeWhenThresholdExceeded(error);
}

根据当前断路器的状态和错误统计情况,ExceptionCircuitBreaker选择是否要改变断路器状态:

  1. 如果断路器开启,子类什么都不做,只能由父类tryPass方法负责从OPEN变为HALF_OPEN(根据熔断时间);
  2. 如果断路器半开,判断当前调用是否发生异常。如果发生异常,重新开启断路器,并根据熔断时间更新下次重试时间;如果没发生异常,关闭断路器,清空统计数据;
  3. 如果断路器关闭,首先判断1个时间窗口内的请求总数,是否超过降级规则的最小请求数,如果小于这个数,不做处理(即使到达错误阈值)。如果超过最小请求数量,校验错误数或错误率是否超过阈值,如果超过阈值,开启断路器,并根据熔断时间更新下次重试时间。
// ExceptionCircuitBreaker.java
private void handleStateChangeWhenThresholdExceeded(Throwable error) {
    // 1. 如果当前状态是OPEN,不变,由父类tryPass方法负责从OPEN变为HALF_OPEN
    if (currentState.get() == State.OPEN) {
        return;
    }
    // 2. 如果当前状态是HALF_OPEN,判断本次请求是否发生异常,如果发生异常,重新变为OPEN,否则变为CLOSE
    if (currentState.get() == State.HALF_OPEN) {
        if (error == null) {
            fromHalfOpenToClose();
        } else {
            fromHalfOpenToOpen(1.0d);
        }
        return;
    }
    // 3. 当前是CLOSE状态,根据统计数据,判断是否需要OPEN
    List<SimpleErrorCounter> counters = stat.values();
    long errCount = 0;
    long totalCount = 0;
    // 其实这边只会有一个窗口,因为构造SimpleErrorCounterLeapArray时窗口数量是1
    for (SimpleErrorCounter counter : counters) {
        errCount += counter.errorCount.sum();
        totalCount += counter.totalCount.sum();
    }
    // 如果窗口时间内,请求数量不足,不做处理
    if (totalCount < minRequestAmount) {
        return;
    }
    // 错误数或错误率
    double curCount = errCount;
    if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) {
        curCount = errCount * 1.0d / totalCount;
    }
    // 如果错误数或错误率超过阈值,调用父类方法OPEN断路器,并更新下次重试时间
    if (curCount > threshold) {
        transformToOpen(curCount);
    }
}

其实Sentinel的DegradeSlot在根据异常率熔断时,和Hystrix的逻辑是差不多的。

类比Hystrix的几个配置:

Sentinel配置项Hystrix配置项释义
DegradeRule.statIntervalMsmetrics.rollingStats.timeInMilliseconds数据统计时间窗口大小
DegradeRule.minRequestAmountcircuitBreaker.requestVolumeThreshold满足一个时间窗口内,请求数到达多少,才判断熔断阈值
DegradeRule.countcircuitBreaker.errorThresholdPercentage异常率阈值
DegradeRule.timeWindowcircuitBreaker.sleepWindowInMilliseconds熔断时长,断路器从OPEN到HALF_OPEN的时长

慢调用比率

控制台-降级规则-.png

ResponseTimeCircuitBreaker实现了基于慢调用比率控制的断路器。

public class ResponseTimeCircuitBreaker extends AbstractCircuitBreaker {

    // 最大允许响应时间 = DegradeRule.count
    private final long maxAllowedRt;
    // 最大慢请求比率 = DegradeRule.slowRatioThreshold
    private final double maxSlowRequestRatio;
    // 最小请求数量
    private final int minRequestAmount;
    // 窗口数据统计
    private final LeapArray<SlowRequestCounter> slidingCounter;

    public ResponseTimeCircuitBreaker(DegradeRule rule) {
        this(rule, new SlowRequestLeapArray(1, rule.getStatIntervalMs()));
    }

    ResponseTimeCircuitBreaker(DegradeRule rule, LeapArray<SlowRequestCounter> stat) {
        super(rule);
        AssertUtil.isTrue(rule.getGrade() == RuleConstant.DEGRADE_GRADE_RT, "rule metric type should be RT");
        AssertUtil.notNull(stat, "stat cannot be null");
        this.maxAllowedRt = Math.round(rule.getCount());
        this.maxSlowRequestRatio = rule.getSlowRatioThreshold();
        this.minRequestAmount = rule.getMinRequestAmount();
        this.slidingCounter = stat;
    }
}

与错误统计类似,这里用LeapArray+SlowRequestCounter记录慢调用数据。

static class SlowRequestLeapArray extends LeapArray<SlowRequestCounter> {
    public SlowRequestLeapArray(int sampleCount, int intervalInMs) {
        super(sampleCount, intervalInMs);
    }
    @Override
    public SlowRequestCounter newEmptyBucket(long timeMillis) {
        return new SlowRequestCounter();
    }
    @Override
    protected WindowWrap<SlowRequestCounter> resetWindowTo(WindowWrap<SlowRequestCounter> w, long startTime) {
        w.resetTo(startTime);
        w.value().reset();
        return w;
    }
}

static class SlowRequestCounter {
  // 慢调用数量
  private LongAdder slowCount;
  // 总数量
  private LongAdder totalCount;
  public SlowRequestCounter reset() {
    slowCount.reset();
    totalCount.reset();
    return this;
  }
}

当断路器关闭,重置当前窗口统计数据。

// ResponseTimeCircuitBreaker.java
// 窗口数据统计
private final LeapArray<SlowRequestCounter> slidingCounter;
public void resetStat() {
    slidingCounter.currentWindow().value().reset();
}

重点关注onRequestComplete方法。

首先统计数据,如果本次请求响应时间超过最大允许响应时间,慢调用次数+1。

// ResponseTimeCircuitBreaker.java
public void onRequestComplete(Context context) {
    // 当前窗口统计数据
    SlowRequestCounter counter = slidingCounter.currentWindow().value();
    Entry entry = context.getCurEntry();
    if (entry == null) {
        return;
    }
    long completeTime = entry.getCompleteTimestamp();
    if (completeTime <= 0) {
        completeTime = TimeUtil.currentTimeMillis();
    }
    long rt = completeTime - entry.getCreateTimestamp();
    // 如果响应时间,超过最大响应时间,记录慢调用次数
    if (rt > maxAllowedRt) {
        counter.slowCount.add(1);
    }
    // 记录总调用次数
    counter.totalCount.add(1);

    // 判断断路器状态是否要变更
    handleStateChangeWhenThresholdExceeded(rt);
}

handleStateChangeWhenThresholdExceeded根据断路器状态和慢调用统计数据,决定断路器状态是否变更,逻辑与错误率类似:

  1. 如果断路器开启,子类什么都不做,只能由父类tryPass方法负责从OPEN变为HALF_OPEN(根据熔断时间);
  2. 如果断路器半开,判断当前调用是否超出RT阈值。如果超出,重新开启断路器,并根据熔断时间更新下次重试时间;如果没超出,关闭断路器,清空统计数据;
  3. 如果断路器关闭,首先判断1个时间窗口内的请求总数,是否超过降级规则的最小请求数,如果小于这个数,不做处理(即使到达慢调用比率阈值)。如果超过最小请求数量,校验慢调用比率是否超过阈值,如果超过阈值,开启断路器,并根据熔断时间更新下次重试时间。
// ResponseTimeCircuitBreaker.java
private void handleStateChangeWhenThresholdExceeded(long rt) {
    // 1. 如果OPEN,不处理
    if (currentState.get() == State.OPEN) {
        return;
    }

    // 2. 如果HALF_OPEN,根据本次请求,调用速度有没有恢复到正常水平,决定 OPEN or CLOSE
    if (currentState.get() == State.HALF_OPEN) {
        if (rt > maxAllowedRt) {
            fromHalfOpenToOpen(1.0d);
        } else {
            fromHalfOpenToClose();
        }
        return;
    }

    // 3. 如果CLOSE,判断慢调用比率是否超过阈值,如果超过阈值,OPEN
    List<SlowRequestCounter> counters = slidingCounter.values();
    long slowCount = 0;
    long totalCount = 0;
    for (SlowRequestCounter counter : counters) {
        slowCount += counter.slowCount.sum();
        totalCount += counter.totalCount.sum();
    }
    if (totalCount < minRequestAmount) {
        return;
    }
    // 慢调用比率 = 慢调用次数 / 总次数
    double currentRatio = slowCount * 1.0d / totalCount;
    if (currentRatio > maxSlowRequestRatio) {
        transformToOpen(currentRatio);
    }
    // 特例,当前慢调用比率=配置慢调用比率=1
    if (Double.compare(currentRatio, maxSlowRequestRatio) == 0 &&
            Double.compare(maxSlowRequestRatio, SLOW_REQUEST_RATIO_MAX_VALUE) == 0) {
        transformToOpen(currentRatio);
    }
}

总结

本章学习了两个规则校验:

  1. ParamFlowSlot:热点参数规则校验。在FlowSlot流控规则校验的基础上,增加了参数例外项。资源除了基础的阈值限制以外,可以控制不同入参的阈值限制。

    从编码角度,热点参数规则支持QPS线程数两种阈值类型。

    阈值类型=QPS:支持默认流控效果排队等待流控效果。默认流控效果,使用令牌桶算法,将QPS转换为令牌;排队等待流控效果,使用漏桶算法,将QPS转换为RT;

    阈值类型=并发线程数:在StatisticSlot中,通过热点规则注入的回调函数,统计每个参数的并发线程数。在ParamFlowSlot中利用这些数据做规则校验;

  2. DegradeSlot:降级规则校验。

    每个降级规则对应一个断路器CircuitBreaker。断路器有三种状态

    1):统计数据超过规则阈值,禁止请求通过;

    2)半开:断路器保持开启状态超过熔断时间,仅允许一个请求通过。如果这个请求正常通过(取决于熔断策略,错误策略需要该请求无异常,慢调用比率策略需要该请求RT小于最大RT阈值),断路器关闭,否则恢复开放;

    3)关闭:初始状态,允许请求通过。

断路器状态.png

熔断策略有三种:

1)异常率:与Hystrix完全类似,统计一个时间窗口内的错误率。在断路器关闭时,在满足最小请求数量的基础上,如果异常率超过阈值,开启断路器;在断路器半开时,如果本次请求没有发生异常,则关闭断路器,反之恢复开启;在断路器开启时,会判断当前时间是否已经超过了熔断时间窗口,如果超过了,变为半开;

2)异常数:与异常率类似,只不过阈值是异常数;

3)慢调用比率:慢调用比率=超出最大RT的请求次数 / 总请求次数。与异常率逻辑类似,只不过阈值是慢调用比率,从半开变为关闭的条件是请求RT小于最大RT阈值;