服务降级与熔断机制实战:让系统优雅地挂

促销活动开始10分钟,商品服务挂了。

然后呢?订单服务调商品服务超时,线程池打满。用户服务调订单服务超时,线程池也打满。整个系统像多米诺骨牌一样全倒了。

这就是经典的雪崩效应。

解决方案:熔断和降级。

雪崩是怎么发生的

复制代码
用户请求
│
▼
┌─────────┐    调用    ┌─────────┐    调用    ┌─────────┐
│ 用户服务 │ ────────▶ │ 订单服务 │ ────────▶ │ 商品服务 │ ← 挂了
└─────────┘           └─────────┘           └─────────┘
│
▼
线程等待超时
│
▼
线程池满了
│
▼
订单服务也挂了
│
▼
用户服务也挂了

一个服务挂,全链路崩。

熔断器原理

熔断器有三种状态:

sql 复制代码
┌─────────────────────────────────────┐
│                                     │
▼                                     │
┌───────┐  失败率超阈值  ┌───────┐  冷却后  ┌───────────┐
│ 关闭  │ ────────────▶ │ 打开  │ ───────▶ │ 半开      │
│ CLOSED│               │ OPEN  │          │ HALF-OPEN │
└───────┘               └───────┘          └───────────┘
▲                                          │
│            成功率恢复                     │
└──────────────────────────────────────────┘
  • CLOSED:正常状态,所有请求通过
  • OPEN:熔断状态,请求直接失败,不调下游
  • HALF_OPEN:试探状态,放一部分请求过去试试

Sentinel实战

阿里开源的Sentinel,生产环境用得最多。

基本配置

xml 复制代码
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.6</version>
</dependency>
java 复制代码
// 定义资源
@SentinelResource(value = "getProduct", 
    blockHandler = "getProductBlockHandler",
    fallback = "getProductFallback")
public Product getProduct(Long productId) {
    return productService.getById(productId);
}

// 熔断/限流时的处理
public Product getProductBlockHandler(Long productId, BlockException e) {
    log.warn("getProduct被熔断: {}", productId);
    return Product.defaultProduct();  // 返回默认商品
}

// 异常时的降级
public Product getProductFallback(Long productId, Throwable t) {
    log.error("getProduct异常降级: {}", productId, t);
    return Product.defaultProduct();
}

熔断规则

java 复制代码
// 配置熔断规则
DegradeRule rule = new DegradeRule();
rule.setResource("getProduct");
rule.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType());  // 按错误率熔断
rule.setCount(0.5);  // 错误率50%
rule.setMinRequestAmount(20);  // 最小请求数
rule.setTimeWindow(10);  // 熔断时长10秒
rule.setStatIntervalMs(10000);  // 统计时间窗口

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

参数解释:

  • 10秒内请求超过20次,且错误率超过50%,触发熔断
  • 熔断10秒后进入半开状态

限流规则

java 复制代码
FlowRule rule = new FlowRule();
rule.setResource("getProduct");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);  // 按QPS限流
rule.setCount(100);  // 每秒100次

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

Resilience4j实战

Spring Cloud官方推荐,比Hystrix轻量。

熔断配置

yaml 复制代码
resilience4j:
  circuitbreaker:
    instances:
      productService:
        sliding-window-type: COUNT_BASED
        sliding-window-size: 10
        minimum-number-of-calls: 5
        failure-rate-threshold: 50
        wait-duration-in-open-state: 10s
        permitted-number-of-calls-in-half-open-state: 3

参数解释:

  • 基于最近10次调用统计
  • 至少5次调用才开始计算
  • 失败率超过50%触发熔断
  • 熔断10秒后半开
  • 半开状态放3个请求试探

代码使用

java 复制代码
@CircuitBreaker(name = "productService", fallbackMethod = "getProductFallback")
public Product getProduct(Long productId) {
    return restTemplate.getForObject(
        "http://product-service/products/" + productId, 
        Product.class
    );
}

public Product getProductFallback(Long productId, Exception e) {
    log.warn("商品服务熔断,返回默认值: {}", productId);
    return Product.defaultProduct();
}

组合使用

java 复制代码
@CircuitBreaker(name = "productService", fallbackMethod = "fallback")
@RateLimiter(name = "productService")
@Retry(name = "productService")
@Bulkhead(name = "productService")
public Product getProduct(Long productId) {
    return productService.getById(productId);
}

执行顺序:Retry → CircuitBreaker → RateLimiter → Bulkhead → 实际调用

降级策略

策略一:返回默认值

java 复制代码
public Product getProductFallback(Long productId, Exception e) {
    // 返回一个空商品,让页面能展示
    return Product.builder()
        .id(productId)
        .name("商品加载中...")
        .price(BigDecimal.ZERO)
        .stock(-1)  // -1表示库存未知
        .build();
}

策略二:返回缓存数据

java 复制代码
public Product getProductFallback(Long productId, Exception e) {
    // 从本地缓存取
    Product cached = localCache.get("product:" + productId);
    if (cached != null) {
        cached.setFromCache(true);  // 标记来自缓存
        return cached;
    }
    
    // 缓存也没有,返回默认值
    return Product.defaultProduct();
}

策略三:静态数据兜底

java 复制代码
public List<Product> getHotProductsFallback(Exception e) {
    // 返回预先准备好的静态热门商品
    return staticHotProducts;
}

适合首页推荐、热门榜单这类场景。

策略四:功能降级

java 复制代码
public OrderResult createOrder(Order order) {
    // 正常流程:实时校验库存
    // 降级流程:异步校验,先让订单创建成功
    
    if (isProductServiceDown()) {
        // 商品服务挂了,跳过库存校验
        order.setStockCheckSkipped(true);
        // 发消息异步补偿
        mqTemplate.send("stock-check-later", order);
    }
    
    return orderService.create(order);
}

线程池隔离

另一种防雪崩的方式:线程池隔离。

java 复制代码
@HystrixCommand(
    commandKey = "getProduct",
    threadPoolKey = "productPool",
    threadPoolProperties = {
        @HystrixProperty(name = "coreSize", value = "10"),
        @HystrixProperty(name = "maxQueueSize", value = "20")
    }
)
public Product getProduct(Long productId) {
    return productService.getById(productId);
}

每个服务用独立线程池,一个服务慢不影响其他。

Resilience4j用Bulkhead实现:

yaml 复制代码
resilience4j:
  bulkhead:
    instances:
      productService:
        maxConcurrentCalls: 10  # 最大并发数
        maxWaitDuration: 100ms  # 等待时间

超时配置

超时配置很关键,配错了熔断器不生效。

调用链超时

scss 复制代码
用户 → 网关(10s) → 用户服务(8s) → 订单服务(5s) → 商品服务(3s)

原则:上游超时 > 下游超时

常见配置

yaml 复制代码
# Feign客户端
feign:
  client:
    config:
      default:
        connectTimeout: 2000
        readTimeout: 5000

# RestTemplate
@Bean
public RestTemplate restTemplate() {
    HttpComponentsClientHttpRequestFactory factory = 
        new HttpComponentsClientHttpRequestFactory();
    factory.setConnectTimeout(2000);
    factory.setReadTimeout(5000);
    return new RestTemplate(factory);
}

超时 vs 熔断

markdown 复制代码
请求超时 5s,熔断冷却 10s

场景:商品服务响应变慢(6s)

1. 请求发出
2. 等待5s,超时失败
3. 触发fallback
4. 统计失败率
5. 失败率超阈值,熔断打开
6. 后续请求直接走fallback(不用等5s了)
7. 10s后半开,试探
8. 如果成功,关闭熔断

熔断的意义:快速失败,不浪费时间等超时。

监控告警

熔断了要能看到。

Sentinel Dashboard

bash 复制代码
java -jar sentinel-dashboard-1.8.6.jar --server.port=8080

# 应用接入
java -Dcsp.sentinel.dashboard.server=localhost:8080 \
     -Dproject.name=order-service \
     -jar order-service.jar

Prometheus指标

Resilience4j原生支持Prometheus:

yaml 复制代码
management:
  endpoints:
    web:
      exposure:
        include: health,prometheus,circuitbreakers
promql 复制代码
# 熔断器状态
resilience4j_circuitbreaker_state{name="productService"}

# 失败率
resilience4j_circuitbreaker_failure_rate{name="productService"}

# 调用次数
resilience4j_circuitbreaker_calls_total{name="productService"}

运维实践

我们有几个服务部署在不同城市的机房,需要统一监控熔断状态。用星空组网把各地节点连起来后,Prometheus可以直接采集所有节点的metrics,监控配置简单多了。

总结

熔断降级核心要点:

机制 作用 配置要点
熔断 快速失败 失败率阈值、冷却时间
限流 保护后端 QPS/并发数
降级 用户体验 返回什么数据
隔离 防止蔓延 线程池大小
超时 及时释放 上游>下游

降级策略选择:

策略 适用场景
返回默认值 非核心数据
返回缓存 数据时效性不敏感
静态数据 榜单、推荐位
功能降级 可延后处理的业务
直接失败 核心功能,必须告知用户

系统设计的时候就要想好:哪些功能可以降级,降级后返回什么。别等出事了才想。


熔断降级这块有实战经验的欢迎交流~

相关推荐
乘云数字DATABUFF4 天前
5分钟部署开源APM Databuff:OpenTelemetry全链路追踪入门实战
运维·后端
荣--6 天前
一键部署不是为了省时间 —— 它是把"买来的 PaaS"变成"自己的平台"的拐点
运维·zabbix·工程化·一键部署·平台化·边界设计
江华森6 天前
动手实战学 Docker — 从零到集群编排完全指南
运维
Avan_菜菜6 天前
FRP 内网穿透完整实战:从 HTTP 映射到 HTTPS 自签代理
运维·nginx·https
SelectDB7 天前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
XIAOHEZIcode9 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
用户0328472220709 天前
如何搭建本地yum源(上)
运维
大树8812 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠12 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质12 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务