熔断降级
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用其他模块,可能是远程服务、数据库或第三方 API。当依赖的服务出现不稳定时,响应时间变长,会导致调用方线程堆积,最终可能耗尽线程池,服务本身变得不可用。
为什么需要熔断降级
在微服务架构中,服务之间的调用关系错综复杂。一个服务的稳定性问题往往会引发连锁反应:
服务 A -> 服务 B -> 服务 C
|
v
数据库 D
如果数据库 D 出现问题,响应变慢:
- 服务 C 的线程开始堆积
- 服务 B 调用服务 C 超时,线程也开始堆积
- 服务 A 调用服务 B 超时,线程堆积
- 最终整个链路都不可用
这就是所谓的"雪崩效应"。熔断降级的作用就是在某个环节出现问题时,及时切断调用,防止故障蔓延。
熔断器状态
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.0minRequestAmount:熔断触发的最小请求数,避免请求量太少时误触发statIntervalMs:统计时长,单位毫秒timeWindow:熔断时长,单位秒
工作流程:
- 统计时长内请求数 >= minRequestAmount
- 计算慢调用比例 = 慢调用数 / 总请求数
- 如果慢调用比例 >= slowRatioThreshold,触发熔断
- 熔断时长结束后,进入半开启状态进行探测
异常比例
当单位统计时长内请求数目大于最小请求数,且异常比例大于阈值时,触发熔断。
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 或线程数超过阈值 | 响应时间或异常率超过阈值 |
| 恢复机制 | 阈值降低后自动恢复 | 需要经过探测恢复 |
| 适用场景 | 保护自身系统 | 保护对下游的调用 |
最佳实践
1. 合理设置熔断阈值
- 慢调用阈值应该基于正常情况下的响应时间设置,一般设置为 P99 响应时间的 2-3 倍
- 异常比例阈值需要考虑业务的容错能力
- 熔断时长不宜过短,给下游服务恢复时间
2. 设置最小请求数
- 避免请求量太少时误触发熔断
- 根据业务 QPS 合理设置,一般建议 5-10
3. 提供优雅的降级处理
- 返回默认值或缓存数据
- 提供友好的错误提示
- 记录日志便于排查问题
4. 监控熔断状态
- 注册状态变化监听器
- 设置告警,及时发现异常
- 结合日志分析问题原因