07.CircuitBreaker斷路器

长名06發表於2024-10-16

1.Hystrix進入維護模式

1.1 是什麼

Hystrix是一個用於處理分散式系統的延遲容錯的開源庫,在分散式系統裡,許多依賴不可避免的會呼叫失敗,比如超時、異常等,Hystrix能夠保證在一個依賴出問題的情況下,不會導致整體服務失敗,避免級聯故障,以提高分散式系統的彈性。

瞭解,這是Netflix專案中的元件,不會再使用了。

1.2 替代方案

Resilience4j

2.斷路器概述

2.1 分散式系統面臨的問題

分散式系統面臨的問題,複雜分散式體系結構中的應用程式有數十個依賴關係,每個依賴關係在某些時候將不可避免地失敗。

2.2 服務雪崩現象

多個微服務之間呼叫的時候,假設微服務A呼叫微服務B和微服務C,微服務B和微服務C又呼叫其它的微服務,這就是所謂的“扇出”。如果扇出的鏈路上某個微服務的呼叫響應時間過長或者不可用,對微服務A的呼叫就會佔用越來越多的系統資源,進而引起系統崩潰,所謂的“雪崩效應”。

對於高流量的應用來說,單一的後端依賴可能會導致所有伺服器上的所有資源都在幾秒鐘內飽和。比失敗更糟糕的是,這些應用程式還可能導致服務之間的延遲增加,備份佇列,執行緒和其他系統資源緊張,導致整個系統發生更多的級聯故障。這些都表示需要對故障和延遲進行隔離和管理,以便單個依賴關係的失敗,不能導致整個應用程式或系統都崩潰。

所以,通常當你發現一個模組下的某個例項失敗後,這時候這個模組依然還會接收流量,然後這個有問題的模組還呼叫了其他的模組,這樣就可能會發生級聯故障,或者叫雪崩。

2.3 微服務系統的述求

問題:禁止服務雪崩故障

解決: 有問題的節點,快速熔斷(快速返回失敗處理或者返回預設兜底資料也被稱為服務降級)。

“斷路器”本身是一種開關裝置,當某個服務單元發生故障之後,透過斷路器的故障監控(類似熔斷保險絲),向呼叫方返回一個符合預期的、可處理的備選響應(FallBack),而不是長時間的等待或者丟擲呼叫方無法處理的異常,這樣就保證了服務呼叫方的執行緒不會被長時間、不必要地佔用,從而避免了故障在分散式系統中的蔓延,乃至雪崩。

一句話,出故障了“保險絲”跳閘,別把整個家給燒了

2.4 解決方案

2.4.1 服務熔斷 + 服務降級

服務熔斷,就是當服務b發生故障時,服務a不在訪問b,也就是說不能再訪問b。

服務降級,就是給呼叫方一個預設的fallback,而不是一直在等待。比如,伺服器忙,請稍後再試。

2.4.2 服務限流

透過限流演算法,將瞬間很大的請求數量,轉換為系統承受範圍內的請求,從而保證服務不會被流量沖垮。

3.Spring Circuit Breaker

3.1 官網

https://spring.io/projects/spring-cloud-circuitbreaker

3.2 實現原理

CircuitBreaker的目的是保護分散式系統免受故障和異常,提高系統的可用性和健壯性。

當一個元件或服務出現故障時,CircuitBreaker會迅速切換到開放OPEN狀態(保險絲跳閘斷電),阻止請求傳送到該元件或服務從而避免更多的請求傳送到該元件或服務。這可以減少對該元件或服務的負載,防止該元件或服務進一步崩潰,並使整個系統能夠繼續正常執行。同時,CircuitBreaker還可以提高系統的可用性和健壯性,因為它可以在分散式系統的各個元件之間自動切換,從而避免單點故障的問題。

3.3 CircuitBreaker和Resilience4j關係

CircuitBreaker是一種思想,Resilience4j是其思想的具體實現。

4.Resilience4j

4.1 是什麼


Resilience4j是一個為函數語言程式設計設計的輕量級容錯庫。Resilience4j提供高階函式(裝飾器),以增強與斷路器、速率限制器、重試或隔板的任何功能介面、lambda表示式或方法引用。您可以在任何函式介面、lambda表示式或方法引用上堆疊多個裝飾器。優點是,您可以選擇所需的裝飾器,而無需其他任何選擇。
Resilience4j 2需要Java 17。

4.2 能幹啥


本課程,只講了這三個主要功能,分別是熔斷(Circuit Breaking),限流(limiting),艙壁(Bulkheading)。

4.3 官網

https://resilience4j.readme.io/docs/circuitbreaker

中文手冊

https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/index.md

5.案例實戰

5.1 熔斷和降級(circuitbreaker&fallback)

5.1.1 斷路器的三大狀態

5.1.2 斷路器三大狀態的轉換
  • 斷路器有三個普通狀態:關閉(CLOSED)開啟(OPEN)半開(HALF_OPEN),還有兩個特殊狀態,禁用(DISABLED)強制開啟(FORCED_OPEN)

  • 當斷路器關閉時,所有的請求都會透過斷路器,就行入戶的電都會流經關閉的保險絲。

    • 如果失敗率超過設定的閾值,熔斷器就會從關閉狀態轉換到開啟狀態,這時所有的請求都會被拒絕。
    • 當經過一段時間後,熔斷器會從開啟狀態轉換到半開狀態,這時僅有一定數量的請求會被放入,並重新計算失敗率。
    • 如果失敗率超過閾值,則變為開啟狀態,如果失敗率低於閾值,則變為關閉狀態。
  • 斷路器使用滑動視窗來儲存和統計呼叫的結果。你可以選擇基於呼叫數量的滑動視窗或者基於時間的滑動視窗。

    • 基於訪問數量的滑動視窗統計了最近N次呼叫的返回結果。居於時間的滑動視窗統計了最近N秒的呼叫返回結果。
  • 除此以外,熔斷器還會有兩種特殊狀態:DISABLED(始終允許訪問)和FORCED_OPEN(始終拒絕訪問)。

    • 這兩個狀態不會生成熔斷器事件(除狀態裝換外),並且不會記錄事件的成功或者失敗。
    • 退出這兩個狀態的唯一方法是觸發狀態轉換或者重置熔斷器。
5.1.3 斷路器配置引數參考

官網

https://resilience4j.readme.io/docs/circuitbreaker#create-and-configure-a-circuitbreaker

中文手冊

https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/CircuitBreaker.md

預設CircuitBreakerConfig.java配置類(io.github.resilience4j.circuitbreaker)

精簡版配置引數

引數 含義
failure-rate-threshold 以百分比配置失敗率峰值
sliding-window-type 斷路器的滑動視窗期型別 可以基於“次數”(COUNT_BASED)或者“時間”(TIME_BASED)進行熔斷,預設是COUNT_BASED。
sliding-window-size 若COUNT_BASED,則10次呼叫中有50%失敗(即5次)開啟熔斷斷路器;****若為TIME_BASED則,此時還有額外的兩個設定屬性,含義為:在N秒內(sliding-window-size)100%(slow-call-rate-threshold)的請求超過N秒(slow-call-duration-threshold)開啟斷路器。
slowCallRateThreshold 以百分比的方式配置,斷路器把呼叫時間大於slowCallDurationThreshold的呼叫視為慢呼叫,當慢呼叫比例大於等於峰值時,斷路器開啟,並進入服務降級。
slowCallDurationThreshold 配置呼叫時間的峰值,高於該峰值的視為慢呼叫。
permitted-number-of-calls-in-half-open-state 執行斷路器在HALF_OPEN狀態下時進行N次呼叫,如果故障或慢速呼叫仍然高於閾值,斷路器再次進入開啟狀態。
minimum-number-of-calls 在每個滑動視窗期樣本數,配置斷路器計算錯誤率或者慢呼叫率的最小呼叫數。比如設定為5意味著,在計算故障率之前,必須至少呼叫5次。如果只記錄了4次,即使4次都失敗了,斷路器也不會進入到開啟狀態。
wait-duration-in-open-state 從OPEN到HALF_OPEN狀態需要等待的時間
5.1.4 熔斷 + 降級需求說明

6次訪問中當執行方法的失敗率達到50%時CircuitBreaker將進入開啟OPEN狀態(保險絲跳閘斷電)拒絕所有請求。

等待5秒後,CircuitBreaker 將自動從開啟OPEN狀態過渡到半開HALF_OPEN狀態,允許一些請求透過以測試服務是否恢復正常。

如還是異常CircuitBreaker 將重新進入開啟OPEN狀態;如正常將進入關閉CLOSE閉合狀態恢復正常處理請求。

具體時間和頻次等屬性見具體實際案例

5.2 熔斷降級案例

5.2.1 計數的滑動視窗(count_based)

1.修改cloud-provider-payment8001

新建PayCircuitController類

@RestController
public class PayCircuitController {
    //=========Resilience4j CircuitBreaker 的例子
    @GetMapping(value = "/pay/circuit/{id}")
    public String myCircuit(@PathVariable("id") Integer id)
    {
        if(id == -4) throw new RuntimeException("----circuit id 不能負數");
        if(id == 9999){
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
        }
        return "Hello, circuit! inputId:  "+id+" \t " + IdUtil.simpleUUID();
    }
}

2.修改PayFeignApi介面

新增

/**
 * Resilience4j CircuitBreaker 的例子
 * @param id
 * @return
 */
@GetMapping(value = "/pay/circuit/{id}")
String myCircuit(@PathVariable("id") Integer id);

3.修改cloud-consumer-feign-order80

改pom

<!--resilience4j-circuitbreaker-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- 由於斷路保護等需要AOP實現,所以必須匯入AOP包 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

完整YML

server:
  port: 8080

spring:
  application:
    name: cloud-consumer-openfeign-order
  ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #優先使用服務ip進行註冊
        service-name: ${spring.application.name}
    openfeign:
      #新增	
      circuitbreaker:
        enabled: true
        group:
          enabled: true
      #    
      httpclient:
        hc5:
          enabled: true
      compression:
        request:
          enabled: true
          min-request-size: 2048 #最小觸發壓縮的大小
          mime-types: text/xml,application/xml,application/json #觸發壓縮資料型別
        response:
          enabled: true
      client:
        config:
          #全域性配置
          default:
            #連線超時時間
            connectTimeout: 3000
            #讀取超時時間
            readTimeout: 3000
          #單個微服務的配置,細粒度的重寫全域性
          cloud-payment-service:
            #連線超時時間
            connect-timeout: 20000
            #讀取超時時間
            read-timeout: 20000
logging:
  level:
    com.atguigu.cloud.apis.PayFeignApi: debug
# Resilience4j CircuitBreaker 按照次數:COUNT_BASED 的例子
#新增
resilience4j:
  circuitbreaker:
    configs:
      default:
        failureRateThreshold: 50 #設定50%的呼叫失敗時開啟斷路器,超過失敗請求百分⽐CircuitBreaker變為OPEN狀態。
        slidingWindowType: COUNT_BASED # 滑動視窗的型別
        slidingWindowSize: 6 #滑動窗⼝的⼤⼩配置COUNT_BASED表示6個請求,配置TIME_BASED表示6秒
        minimumNumberOfCalls: 6 #斷路器計算失敗率或慢呼叫率之前所需的最小樣本(每個滑動視窗週期)。如果minimumNumberOfCalls為10,則必須最少記錄10個樣本,然後才能計算失敗率。如果只記錄了9次呼叫,即使所有9次呼叫都失敗,斷路器也不會開啟。
        automaticTransitionFromOpenToHalfOpenEnabled: true # 是否啟用自動從開啟狀態過渡到半開狀態,預設值為true。如果啟用,CircuitBreaker將自動從開啟狀態過渡到半開狀態,並允許一些請求透過以測試服務是否恢復正常
        waitDurationInOpenState: 5s #從OPEN到HALF_OPEN狀態需要等待的時間
        permittedNumberOfCallsInHalfOpenState: 2 #半開狀態允許的最大請求數,預設值為10。在半開狀態下,CircuitBreaker將允許最多permittedNumberOfCallsInHalfOpenState個請求透過,如果其中有任何一個請求失敗,CircuitBreaker將重新進入開啟狀態。
        recordExceptions:
          - java.lang.Exception
    instances:
      cloud-payment-service:
        baseConfig: default
            

新增OrderCircuitController類

@RestController
public class OrderCircuitController {
    @Resource
    private PayFeignApi payFeignApi;

    @GetMapping(value = "/feign/pay/circuit/{id}")
    @CircuitBreaker(name = "cloud-payment-service", fallbackMethod = "myCircuitFallback")
    public String myCircuitBreaker(@PathVariable("id") Integer id) {
        return payFeignApi.myCircuit(id);
    }

    //myCircuitFallback就是服務降級後的兜底處理方法
    public String myCircuitFallback(Integer id, Throwable t) {
        // 這裡是容錯處理邏輯,返回備用結果
        return "myCircuitFallback,系統繁忙,請稍後再試-----/(ㄒoㄒ)/~~";
    }
}

@CircuitBreaker註解,以及預設的回撥fallbackMethod

5.2.2 測試

5.2.3 計時的滑動視窗(TIME_BASED)

原理

基於時間的滑動視窗是透過有N個桶的環形陣列實現。

如果滑動視窗的大小為10秒,這個環形陣列總是有10個桶,每個桶統計了在這一秒發生的所有呼叫的結果(部分統計結果),陣列中的第一個桶儲存了當前這一秒內的所有呼叫的結果,其他的桶儲存了之前每秒呼叫的結果。

滑動視窗不會單獨儲存所有的呼叫結果,而是對每個桶內的統計結果和總的統計值進行增量的更新,當新的呼叫結果被記錄時,總的統計值會進行增量更新。

檢索快照(總的統計值)的時間複雜度為O(1),因為快照已經預先統計好了,並且和滑動視窗大小無關。

關於此方法實現的空間需求(記憶體消耗)約等於O(n)。由於每次呼叫結果(元組)不會被單獨儲存,只是對N個桶進行單獨統計和一次總分的統計。

每個桶在進行部分統計時存在三個整型,為了計算,失敗呼叫數,慢呼叫數,總呼叫數。還有一個long型別變數,儲存所有呼叫的響應時間。

YML修改

resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 10s #神坑的位置,timelimiter 預設限制遠端1s,超於1s就超時異常,配置了降級,就走降級邏輯
  circuitbreaker:
    configs:
    #配置解釋,在slidingWindowSize(s)這個視窗內,最少有minimumNumberOfCalls個請求,且這麼多請求中,有minimumNumberOfCalls * slowCallRateThreshold次的時間是大於slowCallDurationThreshold時,就開啟斷路器,導致正常的請求也也會走降級方法,返回預設值。但是在5s後就變為半開狀態,同時,再有請求時,放過去2個,如果都成功,切換回關閉狀態,如果失敗,繼續等關閉5s然後嘗試。
      default:
        failureRateThreshold: 50 #設定50%的呼叫失敗時開啟斷路器,超過失敗請求百分⽐CircuitBreaker變為OPEN狀態。
        slowCallDurationThreshold: 2s #慢呼叫時間閾值,高於這個閾值的視為慢呼叫並增加慢呼叫比例。
        slowCallRateThreshold: 30 #慢呼叫百分比峰值,斷路器把呼叫時間⼤於slowCallDurationThreshold,視為慢呼叫,當慢呼叫比例高於閾值,斷路器開啟,並開啟服務降級
        slidingWindowType: TIME_BASED # 滑動視窗的型別
        slidingWindowSize: 2 #滑動視窗的大小配置,配置TIME_BASED表示2秒
        minimumNumberOfCalls: 2 #斷路器計算失敗率或慢呼叫率之前所需的最小樣本(每個滑動視窗週期)。
        permittedNumberOfCallsInHalfOpenState: 2 #半開狀態允許的最大請求數,預設值為10。
        waitDurationInOpenState: 5s #從OPEN到HALF_OPEN狀態需要等待的時間
        recordExceptions:
          - java.lang.Exception
    instances:
      cloud-payment-service:
        baseConfig: default

為了避免影響實驗,關閉FeignConfig的重試3次

    @Bean
    public Retryer retryer(){
        return Retryer.NEVER_RETRY;
        //maxAttempts - 1才是重試次數
        //最大請求次數(1 + 2),初始間隔時間100ms,重試間最大間隔時間1s
//        return new Retryer.Default(100,1,3);
    }

5.2.3 測試結果

5.3 熔斷+降級小總結


斷路器開閉的條件

當慢查詢達到一定峰值,或失敗率達到一定條件後,斷路器轉為open狀態,服務熔斷。當open時,都走的時fallbackMethod兜底方法,服務降級。

根據配置,一段時間後,open轉為half_open狀態,會根據配置,放幾個請求過去,如果都成功轉為closed,否則繼續open。

陽哥建議使用COUNT_BASED型別,計數型別。

5.4 隔離(bulkhead)

5.4.1 官網

https://resilience4j.readme.io/docs/bulkhead

中文

https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/bulkhead.md

5.4.2 是什麼

bulkhead(船的)艙壁/(飛機的)隔板

隔板來自造船行業,床倉內部一般會分成很多小隔艙,一旦一個隔艙漏水因為隔板的存在而不至於影響其它隔艙和整體船。

限制併發

5.4.3 能幹嗎


依賴隔離和負載保護:用來限制對下游服務的最大併發數量的限制。

Resilence4j提供了兩種隔離的實現方式,可以限制併發執行的數量。

訊號量(SemaphoreBulkhead)固定執行緒池(FixedThreadPoolBulkhead)

5.5 SemaphoreBulkhead案例

5.5.1 概述

基本上就是JUC訊號燈內容的同樣思想

訊號量艙壁(SemaphoreBulkhead)原理

當訊號量有空閒時,進入系統的請求會直接獲取訊號量並開始業務處理。

當訊號量全被佔用時,接下來的請求將會進入阻塞狀態,SemaphoreBulkhead提供了一個阻塞計時器,如果阻塞狀態的請求在阻塞計時內無法獲取到訊號量則系統會拒絕這些請求。若請求在阻塞計時內獲取到了訊號量,那將直接獲取訊號量並執行相應的業務處理。

5.5.2 原始碼
public SemaphoreBulkhead(String name, @Nullable BulkheadConfig bulkheadConfig,
    Map<String, String> tags) {
    this.name = name;
    this.config = requireNonNull(bulkheadConfig, CONFIG_MUST_NOT_BE_NULL);
    this.tags = requireNonNull(tags, TAGS_MUST_NOTE_BE_NULL);
    // init semaphore 
    //java.util.concurrent.Semaphore
    this.semaphore = new Semaphore(config.getMaxConcurrentCalls(), config.isFairCallHandlingEnabled());

    this.metrics = new BulkheadMetrics();
    this.eventProcessor = new BulkheadEventProcessor();
}

5.5.3 修改PayCircuitController

//=========Resilience4j bulkhead 的例子
@GetMapping(value = "/pay/bulkhead/{id}")
public String myBulkhead(@PathVariable("id") Integer id)
{
    if(id == -4) throw new RuntimeException("----bulkhead id 不能-4");

    if(id == 9999)
    {
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
    }

    return "Hello, bulkhead! inputId:  "+id+" \t " + IdUtil.simpleUUID();
}
5.5.4 修改PayFeignApi介面
/**
 * Resilience4j Bulkhead 的例子
 * @param id
 * @return
 */
@GetMapping(value = "/pay/bulkhead/{id}")
String myBulkhead(@PathVariable("id") Integer id);
5.5.5 修改cloud-consumer-feign-order80

POM

<!--resilience4j-bulkhead-->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
</dependency>

YML

resilience4j:
  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 2 # 隔離允許併發執行緒執行的最大數量
        maxWaitDuration: 1s # 當達到併發呼叫數量時,新的執行緒的阻塞時間,我只願意等待1秒,過時不候進艙壁兜底fallback
    instances:
      cloud-payment-service:
        baseConfig: default
  timelimiter:
    configs:
      default:
        timeout-duration: 20s

OrderCircuitController修改

/**
 *(船的)艙壁,隔離
 * @param id
 * @return
 */
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service", fallbackMethod = "myBulkheadFallback", type = Bulkhead.Type.SEMAPHORE)
public String myBulkhead(@PathVariable("id") Integer id) {
    return payFeignApi.myBulkhead(id);
}

public String myBulkheadFallback(Throwable t) {
    return "myBulkheadFallback,隔板超出最大數量限制,系統繁忙,請稍後再試-----/(ㄒoㄒ)/~~";
}
5.5.6 測試

5.6 FixedThreadPoolBulkhead案例

5.6.1 概述

基本上就是我們JUC-執行緒池內容的同樣思想

固定執行緒池艙壁(FixedThreadPoolBulkhead)

FixedThreadPoolBulkhead的功能與SemaphoreBulkhead一樣也是用於限制併發執行的次數的,但是二者的實現原理存在差別而且表現效果也存在細微的差別。FixedThreadPoolBulkhead使用一個固定執行緒池和一個等待佇列來實現艙壁。

當執行緒池中存在空閒時,則此時進入系統的請求將直接進入執行緒池開啟新執行緒或使用空閒執行緒來處理請求。當執行緒池中無空閒時時,接下來的請求將進入等待佇列,若等待佇列仍然無剩餘空間時接下來的請求將直接被拒絕,在佇列中的請求等待執行緒池出現空閒時,將進入執行緒池進行業務處理。

另外:ThreadPoolBulkhead只對CompletableFuture方法有效,所以我們必建立返回CompletableFuture型別的方法

5.6.2 原始碼
public FixedThreadPoolBulkhead(String name, @Nullable ThreadPoolBulkheadConfig bulkheadConfig,
    Map<String, String> tags) {
    this.name = name;
    this.config = requireNonNull(bulkheadConfig, CONFIG_MUST_NOT_BE_NULL);
    this.tags = requireNonNull(tags, TAGS_MUST_NOTE_BE_NULL);
    // init thread pool executor
    this.executorService = new ThreadPoolExecutor(config.getCoreThreadPoolSize(),
        config.getMaxThreadPoolSize(),
        config.getKeepAliveDuration().toMillis(), TimeUnit.MILLISECONDS,
        config.getQueueCapacity() == 0 ? new SynchronousQueue<>() : new ArrayBlockingQueue<>(config.getQueueCapacity()),
        new BulkheadNamingThreadFactory(name),
        config.getRejectedExecutionHandler());
    // adding prover jvm executor shutdown
    this.metrics = new FixedThreadPoolBulkhead.BulkheadMetrics();
    this.eventProcessor = new FixedThreadPoolBulkhead.BulkheadEventProcessor();
}

@Override
public <T> CompletableFuture<T> submit(Callable<T> callable) {
    final CompletableFuture<T> promise = new CompletableFuture<>();
    try {
        CompletableFuture.supplyAsync(ContextPropagator.decorateSupplier(config.getContextPropagator(),() -> {
            try {
                publishBulkheadEvent(() -> new BulkheadOnCallPermittedEvent(name));
                return callable.call();
            } catch (CompletionException e) {
                throw e;
            } catch (Exception e){
                throw new CompletionException(e);
            }
        }), executorService).whenComplete((result, throwable) -> {
            publishBulkheadEvent(() -> new BulkheadOnCallFinishedEvent(name));
            if (throwable != null) {
                promise.completeExceptionally(throwable);
            } else {
                promise.complete(result);
            }
        });
    } catch (RejectedExecutionException rejected) {
        publishBulkheadEvent(() -> new BulkheadOnCallRejectedEvent(name));
        throw BulkheadFullException.createBulkheadFullException(this);
    }
    return promise;
}
5.6.3 修改cloud-consumer-feign-order80

POM用修改了,引入resilience4j-bulkhead就包括Fixed..型別了

YML

####resilience4j bulkhead -THREADPOOL的例子
resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 10s #timelimiter預設限制遠端1s,超過報錯不好演示效果所以加上10秒
  thread-pool-bulkhead:
    configs:
      #最大請求是max-thread-pool-size + queue-capacity個,後面的如果再來報錯
      default:
        core-thread-pool-size: 1
        max-thread-pool-size: 1
        queue-capacity: 1
    instances:
      cloud-payment-service:
        baseConfig: default
# spring.cloud.openfeign.circuitbreaker.group.enabled 請設定為false 新啟執行緒和原來主執行緒脫離

修改

/**
 * (船的)艙壁,隔離,THREADPOOL
 * @param id
 * @return
 */
@GetMapping(value = "/feign/pay/bulkhead/{id}")
@Bulkhead(name = "cloud-payment-service",fallbackMethod = "myBulkheadPoolFallback",type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<String> myBulkheadTHREADPOOL(@PathVariable("id") Integer id)
{
    System.out.println(Thread.currentThread().getName()+"\t"+"enter the method!!!");
    try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
    System.out.println(Thread.currentThread().getName()+"\t"+"exist the method!!!");

    return CompletableFuture.supplyAsync(() -> payFeignApi.myBulkhead(id) + "\t" + " Bulkhead.Type.THREADPOOL");
}
public CompletableFuture<String> myBulkheadPoolFallback(Integer id,Throwable t)
{
    return CompletableFuture.supplyAsync(() -> "Bulkhead.Type.THREADPOOL,系統繁忙,請稍後再試-----/(ㄒoㄒ)/~~");
}
5.6.4 測試

5.7 限流(ratelimiter)

5.7.1 官網

https://resilience4j.readme.io/docs/ratelimiter

中文

https://github.com/lmhmhl/Resilience4j-Guides-Chinese/blob/main/core-modules/ratelimiter.md

5.7.2 是什麼

限流:就是限制最大訪問流量。系統能提供的最大併發是有限的,同時來的請求又太多,就需要限流。

比如商城秒殺業務,瞬時大量請求湧入,伺服器忙不過就只好排隊限流了,和去景點排隊買票和去醫院辦理業務排隊等號道理相同。

所謂限流,就是透過對併發訪問/請求進行限速,或者對一個時間視窗內的請求進行限速,以保護應用系統,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理。

5.7.3 常見限流演算法

漏桶演算法

一個固定容量的漏桶,按照設定常量固定速率流出水滴,類似醫院打吊針,不管你源頭流量多大,我設定勻速流出。

如果流入水滴超出了桶的容量,則流入的水滴將會溢位了(被丟棄),而漏桶容量是不變的。

這裡有兩個變數,一個是桶的大小,支援流量突發增多時可以存多少的水(burst),另一個是水桶漏洞的大小(rate)。因為漏桶的漏出速率是固定的引數,所以,即使網路中不存在資源衝突(沒有發生擁塞),漏桶演算法也不能使流突發(burst)到埠速率。因此,漏桶演算法對於存在突發特性的流量來說缺乏效率。

令牌桶演算法

令牌桶演算法(Token Bucket Algorithm)是一種流量控制演算法,廣泛應用於計算機網路的流量整形和速率限制中。其工作原理相對直觀:

  1. 初始化:建立一個令牌桶,並設定桶的最大容量(以令牌的數量表示)。初始時,桶內可能為空或含有一定數量的令牌。
  2. 令牌生成:以恆定的速率向桶內新增令牌。如果桶已滿,則新生成的令牌會被丟棄。
  3. 請求處理:每當有一個資料包(或請求)到達時,系統會嘗試從桶中取出一個令牌。如果此時桶中有可用的令牌,則取出令牌並允許資料包透過(即處理請求)。如果桶中沒有令牌,則該資料包可能被延遲或者直接拒絕。
  4. 突發流量處理:由於桶可以儲存一定數量的令牌,所以當短時間內有大量的資料包到來時(即突發流量),只要桶內的令牌足夠,就可以立即處理這些資料包,從而支援一定程度上的流量突增。
  5. 動態調整:某些實現允許動態調整生成令牌的速率或桶的大小,以適應不同的網路狀況或服務質量(QoS)需求。

與漏桶演算法相比,令牌桶演算法能夠更好地處理突發流量,因為它允許某種程度上的“儲存”流量處理能力(即儲存令牌),而漏桶演算法則嚴格按恆定速率處理所有到來的流量,超出處理能力的流量都會被丟棄。
滾動時間窗演算法

允許固定數量的請求進入(比如1秒取4個資料相加,超過25值就over)超過數量就拒絕或者排隊,等下一個時間段進入。

由於是在一個時間間隔內進行限制,如果使用者在上個時間間隔結束前請求(但沒有超過限制),同時在當前時間間隔剛開始請求(同樣沒超過限制),在各自的時間間隔內,這些請求都是正常的。下圖統計了3次,but......

缺點

假如設定1分鐘最多可以請求100次某個介面,如12:00:00-12:00:59時間段內沒有資料請求但12:00:59-12:01:00時間段內突然併發100次請求,緊接著瞬間跨入下一個計數週期計數器清零;在12:01:00-12:01:01內又有100次請求。那麼也就是說在時間臨界點左右可能同時有2倍的峰值進行請求,從而造成後臺處理請求加倍過載的bug,導致系統運營能力不足,甚至導致系統崩潰,/(ㄒoㄒ)/~~

滑動時間窗演算法

滑動時間視窗(sliding time window)

顧名思義,該時間視窗是滑動的。所以,從概念上講,這裡有兩個方面的概念需要理解:

- 視窗:需要定義視窗的大小

- 滑動:需要定義在視窗中滑動的大小,但理論上講滑動的大小不能超過視窗大小

滑動視窗演算法是把固定時間片進行劃分並且隨著時間移動,移動方式為開始時間點變為時間列表中的第2個時間點,結束時間點增加一個時間點,

不斷重複,透過這種方式可以巧妙的避開計數器的臨界點的問題。下圖統計了5次

5.8 限流案例

5.8.1 修改cloud-provider-payment8001

PayCircuitController類新增

//=========Resilience4j ratelimit 的例子
@GetMapping(value = "/pay/ratelimit/{id}")
public String myRatelimit(@PathVariable("id") Integer id)
{
    return "Hello, myRatelimit歡迎到來 inputId:  "+id+" \t " + IdUtil.simpleUUID();
}
5.8.2 PayFeignApi
/**
 * Resilience4j Ratelimit 的例子
 * @param id
 * @return
 */
@GetMapping(value = "/pay/ratelimit/{id}")
String myRatelimit(@PathVariable("id") Integer id);
5.8.3 修改cloud-consumer-feign-order80

POM

<!--resilience4j-ratelimiter-->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-ratelimiter</artifactId>
</dependency>

YML

####resilience4j ratelimiter 限流的例子
resilience4j:
  ratelimiter:
    configs:
      default:
        limitForPeriod: 2 #在一次重新整理週期內,允許執行的最大請求數
        limitRefreshPeriod: 1s # 限流器每隔limitRefreshPeriod重新整理一次,將允許處理的最大請求數量重置為limitForPeriod
        timeout-duration: 1 # 執行緒等待許可權的預設等待時間
    instances:
      cloud-payment-service:
        baseConfig: default

OrderCircuitController類新增

@GetMapping(value = "/feign/pay/ratelimit/{id}")
@RateLimiter(name = "cloud-payment-service",fallbackMethod = "myRatelimitFallback")
public String myBulkhead(@PathVariable("id") Integer id)
{
    return payFeignApi.myRatelimit(id);
}
public String myRatelimitFallback(Integer id,Throwable t)
{
    return "你被限流了,禁止訪問/(ㄒoㄒ)/~~";
}
5.8.4 測試

只是為了記錄自己的學習歷程,且本人水平有限,不對之處,請指正。

相關文章