Sentinel源码分析(五) - 熔断降级

2023-05-16

@Author:zxw
@Email:502513206@qq.com


目录

  1. Sentinel源码分析(一) - 初识Sentinel
  2. Sentinel源码分析(二) - Entry构建
  3. Sentinel源码分析(三) - 调用链路
  4. Sentinel源码分析(四) - 限流规则​

1.前言

在通过了流控插槽后,接下来则是断路器的插槽了。同样的,通过Sentinel的控制台看下断路器的配置有哪些
在这里插入图片描述

Sentinel 提供以下几种熔断策略:

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

对应的测试代码实现

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);
    }

首先对DegradeRule类的元数据进行分析,通过之前的FlowRule可以知道Sentinel的Rule类都继承了AbstractRule父类有两个共用的字段

private String resource;
private String limitApp;

通过grade字段标识使用的策略,这点和FlowRule是一样的

// RuleConstant
public static final int DEGRADE_GRADE_RT = 0;
public static final int DEGRADE_GRADE_EXCEPTION_RATIO = 1;
public static final int DEGRADE_GRADE_EXCEPTION_COUNT = 2;

// DegradeRule
private int grade = RuleConstant.DEGRADE_GRADE_RT;

对于慢比例调用,那么就需要我们设置一个响应时间的阈值,当响应时间大于我们期望的阈值后那就是慢比例调用。当然在熔断策略为异常比例和异常数时,该字段表示的就是最大限制数量

// 临界RT值
// 异常数量阈值
private double count;

对于服务熔断后不可能一直处于熔断状态,所以当达到一定的时间后,熔断会取消恢复成正常状态,所以也需要存储熔断时长

private int timeWindow;

当我们一个请求超过预计响应时长时,有可能只是该请求出了问题而别的请求并未产生问题,所以只有当一定比例的请求都发生问题时,才会产生熔断。Sentinel中可以配置比例阈值来判断当请求超时数达到多少时才会进行熔断。不过该值仅在慢比例策略下生效

private double slowRatioThreshold = 1.0d;

虽然可以通过比例阈值去控制熔断,但是Sentinel还提供了另一种配置就是最小请求数,该值是优于比例阈值的,也就是说即便当前已经达到比例阈值了,但是还没有达到最小请求数,那么请求也是不会熔断的。

private int minRequestAmount = RuleConstant.DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT;

Sentinel对断路器有3种状态表示,分别为关闭、打开、半开,对于半开状态。断路器将允许“探测”调用。如果按策略调用异常(如慢),断路器会重新转换到OPEN状态,等待下一个恢复时间点;否则资源将被视为“已恢复”,断路器将停止切断请求并转换为 CLOSED 状态。

enum State {
        OPEN,
        HALF_OPEN,
        CLOSED
    }

2.源码分析

在测试代码中,我们配置了DegradeRule熔断规则对象,但是Sentinel最终使用的并不是该对象,而是在配置时通过监听器RulePropertyListener将其构建为了CircuitBreaker对象。这里会通过grade配置的策略生成不同的CircuitBreaker,可以看到只有慢比例调用和异常数会生成CircuitBreaker对象

// DegradeRuleManager
private static CircuitBreaker newCircuitBreakerFrom(/*@Valid*/ 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;
        }
  }

先看下类图的接口,通过上面的分析知道只有两个实现类
在这里插入图片描述

2.1 AbstractCircuitBreaker

在Sentinel的DegradeSlot熔断插槽中,具体使用的对象就是CircuitBreaker而不是DegradeRule了,跟流控规则一样,拿到所有的熔断规则然后遍历判断是否满足需求

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());
            }
        }
    }

在熔断器中有一个开关的存在,如果开关打开那么就代表断路器打开,如果关闭则表示不需要熔断

public boolean tryPass(Context context) {
        // Template implementation.
        if (currentState.get() == State.CLOSED) {
            return true;
        }
        if (currentState.get() == State.OPEN) {
            // For half-open state we allow a request for probing.
            return retryTimeoutArrived() && fromOpenToHalfOpen(context);
        }
        return false;
    }

那么断路器会在什么时候进行打开呢,那肯定是链路结束后才能,因为这样才能统计到请求的耗时和响应时间等。之前分析过链路ProcessorSlot提供了exit方法,该方法会在entry方法执行完毕后,开始调用exit方法,就这里进行熔断器打开操作。
先来看看CircuitBreaker接口提供的方法,通过方法名可以看到onRequestComplete为请求完成后的回调方法,那么这个方法正好时候链路退出exit时进行断路器的回调来打开开关

public interface CircuitBreaker {
    boolean tryPass(Context context);
    State currentState();
    void onRequestComplete(Context context);
}

要进行回调首先得拿到我们所有的断路器,然后遍历回调方法

if (curEntry.getBlockError() == null) {
            // passed request
            for (CircuitBreaker circuitBreaker : circuitBreakers) {
                circuitBreaker.onRequestComplete(context);
            }
}

这里的回调方法是根据具体的子类来实现的,现在就来看看不同策略下的断路器是如何进行熔断的

2.2 ResponseTimeCircuitBreaker

在判断是否开打断路器前首先需要判断该断路器的状态,如果已经是打开状态直接跳过即可

if (currentState.get() == State.OPEN) {
            return;
}

但是断路器也有可能处于半开状态,半开状态是有可能让断路器恢复到关闭状态的,也就是说当前如果处于半路器的状态并且当前请求的响应时间已经恢复了正常,那么断路器就应该恢复关闭

if (currentState.get() == State.HALF_OPEN) {
    		// rt:响应时间
    		// maxAllowedRt:阈值时间
            if (rt > maxAllowedRt) {
                fromHalfOpenToOpen(1.0d);
            } else {
                fromHalfOpenToClose();
            }
            return;
        }

该断路器是根据慢比例调用来计算的,既然是慢比例那么就需要获取创建时间和结束时间来判断时间差是否大于我们配置的RT时间,如果大于则记录一次慢比例调用,对于时间范围的统计则是使用Sentinel的滑动窗口进行统计的。

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);

先前说过判断是否熔断有两点

  • 最小请求数
  • 比例阈值

最小请求时好理解,直接判断当前请求的数量是否大于最小请求数,如果不是的话,那么直接放行即可

if (totalCount < minRequestAmount) {
            return;
}

如果是比例阈值的话,那么就需要计算出(慢比例调用数 / 总请求数数) <= 比例阈值,如果不是的话,那么就需要打开我们短路器的开关了

double currentRatio = slowCount * 1.0d / totalCount;
        if (currentRatio > maxSlowRequestRatio) {
            transformToOpen(currentRatio);
        }

对于慢调用比例已经分析完毕,接下来总结一下
在这里插入图片描述

2.3 ExceptionCircuitBreaker

其实两部分的逻辑并没什么相差,都是判断断路器打开、半开的状态,唯一不同点就是这里使用的判断方式是异常的数量,如果当前的异常数量超过了设置的阈值,那么就会触发熔断开关

private void handleStateChangeWhenThresholdExceeded(Throwable error) {
        if (currentState.get() == State.OPEN) {
            return;
        }
        
        if (currentState.get() == State.HALF_OPEN) {
            // In detecting request
            if (error == null) {
                fromHalfOpenToClose();
            } else {
                fromHalfOpenToOpen(1.0d);
            }
            return;
        }
        
        List<SimpleErrorCounter> counters = stat.values();
        long errCount = 0;
        long totalCount = 0;
        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) {
            // Use errorRatio
            curCount = errCount * 1.0d / totalCount;
        }
        if (curCount > threshold) {
            transformToOpen(curCount);
        }
    }

3.状态转换

接下来就是对断路器的状态转换进行一个分析,前面已经分析当达到异常限定时,会调用transformToOpen将断路器的状态打开
在这里插入图片描述

具体实现为

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;
    }

在进入断路器具体逻辑前,会判断当前断路器的状态,如果是半开那么会进行一次判断,以下是慢调用中的逻辑

if (currentState.get() == State.HALF_OPEN) {
            // In detecting request
            // TODO: improve logic for half-open recovery
            if (rt > maxAllowedRt) {
                fromHalfOpenToOpen(1.0d);
            } else {
                fromHalfOpenToClose();
            }
            return;
        }

那么此时的状态流转为
在这里插入图片描述

那么现在还缺少一个逻辑就是OPEN变为HALF_OPEN的状态,关于这块的状态变更则是在上面的tryPass中,在来回顾一下链路调用entry方法时,会执行以下方法

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());
            }
        }
    }

public boolean tryPass(Context context) {
        // Template implementation.
        if (currentState.get() == State.CLOSED) {
            return true;
        }
        if (currentState.get() == State.OPEN) {
            // For half-open state we allow a request for probing.
            return retryTimeoutArrived() && fromOpenToHalfOpen(context);
        }
        return false;
    }

如果当前断路器已经处于打开状态了,对于是否要关闭断路器,首先当前时间肯定得大于我们设定的熔断时间

 protected boolean retryTimeoutArrived() {
        return TimeUtil.currentTimeMillis() >= nextRetryTimestamp;
    }

接着让断路器继续打开还是说进行恢复呢?所以才有半开这个中间状态,来判断是否让熔断继续,如果尝试调用恢复了正常,那么就会让断路器关闭,否则打开

protected boolean fromOpenToHalfOpen(Context context) {
        if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
            notifyObservers(State.OPEN, State.HALF_OPEN, null);
            Entry entry = context.getCurEntry();
            entry.whenTerminate(new BiConsumer<Context, Entry>() {
                @Override
                public void accept(Context context, Entry entry) {
                    // Note: This works as a temporary workaround for https://github.com/alibaba/Sentinel/issues/1638
                    // Without the hook, the circuit breaker won't recover from half-open state in some circumstances
                    // when the request is actually blocked by upcoming rules (not only degrade rules).
                    if (entry.getBlockError() != null) {
                        // Fallback to OPEN due to detecting request is blocked
                        currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
                        notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d);
                    }
                }
            });
            return true;
        }
        return false;
    }

那么状态的流转就如下图所示
在这里插入图片描述

4.总结

对于断路器的分析就到这里结束,可以发现并不算复杂,主要的断路器逻辑都在链路的exit方法中。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Sentinel源码分析(五) - 熔断降级 的相关文章

随机推荐