促銷活動開始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/併發數

降級

用户體驗

返回什麼數據

隔離

防止蔓延

線程池大小

超時

及時釋放

上游>下游

降級策略選擇:

策略

適用場景

返回默認值

非核心數據

返回緩存

數據時效性不敏感

靜態數據

榜單、推薦位

功能降級

可延後處理的業務

直接失敗

核心功能,必須告知用户

系統設計的時候就要想好:哪些功能可以降級,降級後返回什麼。別等出事了才想。