除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用其它模块,可能是一个远程服务、数据库、或者第三方 API 等。然而,被依赖的服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,导致请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会堆积,最终可能会耗尽业务自身的线程池,甚至服务本身变得不可用。
现在的微服务架构都是分布式的,由非常多的服务组成。不同的服务之间相互调用,形成复杂的调用链路。链路中某一环不稳定,可能会层层级联,最终导致整个链路不可用。因此需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定的调用,避免局部不稳定因素导致整体的雪崩。熔断降级通常在客户端(调用端)进行配置。
熔断可以类比成生活中的保险丝,一旦电流过载,保险丝就会断开。
Sentinel 熔断降级基于熔断器模式 (circuit breaker pattern) 实现。熔断器内部维护了一个熔断器的状态机,状态机的转换关系如下图所示:
熔断器有三种状态:
Sentinel 提供了如下三种熔断策略。
SLOW_REQUEST_RATIO
):选择慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),如果请求的时间大于该阈值则被统计为慢调用。当单位统计时长(statIntervalMs
)内请求的数量大于设置的最小请求数量,并且慢调用的比例大于阈值,则接下来的熔断时长(timeWindow
)内请求自动被熔断。经过熔断时长后,熔断器进入探测恢复状态(Half-Open 状态),如果接下来的一个请求的响应时间小于设置的慢调用 RT 则结束熔断;如果大于则再次被熔断。慢调用比例的阈值范围为 [0.0, 1.0]
,代表 0% - 100%。ERROR_RATIO
):当单位统计时长(statIntervalMs
)内请求数量大于设置的最小请求数量,并且异常比例大于阈值,则接下来的熔断时长(timeWindow
)内请求自动被熔断。经过熔断时长后,熔断器进入探测恢复状态(Half-Open 状态),如果接下来的一个请求成功完成(没有错误)则结束熔断;否则再次被熔断。异常比例的阈值范围为 [0.0, 1.0]
,代表 0% - 100%。ERROR_COUNT
):当单位统计时长(statIntervalMs
)内的异常数量超过阈值之后,则接下来的熔断时长(timeWindow
)内请求自动被熔断。经过熔断时长后,熔断器进入探测恢复状态(Half-Open 状态),如果接下来的一个请求成功完成(没有错误)则结束熔断;否则再次被熔断。字段 | 说明 | 默认值 |
---|---|---|
resource | 资源名,即规则作用的对象 | |
grade | 熔断策略,支持慢调用比例/异常比例/异常数 | 慢调用比例 |
count | 慢调用比例模式下对应慢调用RT(超过该值即为慢调用);异常比例/异常数模式下为对应的阈值 | |
timeWindow | 熔断时长,单位为秒 | |
minRequestAmount | 熔断触发的最小请求数,请求数小于该值时即使异常比例超过阈值也不会熔断(1.7.0 版本引入) | 5 |
statIntervalMs | 统计时长,单位为毫秒(1.8.0 版本引入) | 1000 |
slowRationThreshold | 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 版本引入) |
同一个资源可以同时有多个熔断降级规则。
在 Nacos 的控制台中的配置管理/配置列表中,在 public 的命名空间中创建一个如下配置:
dataId:spring-cloud-demo-consumer-sentinel-degrade
group:DEFAULT
[
{
"resource": "/hello/say",
"limitApp": "default",
"grade": 0,
"count": 200,
"timeWindow": 10,
"statIntervalMs": 1000,
"slowRatioThreshold": 0.6
}
]
如果 1 秒内,请求数量至少达到 200,并且(请求的响应时间超过 200 毫秒即为慢调用)慢调用的比例达到 60%,则进行熔断,熔断时长为 10 秒。
对应的客户端的配置文件如下:
spring:
application:
name: spring-cloud-demo-consumer
cloud:
nacos:
discovery:
server-addr: 10.211.55.11:8848,10.211.55.12:8848,10.211.55.13:8848
enabled: true
sentinel:
transport:
dashboard: 127.0.0.1:9000
eager: true
datasource:
degrade-nacos-datasource:
nacos:
server-addr: 10.211.55.11:8848,10.211.55.12:8848,10.211.55.13:8848
group-id: DEFAULT_GROUP
namespace: public
data-id: ${spring.application.name}-sentinel-degrade
data-type: json
rule-type: degrade
username: nacos
password: nacos
负责熔断降级规则的判断。
@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 {
// 由DegradeRuleManager负责加载所有的断路器
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
// 如果断路器列表为空,则直接返回
if (circuitBreakers == null || circuitBreakers.isEmpty()) {
return;
}
// 遍历断路器列表,只要有一个断路器判定请求不通过,则抛出DegradeException异常
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();
// 如果调用过程中存在BlockException异常,则直接返回
if (curEntry.getBlockError() != null) {
fireExit(context, r, count, args);
return;
}
// 由DegradeRuleManager负责加载所有的断路器
List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
// 如果断路器列表为空,则直接返回
if (circuitBreakers == null || circuitBreakers.isEmpty()) {
fireExit(context, r, count, args);
return;
}
// 如果调用过程中不存在BlockException异常
if (curEntry.getBlockError() == null) {
// 遍历断路器列表,触发每个断路器的onRequestComplete方法的回调
for (CircuitBreaker circuitBreaker : circuitBreakers) {
circuitBreaker.onRequestComplete(context);
}
}
fireExit(context, r, count, args);
}
}
接下来看下断路器如何判断请求是否通过的。
AbstractCircuitBreaker
AbstractCircuitBreaker(DegradeRule rule, EventObserverRegistry observerRegistry) {
AssertUtil.notNull(observerRegistry, "observerRegistry cannot be null");
if (!DegradeRuleManager.isValidRule(rule)) {
throw new IllegalArgumentException("Invalid DegradeRule: " + rule);
}
this.observerRegistry = observerRegistry;
this.rule = rule;
this.recoveryTimeoutMs = rule.getTimeWindow() * 1000;
}
接下来看下 tryPass 方法的处理逻辑。
@Override
public boolean tryPass(Context context) {
// 如果断路器的状态是CLOSED,则返回true,表示请求通过
if (currentState.get() == State.CLOSED) {
return true;
}
// 如果断路器的状态是OPEN
if (currentState.get() == State.OPEN) {
// 判断是否到了熔断结束时间,如果到了则尝试将断路器的状态从OPEN变为HALF-OPEN
return retryTimeoutArrived() && fromOpenToHalfOpen(context);
}
// 剩余情况,返回false,表示请求不通过
return false;
}
retryTimeoutArrived 方法
判断是否到了熔断结束时间
protected boolean retryTimeoutArrived() {
// 判断当前时间 >= 下一次的熔断结束时间
return TimeUtil.currentTimeMillis() >= nextRetryTimestamp;
}
fromOpenToHalfOpen 方法
尝试将断路器的状态从OPEN变为HALF-OPEN
protected boolean fromOpenToHalfOpen(Context context) {
// 尝试将断路器的状态从OPEN更新为HALF_OPEN
if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
// 触发所有CircuitBreakerStateChangeObserver的onStateChange方法回调
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) {
// 如果调用过程中存在BlockException异常
if (entry.getBlockError() != null) {
// 将断路器的状态从HALF_OPEN更新为OPEN
currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
// 触发所有CircuitBreakerStateChangeObserver的onStateChange方法回调
notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d);
}
}
});
return true;
}
return false;
}
接下来重点看下 AbstractCircuitBreaker 的子类对于 onRequestComplete 方法的具体实现。
ResponseTimeCircuitBreaker
关注响应时间的断路器实现
public ResponseTimeCircuitBreaker(DegradeRule rule) {
// 统计时长由熔断降级规则的statIntervalMs参数指定,默认1000,即1秒
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;
}
看下 ResponseTimeCircuitBreaker 对于 onRequestComplete 方法的具体实现。
@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();
// 如果响应时间超过了阈值(对应熔断降级规则中的count参数)
if (rt > maxAllowedRt) {
// 慢请求数指标加一
counter.slowCount.add(1);
}
// 总请求数指标加一
counter.totalCount.add(1);
handleStateChangeWhenThresholdExceeded(rt);
}
接下来看下 handleStateChangeWhenThresholdExceeded 方法的处理逻辑。
private void handleStateChangeWhenThresholdExceeded(long rt) {
// 如果断路器的状态是OPEN,则直接返回
if (currentState.get() == State.OPEN) {
return;
}
// 如果断路器的状态是HALF_OPEN
if (currentState.get() == State.HALF_OPEN) {
// 如果请求的响应时间超过了阈值
if (rt > maxAllowedRt) {
// 将断路器的状态更新为OPEN,然后更新下一次的熔断结束时间
fromHalfOpenToOpen(1.0d);
} else {
// 将断路器的状态更新为CLOSED,然后重置慢请求数、总请求数指标
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();
}
// 如果总请求数 < 熔断降级规则中的minRequestAmount参数,则直接返回
if (totalCount < minRequestAmount) {
return;
}
// 计算慢调用比例
double currentRatio = slowCount * 1.0d / totalCount;
// 如果慢调用比例 > 熔断降级队则中的slowRatioThreshold参数值(默认1)
if (currentRatio > maxSlowRequestRatio) {
// 将断路器的状态更新为OPEN,然后更新下一次的熔断结束时间
transformToOpen(currentRatio);
}
// 如果当前的慢调用比例达到了100%
if (Double.compare(currentRatio, maxSlowRequestRatio) == 0 &&
Double.compare(maxSlowRequestRatio, SLOW_REQUEST_RATIO_MAX_VALUE) == 0) {
// 将断路器的状态更新为OPEN,然后更新下一次的熔断结束时间
transformToOpen(currentRatio);
}
}
ExceptionCircuitBreaker
关注异常比例、异常数的断路器实现
public ExceptionCircuitBreaker(DegradeRule rule) {
// 统计时长由熔断降级规则的statIntervalMs参数指定,默认1000,即1秒
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;
}
看下 ExceptionCircuitBreaker 对于 onRequestComplete 方法的具体实现。
@Override
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);
}
接下来看下 handleStateChangeWhenThresholdExceeded 方法的处理逻辑。
private void handleStateChangeWhenThresholdExceeded(Throwable error) {
// 如果断路器的状态是OPEN,则直接返回
if (currentState.get() == State.OPEN) {
return;
}
// 如果断路器的状态是HALF_OPEN
if (currentState.get() == State.HALF_OPEN) {
if (error == null) {
// 将断路器的状态更新为CLOSED,然后重置慢请求数、总请求数指标
fromHalfOpenToClose();
} else {
// 将断路器的状态更新为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();
}
// 如果总请求数 < 熔断降级规则中的minRequestAmount参数,则直接返回
if (totalCount < minRequestAmount) {
return;
}
double curCount = errCount;
// 如果策略是统计异常数比例,则将异常数比例转化成错误请求数
if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) {
curCount = errCount * 1.0d / totalCount;
}
// 如果错误请求数 > 阈值
if (curCount > threshold) {
// 将断路器的状态更新为OPEN,然后更新下一次的熔断结束时间
transformToOpen(curCount);
}
}