服务熔断与降级实战——Sentinel 规则配置、热点限流、系统保护全攻略
服务熔断与降级实战——Sentinel 规则配置、热点限流、系统保护全攻略
适读人群:需要在微服务中实现流量控制和熔断降级的 Java 后端开发者 | 阅读时长:约18分钟 | 核心价值:掌握 Sentinel 全套规则配置,建立完整的服务自保护体系
大促前的那次压测
三年前,我们要做一次双十二大促活动,预估峰值流量是平时的 15 倍。活动前一周,进行了全链路压测。
压测结果很难看:在 3000 并发时,大部分服务还好,但其中一个商品详情服务在 1200 并发时就开始崩溃,然后因为级联失败,调用商品详情的订单服务、购物车服务也相继垮了,最终整个链路全部不可用。
更诡异的是,我们以为只是商品详情服务扛不住,给它加了机器,但加完机器后,1200 并发时还是会崩。
排查了半天发现:商品详情服务有一个接口需要调用 ES,ES 在高并发下响应变慢(从 10ms 到 500ms),商品详情服务的 HTTP 连接池等待超时,把整个服务的线程池打满了。加机器没用,因为瓶颈在 ES 这一层。
解决方案:给商品详情接口加上 Sentinel 的热点限流和熔断降级,ES 响应慢时自动降级返回缓存数据,不让慢请求把线程池打满。
这篇文章把 Sentinel 的完整配置思路都讲出来。
Sentinel 核心概念
Sentinel 是阿里巴巴开源的流量治理组件,围绕资源的实时统计和流量控制规则工作:
- 资源(Resource):要保护的代码块(接口、方法、外部调用)
- 规则(Rule):针对资源的控制规则(限流、熔断、降级、热点、系统)
- 槽(Slot):规则处理链,依次经过统计、校验等槽
快速接入
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- Sentinel Dashboard 持久化:Nacos 数据源 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>spring:
cloud:
sentinel:
transport:
dashboard: sentinel-dashboard-host:8080 # Sentinel Dashboard 地址
port: 8719
# 配置 Nacos 作为规则持久化(防止 Dashboard 重启后规则丢失)
datasource:
flow-rule:
nacos:
server-addr: nacos-host:8848
data-id: ${spring.application.name}-flow-rules
group-id: SENTINEL_GROUP
rule-type: flow
degrade-rule:
nacos:
server-addr: nacos-host:8848
data-id: ${spring.application.name}-degrade-rules
group-id: SENTINEL_GROUP
rule-type: degrade流量控制规则
@RestController
@RequestMapping("/api/product")
public class ProductController {
@Autowired
private ProductService productService;
/**
* 使用 @SentinelResource 注解保护接口
* value:资源名称
* blockHandler:被 Sentinel 规则拦截时的处理方法
* fallback:业务异常时的降级方法
*/
@GetMapping("/{id}")
@SentinelResource(
value = "getProductDetail",
blockHandler = "productBlockHandler",
fallback = "productFallback"
)
public ProductDetail getProductDetail(@PathVariable String id) {
return productService.getDetail(id);
}
/**
* 被限流/熔断时调用(注意参数要和原方法一致,最后加 BlockException)
*/
public ProductDetail productBlockHandler(String id, BlockException ex) {
if (ex instanceof FlowException) {
log.warn("商品详情接口被限流,id={}", id);
} else if (ex instanceof DegradeException) {
log.warn("商品详情接口被熔断,id={}", id);
}
// 返回缓存数据或兜底数据
return productService.getFromCache(id)
.orElse(ProductDetail.placeholder());
}
/**
* 业务异常降级(抛出 Throwable 时调用)
*/
public ProductDetail productFallback(String id, Throwable ex) {
log.error("商品详情接口异常,id={}", id, ex);
return ProductDetail.placeholder();
}
}编程式配置流控规则
@Component
public class SentinelRuleConfig implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
initFlowRules();
initDegradeRules();
}
private void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
// 商品详情接口:QPS 限流,最大 500 QPS
FlowRule productDetailRule = new FlowRule();
productDetailRule.setResource("getProductDetail");
productDetailRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
productDetailRule.setCount(500);
// 流控效果:快速失败(直接返回 BlockException)
// 也可设为 CONTROL_BEHAVIOR_WARM_UP(预热)或 CONTROL_BEHAVIOR_RATE_LIMITER(排队等待)
productDetailRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
rules.add(productDetailRule);
// 下单接口:线程数限流,最大 200 并发线程
// 比 QPS 限流更保险:即使线程都被慢请求占住了也能保护
FlowRule orderRule = new FlowRule();
orderRule.setResource("createOrder");
orderRule.setGrade(RuleConstant.FLOW_GRADE_THREAD);
orderRule.setCount(200);
rules.add(orderRule);
FlowRuleManager.loadRules(rules);
}
private void initDegradeRules() {
List<DegradeRule> rules = new ArrayList<>();
// 异常比例熔断:30秒内异常比例超过50%,熔断10秒
DegradeRule exceptionRatioRule = new DegradeRule();
exceptionRatioRule.setResource("getProductDetail");
exceptionRatioRule.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType());
exceptionRatioRule.setCount(0.5); // 50% 异常率触发
exceptionRatioRule.setMinRequestAmount(10); // 最少10次请求才统计
exceptionRatioRule.setStatIntervalMs(30000); // 30秒统计窗口
exceptionRatioRule.setTimeWindow(10); // 熔断持续时间(秒)
rules.add(exceptionRatioRule);
// 慢调用比例熔断:响应时间 > 500ms 且比例 > 80%,熔断 20 秒
DegradeRule slowCallRule = new DegradeRule();
slowCallRule.setResource("getProductDetail");
slowCallRule.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType());
slowCallRule.setCount(0.8); // 80% 慢调用触发
slowCallRule.setSlowRatioThreshold(500); // 超过 500ms 算慢调用
slowCallRule.setMinRequestAmount(10);
slowCallRule.setTimeWindow(20);
rules.add(slowCallRule);
DegradeRuleManager.loadRules(rules);
}
}热点参数限流
热点限流是 Sentinel 的特色功能,可以针对请求的某个参数值进行限流,比如:
- 某个商品 ID 的请求量突然暴增(爆款商品)
- 某个用户频繁刷接口
@GetMapping("/{id}")
@SentinelResource(
value = "getProductDetail",
blockHandler = "productBlockHandler"
)
public ProductDetail getProductDetail(@PathVariable @RequestParam String id) {
// 第一个参数(index=0,即 id)会被作为热点参数统计
return productService.getDetail(id);
}// 编程式配置热点规则
@PostConstruct
private void initHotspotRules() {
ParamFlowRule rule = new ParamFlowRule("getProductDetail")
.setParamIdx(0) // 对第一个参数(商品 ID)限流
.setCount(100) // 默认:每个商品 ID 最多 100 QPS
.setDurationInSec(1);
// 为特定商品 ID 设置不同的阈值
// 比如:爆款商品 sku-12345 最多允许 500 QPS,其他商品 100 QPS
ParamFlowItem exceptionItem = new ParamFlowItem()
.setObject("sku-12345") // 特定参数值
.setClassType(String.class.getName())
.setCount(500); // 单独的阈值
rule.setParamFlowItemList(Collections.singletonList(exceptionItem));
ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
}实测场景:双十二时,爆款商品的 QPS 可能是普通商品的 100 倍,用热点限流把爆款单独设置更高阈值,保证爆款商品的可用性,同时保护后端不被普通商品的"僵尸刷量"打垮。
系统保护规则
系统保护是从整体视角对服务进行保护,当系统负载达到上限时,自动拒绝入口流量:
private void initSystemRules() {
List<SystemRule> rules = new ArrayList<>();
SystemRule rule = new SystemRule();
// CPU 使用率超过 80% 时,触发系统保护
rule.setHighestCpuUsage(0.8);
// 系统平均响应时间超过 200ms,触发系统保护
rule.setAvgRt(200);
// 并发线程数不超过 1000
rule.setMaxThread(1000);
// 入口 QPS 不超过 5000
rule.setQps(5000.0);
rules.add(rule);
SystemRuleManager.loadRules(rules);
}三大踩坑实录
坑一:blockHandler 方法签名不对导致方法失效
现象: 配置了 @SentinelResource(blockHandler = "myBlockHandler"),但触发限流时没有走到 blockHandler,而是直接抛出了 BlockException,导致调用方收到了 500 错误。
原因: blockHandler 方法的签名有严格要求:
- 方法必须是
public的 - 返回值类型必须和原方法一致
- 参数列表必须和原方法一致,并在最后加上
BlockException类型的参数 - 如果是其他类中的方法,还需要指定
blockHandlerClass
// 错误:缺少 BlockException 参数
public ProductDetail myBlockHandler(String id) { ... }
// 正确:最后加上 BlockException
public ProductDetail myBlockHandler(String id, BlockException ex) { ... }坑二:规则在 Dashboard 配置了但重启后丢失
现象: 在 Sentinel Dashboard 配置了一套完整的规则,重启应用后,所有规则消失了,每次上线后都要重新配置,非常痛苦。
原因: Sentinel 默认的规则存储是在内存中,应用重启后丢失。Sentinel Dashboard 只是一个管理界面,规则推送到应用内存,没有持久化。
解法: 集成 Nacos 或 Apollo 作为规则数据源(如文章开头的配置),规则持久化到 Nacos,应用启动时从 Nacos 加载规则,Dashboard 修改规则时同步写入 Nacos。
坑三:熔断后恢复期内请求仍然大量失败
现象: 熔断触发后,经过 timeWindow 时间,熔断结束,但接下来的请求又立刻触发了熔断,系统始终无法恢复。
原因: Sentinel 的熔断恢复是"半开"状态:timeWindow 结束后,允许一个探测请求,如果探测请求成功才关闭熔断;如果失败,重新开始计时。但如果后端服务本身还没有恢复,探测请求也会失败,系统就一直处于熔断状态。
解法: 需要配合后端服务的健康检查,确保后端真正恢复后才让熔断关闭。同时调整 timeWindow 时间,给后端足够的恢复时间(比如后端重启需要 30 秒,timeWindow 就设为 60 秒)。
写在最后
回到开头的故事,双十二活动最终顺利完成——商品详情服务用了热点限流 + 慢调用熔断,ES 响应变慢时自动降级走缓存,整个活动期间没有出现级联故障。
Sentinel 的价值不只是防止"系统崩溃",更重要的是提供了一种主动的流量治理思路:在问题发生之前,就定义好边界和降级策略,让系统在压力下优雅降级而不是暴力崩溃。
