促銷活動開始10分鐘,商品服務掛了。
然後呢?訂單服務調商品服務超時,線程池打滿。用户服務調訂單服務超時,線程池也打滿。整個系統像多米諾骨牌一樣全倒了。
這就是經典的雪崩效應。
解決方案:熔斷和降級。
雪崩是怎麼發生的
用户請求
│
▼
┌─────────┐ 調用 ┌─────────┐ 調用 ┌─────────┐
│ 用户服務 │ ────────▶ │ 訂單服務 │ ────────▶ │ 商品服務 │ ← 掛了
└─────────┘ └─────────┘ └─────────┘
│
▼
線程等待超時
│
▼
線程池滿了
│
▼
訂單服務也掛了
│
▼
用户服務也掛了
一個服務掛,全鏈路崩。
熔斷器原理
熔斷器有三種狀態:
┌─────────────────────────────────────┐
│ │
▼ │
┌───────┐ 失敗率超閾值 ┌───────┐ 冷卻後 ┌───────────┐
│ 關閉 │ ────────────▶ │ 打開 │ ───────▶ │ 半開 │
│ CLOSED│ │ OPEN │ │ HALF-OPEN │
└───────┘ └───────┘ └───────────┘
▲ │
│ 成功率恢復 │
└──────────────────────────────────────────┘
- CLOSED:正常狀態,所有請求通過
- OPEN:熔斷狀態,請求直接失敗,不調下游
- HALF_OPEN:試探狀態,放一部分請求過去試試
Sentinel實戰
阿里開源的Sentinel,生產環境用得最多。
基本配置
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.6</version>
</dependency>
// 定義資源
@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();
}
熔斷規則
// 配置熔斷規則
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秒後進入半開狀態
限流規則
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輕量。
熔斷配置
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個請求試探
代碼使用
@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();
}
組合使用
@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 → 實際調用
降級策略
策略一:返回默認值
public Product getProductFallback(Long productId, Exception e) {
// 返回一個空商品,讓頁面能展示
return Product.builder()
.id(productId)
.name("商品加載中...")
.price(BigDecimal.ZERO)
.stock(-1) // -1表示庫存未知
.build();
}
策略二:返回緩存數據
public Product getProductFallback(Long productId, Exception e) {
// 從本地緩存取
Product cached = localCache.get("product:" + productId);
if (cached != null) {
cached.setFromCache(true); // 標記來自緩存
return cached;
}
// 緩存也沒有,返回默認值
return Product.defaultProduct();
}
策略三:靜態數據兜底
public List<Product> getHotProductsFallback(Exception e) {
// 返回預先準備好的靜態熱門商品
return staticHotProducts;
}
適合首頁推薦、熱門榜單這類場景。
策略四:功能降級
public OrderResult createOrder(Order order) {
// 正常流程:實時校驗庫存
// 降級流程:異步校驗,先讓訂單創建成功
if (isProductServiceDown()) {
// 商品服務掛了,跳過庫存校驗
order.setStockCheckSkipped(true);
// 發消息異步補償
mqTemplate.send("stock-check-later", order);
}
return orderService.create(order);
}
線程池隔離
另一種防雪崩的方式:線程池隔離。
@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實現:
resilience4j:
bulkhead:
instances:
productService:
maxConcurrentCalls: 10 # 最大併發數
maxWaitDuration: 100ms # 等待時間
超時配置
超時配置很關鍵,配錯了熔斷器不生效。
調用鏈超時
用户 → 網關(10s) → 用户服務(8s) → 訂單服務(5s) → 商品服務(3s)
原則:上游超時 > 下游超時
常見配置
# 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 熔斷
請求超時 5s,熔斷冷卻 10s
場景:商品服務響應變慢(6s)
1. 請求發出
2. 等待5s,超時失敗
3. 觸發fallback
4. 統計失敗率
5. 失敗率超閾值,熔斷打開
6. 後續請求直接走fallback(不用等5s了)
7. 10s後半開,試探
8. 如果成功,關閉熔斷
熔斷的意義:快速失敗,不浪費時間等超時。
監控告警
熔斷了要能看到。
Sentinel Dashboard
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:
management:
endpoints:
web:
exposure:
include: health,prometheus,circuitbreakers
# 熔斷器狀態
resilience4j_circuitbreaker_state{name="productService"}
# 失敗率
resilience4j_circuitbreaker_failure_rate{name="productService"}
# 調用次數
resilience4j_circuitbreaker_calls_total{name="productService"}
運維實踐
我們有幾個服務部署在不同城市的機房,需要統一監控熔斷狀態。用星空組網把各地節點連起來後,Prometheus可以直接採集所有節點的metrics,監控配置簡單多了。
總結
熔斷降級核心要點:
|
機制
|
作用
|
配置要點
|
|
熔斷
|
快速失敗
|
失敗率閾值、冷卻時間
|
|
限流
|
保護後端
|
QPS/併發數
|
|
降級
|
用户體驗
|
返回什麼數據
|
|
隔離
|
防止蔓延
|
線程池大小
|
|
超時
|
及時釋放
|
上游>下游
|
降級策略選擇:
|
策略
|
適用場景
|
|
返回默認值
|
非核心數據
|
|
返回緩存
|
數據時效性不敏感
|
|
靜態數據
|
榜單、推薦位
|
|
功能降級
|
可延後處理的業務
|
|
直接失敗
|
核心功能,必須告知用户
|
系統設計的時候就要想好:哪些功能可以降級,降級後返回什麼。別等出事了才想。