跳到主要内容

熔断降级

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用其他模块,可能是远程服务、数据库或第三方 API。当依赖的服务出现不稳定时,响应时间变长,会导致调用方线程堆积,最终可能耗尽线程池,服务本身变得不可用。

为什么需要熔断降级

在微服务架构中,服务之间的调用关系错综复杂。一个服务的稳定性问题往往会引发连锁反应:

服务 A -> 服务 B -> 服务 C
|
v
数据库 D

如果数据库 D 出现问题,响应变慢:

  1. 服务 C 的线程开始堆积
  2. 服务 B 调用服务 C 超时,线程也开始堆积
  3. 服务 A 调用服务 B 超时,线程堆积
  4. 最终整个链路都不可用

这就是所谓的"雪崩效应"。熔断降级的作用就是在某个环节出现问题时,及时切断调用,防止故障蔓延。

熔断器状态

Sentinel 的熔断器包含三种状态:

Closed(关闭状态)

默认状态,请求正常通过。熔断器会持续统计请求的响应时间和异常情况。

Open(开启状态)

当熔断条件触发时,熔断器进入开启状态。此时所有请求都会被直接拒绝,不再调用实际的服务。

Half-Open(半开启状态)

经过熔断时长后,熔断器进入半开启状态。此时会放行一个请求进行探测:

  • 如果探测成功,熔断器恢复到 Closed 状态
  • 如果探测失败,熔断器重新进入 Open 状态

熔断策略

Sentinel 提供三种熔断策略:慢调用比例、异常比例、异常数。

慢调用比例

当请求的响应时间超过设定的阈值时,该请求被统计为慢调用。当单位统计时长内请求数目大于最小请求数,且慢调用比例大于阈值时,触发熔断。

DegradeRule rule = new DegradeRule("callRemoteService");
rule.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType()); // 慢调用比例
rule.setCount(500); // 慢调用临界 RT,单位 ms
rule.setSlowRatioThreshold(0.5); // 慢调用比例阈值 50%
rule.setMinRequestAmount(10); // 最小请求数
rule.setStatIntervalMs(10000); // 统计时长 10 秒
rule.setTimeWindow(30); // 熔断时长 30 秒

DegradeRuleManager.loadRules(Collections.singletonList(rule));

参数说明:

  • count:慢调用临界 RT,超过这个值的请求被计为慢调用
  • slowRatioThreshold:慢调用比例阈值,取值范围 0.0 - 1.0
  • minRequestAmount:熔断触发的最小请求数,避免请求量太少时误触发
  • statIntervalMs:统计时长,单位毫秒
  • timeWindow:熔断时长,单位秒

工作流程:

  1. 统计时长内请求数 >= minRequestAmount
  2. 计算慢调用比例 = 慢调用数 / 总请求数
  3. 如果慢调用比例 >= slowRatioThreshold,触发熔断
  4. 熔断时长结束后,进入半开启状态进行探测

异常比例

当单位统计时长内请求数目大于最小请求数,且异常比例大于阈值时,触发熔断。

DegradeRule rule = new DegradeRule("callRemoteService");
rule.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType()); // 异常比例
rule.setCount(0.3); // 异常比例阈值 30%
rule.setMinRequestAmount(10); // 最小请求数
rule.setStatIntervalMs(10000); // 统计时长 10 秒
rule.setTimeWindow(30); // 熔断时长 30 秒

DegradeRuleManager.loadRules(Collections.singletonList(rule));

注意:异常降级仅针对业务异常,对 Sentinel 限流降级本身的异常(BlockException)不生效。要统计业务异常,需要通过 Tracer.trace(ex) 记录:

Entry entry = null;
try {
entry = SphU.entry("callRemoteService");
// 业务逻辑,可能抛出业务异常
} catch (BlockException e) {
// 被限流或熔断
} catch (Throwable t) {
// 记录业务异常,用于异常比例统计
Tracer.trace(t);
} finally {
if (entry != null) {
entry.exit();
}
}

使用 @SentinelResource 注解时,会自动统计业务异常:

@SentinelResource(value = "callRemoteService", fallback = "handleFallback")
public String callRemoteService() {
// 业务异常会自动被统计
if (Math.random() > 0.7) {
throw new RuntimeException("服务异常");
}
return "success";
}

public String handleFallback(Throwable t) {
return "服务降级: " + t.getMessage();
}

异常数

当单位统计时长内的异常数目超过阈值时,触发熔断。

DegradeRule rule = new DegradeRule("callRemoteService");
rule.setGrade(CircuitBreakerStrategy.ERROR_COUNT.getType()); // 异常数
rule.setCount(5); // 异常数阈值
rule.setMinRequestAmount(10); // 最小请求数
rule.setStatIntervalMs(10000); // 统计时长 10 秒
rule.setTimeWindow(30); // 熔断时长 30 秒

DegradeRuleManager.loadRules(Collections.singletonList(rule));

适用场景:

  • 对错误容忍度较低的关键服务
  • 需要快速响应故障的场景

熔断规则属性

DegradeRule 包含以下核心属性:

属性说明默认值
resource资源名,规则的作用对象必填
grade熔断策略慢调用比例
count阈值(慢调用 RT / 异常比例 / 异常数)-
timeWindow熔断时长,单位秒-
minRequestAmount熔断触发的最小请求数5
statIntervalMs统计时长,单位毫秒1000
slowRatioThreshold慢调用比例阈值-

熔断器事件监听

可以注册自定义的事件监听器来监听熔断器状态变化:

import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreaker;
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.EventObserverRegistry;

// 注册监听器
EventObserverRegistry.getInstance().addStateChangeObserver("logging",
(prevState, newState, rule, snapshotValue) -> {
if (newState == CircuitBreaker.State.OPEN) {
System.err.println(String.format("熔断器打开: %s -> OPEN, 触发值=%.2f, 规则=%s",
prevState.name(), snapshotValue, rule.getResource()));
} else {
System.err.println(String.format("熔断器状态变化: %s -> %s",
prevState.name(), newState.name()));
}
});

监听器参数说明:

  • prevState:之前的状态
  • newState:新的状态
  • rule:触发状态变化的规则
  • snapshotValue:触发时的快照值(如异常比例、慢调用比例等)

代码示例

完整的熔断降级示例

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreaker;
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreakerStrategy;
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.EventObserverRegistry;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class CircuitBreakerDemo {

public static void main(String[] args) throws InterruptedException {
initDegradeRules();
registerStateObserver();

for (int i = 0; i < 100; i++) {
Entry entry = null;
try {
entry = SphU.entry("callRemoteService");

int sleepTime = new Random().nextInt(1000);
TimeUnit.MILLISECONDS.sleep(sleepTime);

if (Math.random() > 0.7) {
throw new RuntimeException("服务异常");
}

System.out.println("请求成功,耗时: " + sleepTime + "ms");

} catch (BlockException e) {
System.out.println("请求被熔断: " + e.getClass().getSimpleName());
} catch (Throwable t) {
Tracer.trace(t);
System.out.println("业务异常: " + t.getMessage());
} finally {
if (entry != null) {
entry.exit();
}
}

TimeUnit.MILLISECONDS.sleep(100);
}
}

private static void initDegradeRules() {
List<DegradeRule> rules = new ArrayList<>();

DegradeRule rule = new DegradeRule("callRemoteService");
rule.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType());
rule.setCount(500);
rule.setSlowRatioThreshold(0.5);
rule.setMinRequestAmount(10);
rule.setStatIntervalMs(10000);
rule.setTimeWindow(10);

rules.add(rule);
DegradeRuleManager.loadRules(rules);
}

private static void registerStateObserver() {
EventObserverRegistry.getInstance().addStateChangeObserver("logging",
(prevState, newState, rule, snapshotValue) -> {
if (newState == CircuitBreaker.State.OPEN) {
System.err.println(String.format("熔断器打开: %s -> OPEN, 触发值=%.2f",
prevState.name(), snapshotValue));
} else {
System.err.println(String.format("熔断器状态变化: %s -> %s",
prevState.name(), newState.name()));
}
});
}
}

使用注解的熔断示例

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

@SentinelResource(value = "createOrder",
blockHandler = "createOrderBlockHandler",
fallback = "createOrderFallback")
public Order createOrder(OrderRequest request) {
// 调用远程服务
return remoteOrderService.create(request);
}

// 被限流或熔断时的处理
public Order createOrderBlockHandler(OrderRequest request, BlockException e) {
System.out.println("订单创建被限流或熔断: " + e.getClass().getSimpleName());
return Order.builder()
.status("FAILED")
.message("系统繁忙,请稍后重试")
.build();
}

// 业务异常时的降级处理
public Order createOrderFallback(OrderRequest request, Throwable t) {
System.out.println("订单创建异常: " + t.getMessage());
return Order.builder()
.status("FAILED")
.message("服务暂时不可用")
.build();
}
}

熔断与限流的区别

特性限流熔断
目的控制流量,保护系统处理不稳定依赖,防止故障蔓延
触发条件QPS 或线程数超过阈值响应时间或异常率超过阈值
恢复机制阈值降低后自动恢复需要经过探测恢复(半开启状态)
适用场景保护自身系统保护对下游的调用
粒度可以针对所有请求针对特定资源或依赖

实际应用中的关系

限流和熔断通常配合使用:

  • 限流是第一道防线,控制进入系统的请求量,防止系统过载
  • 熔断是第二道防线,当依赖出现问题时,快速失败,防止故障扩散

例如,一个订单服务调用支付服务:

  • 对订单服务整体设置 QPS 限流,防止过多请求进入
  • 对支付服务的调用设置熔断,当支付服务不稳定时自动熔断

最佳实践

1. 合理设置熔断阈值

慢调用阈值设置

  • 基于正常情况下的响应时间分布
  • 建议设置为 P99 响应时间的 2-3 倍
  • 例如:P99 响应时间为 200ms,则设置为 400-600ms

异常比例阈值设置

  • 根据业务容错能力设置
  • 核心服务建议设置较低(如 30%)
  • 非核心服务可以设置稍高(如 50%)

2. 熔断时长设置

熔断时长的设置需要考虑下游服务的恢复时间:

服务类型建议熔断时长原因
数据库服务10-30 秒可能快速恢复
下游微服务20-40 秒需要重启或扩容时间
外部 API30-60 秒恢复时间不确定

注意:熔断时长不宜过短,否则可能导致频繁熔断和恢复;也不宜过长,否则影响服务可用性。

3. 设置最小请求数

最小请求数可以避免请求量太少时误触发熔断:

// 建议:根据业务 QPS 设置
// 如果 QPS 较低,设置为 5
// 如果 QPS 较高,设置为 10-20
rule.setMinRequestAmount(10);

4. 提供优雅的降级处理

降级处理应该:

  • 返回有意义的响应:告知用户当前情况
  • 记录日志:便于问题排查
  • 提供替代方案:如缓存数据、默认值
  • 区分错误类型:限流和熔断应该有不同的响应
// 好的降级处理示例
public Order createOrderFallback(OrderRequest request, Throwable t) {
// 记录日志
log.warn("订单创建降级, userId={}, error={}", request.getUserId(), t.getMessage());

// 返回有意义的响应
return Order.builder()
.status("PENDING") // 标记为待处理
.message("系统繁忙,订单已提交后台处理")
.retryAfter(60) // 建议重试时间
.build();
}

下一步