Sentinel中DegradeSlot的原理

DegradleSlot负责熔断降级,支持异常熔断和慢请求熔断,异常熔断又支持异常数量和异常比例两种策略。本文就来分析一下降级插槽和熔断器的工作原理。


DegradeSlot

v2.0.0
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeSlot.java
@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());
        }
    }
}

在这个插槽中,遍历了熔断器来判断是否需要降级处理。

熔断器

v2.0.0
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/circuitbreaker/AbstractCircuitBreaker.java
@Override
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 fromCloseToOpen(double snapshotValue) {
    State prev = State.CLOSED;
    if (currentState.compareAndSet(prev, State.OPEN)) {
        // 更新熔断器下一次关闭的时间
        updateNextRetryTimestamp();

        notifyObservers(prev, State.OPEN, snapshotValue);
        return true;
    }
    return false;
}
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注册exit回调,之所以要注册回调,是修复1638号issue提出的BUG。
         * 在当前请求结束后执行下面的accept回调函数
         */
        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;
}
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 boolean fromHalfOpenToClose() {
    if (currentState.compareAndSet(State.HALF_OPEN, State.CLOSED)) {
        // 重置数据统计
        resetStat();
        // 通知观察者
        notifyObservers(State.HALF_OPEN, State.CLOSED, null);
        return true;
    }
    return false;
}
protected void transformToOpen(double triggerValue) {
    State cs = currentState.get();
    switch (cs) {
        case CLOSED:
            fromCloseToOpen(triggerValue);
            break;
        case HALF_OPEN:
            fromHalfOpenToOpen(triggerValue);
            break;
        default:
            break;
    }
}

上面的状态转移方法比较多,请参考下图:

如果熔断器是开启状态,则先判断熔断器的熔断时间是否超过上限,如果是则将熔断器的状态从全开改为半开。如果改为了半开后,则先放行该请求,如果该请求还是发生了错误,则又改为全开状态。那什么时候从关闭状态到全开状态呢?这就要看具体的实现类了。

异常熔断器

异常熔断器处理两种策略,异常数量和异常比例。

v2.0.0
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/circuitbreaker/ExceptionCircuitBreaker.java
@Override
public void onRequestComplete(Context context) {
    Entry entry = context.getCurEntry();
    if (entry == null) {
        return;
    }
    Throwable error = entry.getError();
    SimpleErrorCounter counter = stat.currentWindow().value();
    // 对error进行累加
    if (error != null) {
        counter.getErrorCount().add(1);
    }
    // 对total进行累加
    counter.getTotalCount().add(1);

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

如果熔断器是半开,则根据是否发生了错误将其转为全开或关闭状态。如果熔断器是关闭状态,则判断异常数量和异常比例是否超过了阈值,如果是则修改为全开状态。另外注意如果请求数量太少,则不进行熔断。

慢请求熔断器

v2.0.0
java
sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/circuitbreaker/ResponseTimeCircuitBreaker.java
@Override
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);
}
private void handleStateChangeWhenThresholdExceeded(long rt) {
    // 如果已经是全开状态,则不做额外处理
    if (currentState.get() == State.OPEN) {
        return;
    }

    // 如果是半开状态,则判断是否是忙请求,如果是则转换到开启状态;否则转换到关闭状态
    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;
    }

    // 对滑动窗口中的数据进行求和
    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);
    }
    // 如果慢请求比率等于阈值,也转换到开启状态
    if (Double.compare(currentRatio, maxSlowRequestRatio) == 0 &&
            Double.compare(maxSlowRequestRatio, SLOW_REQUEST_RATIO_MAX_VALUE) == 0) {
        transformToOpen(currentRatio);
    }
}

如果熔断器是半开,如果响应时间也超过了阈值,则改为全开;否则改为关闭。如果熔断器是关闭状态,则判断慢请求的比例是否达到了阈值,如果是的话也改为全开。另外注意,如果请求数量太少,则不进行熔断。

总结

总之,熔断器经历过熔断时长以后,会先改为半开状态,如果改为半开后的第一个请求通过,则关闭熔断器,否则重新改为全开状态。