促销活动开始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/并发数 |
| 降级 | 用户体验 | 返回什么数据 |
| 隔离 | 防止蔓延 | 线程池大小 |
| 超时 | 及时释放 | 上游>下游 |
降级策略选择:
| 策略 | 适用场景 |
|---|---|
| 返回默认值 | 非核心数据 |
| 返回缓存 | 数据时效性不敏感 |
| 静态数据 | 榜单、推荐位 |
| 功能降级 | 可延后处理的业务 |
| 直接失败 | 核心功能,必须告知用户 |
系统设计的时候就要想好:哪些功能可以降级,降级后返回什么。别等出事了才想。
熔断降级这块有实战经验的欢迎交流~