【sentinel】熔断降级规则详解及源码分析
概述
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方API等。例如,支付的时候,可能需要远程调用银联提供的API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
Sentinel在1.8.0版本对熔断降级做了大的调整,可以定义任意时长的熔断时间,参照Hystrix引入了半开启恢复的支持。
熔断状态有以下三种:
状态 | 说明 |
---|---|
OPEN | 熔断开启状态,拒绝所有请求 |
HALF_OPEN | 半开状态,如果接下来的一个请求顺利通过则表示结束熔断,否则继续熔断 |
CLOSE | 熔断关闭状态,请求顺利通过 |
熔断状态参考枚举类com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreaker.State
/**
* Circuit breaker state.
*/
enum State {
/**
* In {@code OPEN} state, all requests will be rejected until the next recovery time point.
*/
OPEN,
/**
* In {@code HALF_OPEN} state, the circuit breaker will allow a "probe" invocation.
* If the invocation is abnormal according to the strategy (e.g. it's slow), the circuit breaker
* will re-transform to the {@code OPEN} state and wait for the next recovery time point;
* otherwise the resource will be regarded as "recovered" and the circuit breaker
* will cease cutting off requests and transform to {@code CLOSED} state.
*/
HALF_OPEN,
/**
* In {@code CLOSED} state, all requests are permitted. When current metric value exceeds the threshold,
* the circuit breaker will transform to {@code OPEN} state.
*/
CLOSED
}
熔断降级规则说明
熔断降级规则(DegradeRule)包含下面几个重要的属性:
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,即规则的作用对象 | |
grade | 熔断策略,支持慢调用比例/异常比例/异常数策略 | 慢调用比例 |
count | 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值 | |
timeWindow | 熔断时长,单位为s | |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) | 5 |
statIntervalMs | 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) | 1000 ms |
slowRatioThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入) |
熔断策略
慢调用比例
慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用RT(Response Time),即最大的响应时间,请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断,熔断状态变为OPEN。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用RT则结束熔断(CLOSED),若大于设置的慢调用RT则会再次被熔断(OPEN)。
举个例子:针对资源/sentinel/slowRatio
进行降级,
@RequestMapping("/slowRatio")
public String slowRatio(Integer num) throws InterruptedException {
// 模拟慢请求
TimeUnit.SECONDS.sleep(5);
return String.valueOf(10 / num);
}
降级规则配置如下:
熔断效果如下:
异常比例
异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是01,代表0%100%。
注意异常降级仅针对业务异常,对Sentinel限流降级本身的异常(BlockException)不生效。为了统计异常比例或异常数,需要通过 Tracer.trace(ex)记录业务异常。
示例:
Entry entry = null;
try {
entry = SphU.entry(resource);
// Write your biz code here.
// <<BIZ CODE>>
} catch (Throwable t) {
if (!BlockException.isBlockException(t)) {
Tracer.trace(t);
}
} finally {
if (entry != null) {
entry.exit();
}
}
如果使用了sentinel的适配器模块,如Sentinel Dubbo Adapter, Sentinel Web Servlet Filter或@SentinelResource注解会自动统计业务异常,无需手动调用。
异常数
异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
熔断器事件监听
Sentinel支持注册自定义的事件监听器监听熔断器状态变换事件(state change event)。
示例:
@PostConstruct
public void init() {
EventObserverRegistry.getInstance().addStateChangeObserver("logging",
(prevState, newState, rule, snapshotValue) -> {
// 变换至 OPEN state 时会携带触发时的值
log.error("{} -> {}, snapshotValue={}", prevState, newState, snapshotValue);
});
}
监听上面的规则会打印如下的日志,可以从日志中看到中间会熔断5s,5s过后放一个请求进来,状态变为HALF_OPEN,这一个请求耗时500ms,超过阈值,又触发了熔断。
2023-03-25 10:56:28.422 ERROR 16296 --- [io-8030-exec-16] c.m.user.controller.DegradeController : CLOSED -> OPEN, snapshotValue=1.0
2023-03-25 11:00:53.279 ERROR 16296 --- [nio-8030-exec-8] c.m.user.controller.DegradeController : OPEN -> HALF_OPEN, snapshotValue=null
2023-03-25 11:00:53.839 ERROR 16296 --- [nio-8030-exec-8] c.m.user.controller.DegradeController : HALF_OPEN -> OPEN, snapshotValue=1.0
2023-03-25 11:00:58.879 ERROR 16296 --- [nio-8030-exec-8] c.m.user.controller.DegradeController : OPEN -> HALF_OPEN, snapshotValue=null
源码分析
DegradeSlot
Sentinel的熔断是由责任链中的最后一个DegradeSlot来实现的。
@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 {
// 在触发后续slot前执行熔断的检查
performChecking(context, resourceWrapper);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
void performChecking(Context context, ResourceWrapper r) throws BlockException {
// 根据资源名称查找断路器CircuitBreaker
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
if (circuitBreakers == null || circuitBreakers.isEmpty()) {
return;
}
// 遍历所有的CircuitBreaker,判断是否让这个请求通过
for (CircuitBreaker cb : circuitBreakers) {
// tryPass里面只做了状态检查,熔断是否关闭或者打开
/**
* @see AbstractCircuitBreaker#tryPass(com.alibaba.csp.sentinel.context.Context)
*/
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();
//如果当前其他slot已经有了BlockException直接调用fireExit,不用继续走熔断逻辑了
if (curEntry.getBlockError() != null) {
fireExit(context, r, count, args);
return;
}
// 根据资源名称查找断路器CircuitBreaker
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
if (circuitBreakers == null || circuitBreakers.isEmpty()) {
fireExit(context, r, count, args);
return;
}
if (curEntry.getBlockError() == null) {
// passed request
//调用CircuitBreaker的onRequestComplete()方法
for (CircuitBreaker circuitBreaker : circuitBreakers) {
circuitBreaker.onRequestComplete(context);
}
}
fireExit(context, r, count, args);
}
}
进入DegradeSlot时,只会检查断路器是否已经打开,再根据是否超过了重试时间来开启半开状态,然后就直接返回是否通过。
而真正判断是否需要开启断路器的地方时在exit()方法里面,因为这个方法是在业务方法执行后调用的,只有在业务方法执行完成后,断路器才能收集到业务异常或者业务方法的执行时间来判断是否打开断路器。
先来看进入DegradeSlot的entry()方法,这里调用了CircuitBreaker.tryPass()方法,CircuitBreaker有ExceptionCircuitBreaker和ResponseTimeCircuitBreaker两种类型的断路器,CircuitBreaker继承关系图如下:
AbstractCircuitBreaker
entry()方法实际上调用了AbstractCircuitBreaker.tryPass()方法,这里只做了一个处理,如果断路器开启,但是上一个请求距离现在已经过了重试间隔时间就开启半启动状态。
public boolean tryPass(Context context) {
// Template implementation.
if (currentState.get() == State.CLOSED) {
// 关闭状态,返回true,让请求通过
return true;
}
if (currentState.get() == State.OPEN) {
// For half-open state we allow a request for probing.
// 如果断路器开启,但是上一个请求距离现在已经过了重试间隔时间就开启半开状态
return retryTimeoutArrived() && fromOpenToHalfOpen(context);
}
// 半开状态,直接返回false,拒绝请求
return false;
}
protected boolean retryTimeoutArrived() {
// 判断当前时间是否超过重试时间
return TimeUtil.currentTimeMillis() >= nextRetryTimestamp;
}
protected boolean fromOpenToHalfOpen(Context context) {
// OPEN -> HALF_OPEN
if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
// 通知观察者
// 可通过EventObserverRegistry.getInstance().addStateChangeObserver注册观察者监听状态的变化
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
// 如果异常了
// HALF_OPEN -> OPEN
currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d);
}
}
});
return true;
}
return false;
}
ResponseTimeCircuitBreaker
exit()方法调用了ExceptionCircuitBreaker和ResponseTimeCircuitBreaker的onRequestComplete()方法。
ResponseTimeCircuitBreaker
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) {
// 请求响应时间>最大响应时间
// 慢调用数+1
counter.slowCount.add(1);
}
// 总的请求+1
counter.totalCount.add(1);
handleStateChangeWhenThresholdExceeded(rt);
}
private void handleStateChangeWhenThresholdExceeded(long rt) {
if (currentState.get() == State.OPEN) {
// OPEN 直接返回,已经有其他请求触发熔断降级了
return;
}
if (currentState.get() == State.HALF_OPEN) {
// In detecting request
// TODO: improve logic for half-open recovery
// HALF_OPEN放了一个请求进来
if (rt > maxAllowedRt) {
// HALF_OPEN -> OPEN
fromHalfOpenToOpen(1.0d);
} else {
// HALF_OPEN -> CLOSE
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) {
// 当前慢请求比例 = 最大慢请求比例 = 1.0
transformToOpen(currentRatio);
}
}
ExceptionCircuitBreaker
com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.ExceptionCircuitBreaker#onRequestComplete
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) {
// 异常数+1
counter.getErrorCount().add(1);
}
// 总请求数+1
counter.getTotalCount().add(1);
// 熔断逻辑,处理熔断状态的变更
handleStateChangeWhenThresholdExceeded(error);
}
private void handleStateChangeWhenThresholdExceeded(Throwable error) {
if (currentState.get() == State.OPEN) {
// OPEN 直接返回,已经有其他请求触发熔断降级了
return;
}
if (currentState.get() == State.HALF_OPEN) {
// HALF_OPEN放了一个请求进来
// In detecting request
if (error == null) {
// HALF_OPEN -> CLOSE
fromHalfOpenToClose();
} else {
// HALF_OPEN -> OPEN
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) {
// CLOSE -> OPEN
transformToOpen(curCount);
}
}