springcloud3(六) 服務降級限流熔斷組件Resilience4j

代碼地址:https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/spb-gateway/src/test/java/com/kawa/spbgateway/circuitbreaker/resilience4j

公司的網關(基於Spring Cloud Gateway)上線有一段時間了,目前只有一個簡單的動態路由的功能,接下來的工作一部分會涉及到服務的保護和服務健壯性方面,也就是要加入限流,熔斷和降級等特性。此處找了下業界成熟的開源框架如下表的對比

 Sentinel(Alibaba開源)Hystrix(不再維護)resilience4j(Spring官方推薦)
隔離策略 信號量隔離(併發控制) 線程池隔離/信號量隔離 信號量隔離
熔斷降級策略 基於慢調用比例、異常比例、異常數 基於異常比例 基於異常比例、響應時間
實時統計實現 滑動窗口(LeapArray) 滑動窗口(基於 RxJava) Ring Bit Buffer
動態規則配置 支持多種數據源 支持多種數據源 有限支持
擴展性 多個擴展點 插件的形式 接口的形式
基於註解的支持 支持 支持 支持
限流 基於 QPS,支持基於調用關係的限流 有限的支持 Rate Limiter
流量整形 支持預熱模式與勻速排隊控制效果 不支持 簡單的 Rate Limiter 模式
系統自適應保護 支持 不支持 不支持
多語言支持 Java/Go/C++ Java Java
Service Mesh 支持 支持 Envoy/Istio 不支持 不支持
控制檯 提供開箱即用的控制檯,可配置規則、實時監控、機器發現等 簡單的監控查看 不提供控制檯,可對接其它監控系統

對比來自:https://github.com/alibaba/Sentinel/wiki/Guideline:-%E4%BB%8E-Hystrix-%E8%BF%81%E7%A7%BB%E5%88%B0-Sentinel

最終基於公司的需求,準備引入Resilience4j組件, 所以這篇博客是來梳理Resilience4j的組件的使用方式, 下一篇博客寫結合Spring Cloud Gateway的實現自定義的服務限流保護策略

1. Resilience4j

Resilience4j官方guide: https://resilience4j.readme.io/docs

Resilience4j 常用的組件有5個 -> CircuitBreakerBulkheadRateLimiterRetry 和 TimeLimiter (Cache不推薦在生產環境使用,所以這篇博客不做介紹 ), 本篇博客基於1.7.0的版本介紹

1.1 CircuitBreaker

斷路器是通過具有三個正常狀態的有限狀態機實現的:CLOSED、OPEN 和 HALF_OPEN 以及兩個特殊狀態 DISABLED 和 FORCED_OPEN。CircuitBreaker 使用滑動窗口來存儲和聚合調用的結果。您可以在基於計數的滑動窗口和基於時間的滑動窗口之間進行選擇。基於計數的滑動窗口聚合最後 N 次調用的結果。基於時間的滑動窗口聚合了最近 N 秒的調用結果。

1.1.1 CircuitBreakerConfig

CircuitBreakerConfig看名字大家也知道了它是做什麼的(好的編碼就是見文知意),CircuitBreaker的配置類,在實際項目中除了全局的配置,有些場景需要我們自定義一些CircuitBreaker的配置,這個時候就需要用到Circuitreakeronfig,Circuitreakeronfig全部屬性如下表

配置屬性 默認值 描述
failureRateThreshold 50 以百分比形式配置失敗率閾值。
當故障率等於或大於閾值時,斷路器轉換爲斷開並開始短路調用。
slowCallRateThreshold 100 以百分比配置閾值。當呼叫持續時間大於 或等於閾值時,斷路器將呼叫視爲慢速呼叫。當慢速呼叫的百分比等於或大於閾值時,斷路器轉換爲斷開並開始短路呼叫。slowCallDurationThreshold
slowCallDurationThreshold 60000 [毫秒] 配置持續時間閾值,該數值的呼叫速度緩慢並增加呼叫的速度。
permittedNumberOfCalls
InHalfOpenState
10 配置半開時允許的呼叫數量。
maxWaitDurationInHalfOpenState 0 [毫秒] 配置最大等待持續時間,控制斷路器在切換到打開狀態之前可以保持在半開狀態的最長時間。
值 0 表示斷路器將在 HalfOpen 狀態無限等待,直到所有允許的調用都完成。
slidingWindowType COUNT_BASED 配置用於記錄CircuitBreaker關閉時調用結果的滑動窗口的類型。
滑動窗口可以是基於計數的,也可以是基於時間的。
如果滑動窗口爲 COUNT_BASED,則記錄並彙總最後一次調用。 如果滑動窗口是 TIME_BASED,則記錄和聚合最後幾秒的調用。slidingWindowSize
slidingWindowSize
slidingWindowSize 100 配置用於記錄關閉時調用窗口的窗口大小。
minimumNumberOfCalls 100 配置在斷路器計算錯誤率或慢速調用率之前所需的最小調用數(每個滑動窗口週期)。
例如,如果minimumNumberOfCalls爲10,則必須至少記錄10個呼叫,然後才能計算失敗率。
如果僅記錄了9個呼叫,則即使有9個呼叫都失敗,斷路器也不會轉換爲打開狀態。
waitDurationInOpenState 60000 [毫秒] 半從打開轉換到打開之前應等待的時間。
automaticTransition
FromOpenToHalfOpenEnabled
FALSE 如果設置爲 true,則意味着 CircuitBreaker 將自動從打開狀態轉換爲半打開狀態,並且不需要調用來觸發轉換。創建一個線程來監視 CircuitBreakers 的所有實例,一旦 waitDurationInOpenState 通過,將它們轉換爲 HALF_OPEN。然而,如果設置爲 false,則僅在進行調用時纔會轉換到 HALF_OPEN,即使在傳遞了 waitDurationInOpenState 之後也是如此。這裏的優點是沒有線程監視所有斷路器的狀態。
recordExceptions empty 記錄爲失敗並因此增加失敗率的異常列表。
任何匹配或從列表之一繼承的異常都算作失敗,除非通過。 如果您指定異常列表,則所有其他異常都算作成功,除非它們被明確忽略。ignoreExceptions
ignoreExceptions empty 被忽略且既不計爲失敗也不計爲成功的異常列表。
即使異常是。recordExceptions
recordFailurePredicate throwable -> true
默認情況下,所有異常都記錄爲失敗。
一個自定義Predicate,用於評估是否應將異常記錄爲失敗。
如果異常應算作失敗,則謂詞必須返回 true。如果異常
應算作成功,則謂詞必須返回 false,除非異常被 顯式忽略。ignoreExceptions
ignoreExceptions throwable -> false
默認情況下不會忽略任何異常。
一個自定義Predicate,用於評估是否應忽略異常並且既不視爲失敗也不成功。
如果應忽略異常,謂詞必須返回 true。
如果異常應算作失敗,則謂詞必須返回 false。

1.1.2 CircuitBreakerRegistry

 CircuitBreakerRegistry是CircuitBreaker的註冊器,其有一個唯一的實現類InMemoryCircuitBreakerRegistry,核心方法如下

// 根據name返回CircuitBreaker或返回默認的CircuitBreaker
// 下面的幾個重載的方法,也是一樣的邏輯,有就直接返回,沒有就創建後返回
public CircuitBreaker circuitBreaker(String name)
public CircuitBreaker circuitBreaker(String name, io.vavr.collection.Map<String, String> tags)
public CircuitBreaker circuitBreaker(String name, CircuitBreakerConfig config) {
public CircuitBreaker circuitBreaker(String name, CircuitBreakerConfig config, io.vavr.collection.Map<String, String> tags)
public CircuitBreaker circuitBreaker(String name, String configName)
public CircuitBreaker circuitBreaker(String name, String configName, io.vavr.collection.Map<String, String> tags) {
public CircuitBreaker circuitBreaker(String name, Supplier<CircuitBreakerConfig> circuitBreakerConfigSupplier)
public CircuitBreaker circuitBreaker(String name, Supplier<CircuitBreakerConfig> circuitBreakerConfigSupplier, io.vavr.collection.Map<String, String> tags)

1.1.3 CircuitBreaker

現在到了我們的核心接口CircuitBreaker,下面的靜態方法有20多個,在這我就列幾個常用的方法,其它方法可以看源碼註釋的描述

// 返回一個被CircuitBreaker包裝的 CheckedFunction0.
// CheckedFunction0 是由vavr封裝的類似java8中Supplier的函數
static <T> CheckedFunction0<T> decorateCheckedSupplier(CircuitBreaker circuitBreaker, CheckedFunction0<T> supplier) // 返回一個被CircuitBreaker包裝的 CheckedRunnable.
//CheckedRunnable 是由avr封裝的 Runnable
static CheckedRunnable decorateCheckedRunnable(CircuitBreaker circuitBreaker, CheckedRunnable runnable) // 返回一個被CircuitBreaker包裝的 Callable. static <T> Callable<T> decorateCallable(CircuitBreaker circuitBreaker, Callable<T> callable) // 返回一個被CircuitBreaker包裝的 Supplier. static <T> Supplier<T> decorateSupplier(CircuitBreaker circuitBreaker, Supplier<T> supplier) // 返回一個可以retry的 Supplierstatic <T> Supplier<Try<T>> decorateTrySupplier(CircuitBreaker circuitBreaker, Supplier<Try<T>> supplier) // 返回一個被CircuitBreaker包裝的 Consumer. static <T> Consumer<T> decorateConsumer(CircuitBreaker circuitBreaker, Consumer<T> consumer) // 返回一個被CircuitBreaker包裝的 CheckedConsumer.
// CheckedConsumer 是由avr封裝的CheckedConsumer
static <T> CheckedConsumer<T> decorateCheckedConsumer(CircuitBreaker circuitBreaker, CheckedConsumer<T> consumer) // 返回一個被CircuitBreaker包裝的 Runnable. static Runnable decorateRunnable(CircuitBreaker circuitBreaker, Runnable runnable) // 返回一個被CircuitBreaker包裝的 Function. static <T, R> Function<T, R> decorateFunction(CircuitBreaker circuitBreaker, Function<T, R> function) // 返回一個被CircuitBreaker包裝的 CheckedFunction1.
// CheckedFunction1是由avr封裝的Function
static <T, R> CheckedFunction1<T, R> decorateCheckedFunction(CircuitBreaker circuitBreaker, CheckedFunction1<T, R> function) // 返回一個被CircuitBreaker包裝的 Supplier<Future>. static <T> Supplier<Future<T>> decorateFuture(CircuitBreaker circuitBreaker, Supplier<Future<T>> supplier)

從上面列舉的常用方法看到有很多好像有重複的方法,CircuitBreaker有返回封裝Supplier, Consumer, Function, Runnable的方法,然後還有一個與之對應的返回封裝CheckedSupplier, CheckedConsumer, CheckedFunction, CheckedRunnable的方法。 爲什麼有兩套實現呢?resilience4j,這個項目是基於Java 8開發的,但是java8受限於 Java 標準庫的通用性要求和二進制文件大小,Java 標準庫對函數式編程的 API 支持相對比較有限。函數的聲明只提供了 Function 和 BiFunction 兩種,流上所支持的操作的數量也較少。基於這些原因,需要vavr 來更好得使用Java 8進行函數式開發。

簡單看下方法decorateCheckedSupplier(CircuitBreaker circuitBreaker, CheckedFunction0<T> supplier)

    static <T> CheckedFunction0<T> decorateCheckedSupplier(CircuitBreaker circuitBreaker,
        CheckedFunction0<T> supplier) {
        return () -> {
     // 申請執行函數方法supplier.apply()的許可
// 具體邏輯在CircuiBreakerStateMachine中的CircuitBreakerState中實現     circuitBreaker.acquirePermission();
final long start = circuitBreaker.getCurrentTimestamp(); try {
          // 執行目標方法 T result
= supplier.apply(); long duration = circuitBreaker.getCurrentTimestamp() - start;
//目標方法執行完調用onResult(),check result最終調用onSuccess() circuitBreaker.onResult(duration, circuitBreaker.getTimestampUnit(), result);
return result; } catch (Exception exception) { // Do not handle java.lang.Error long duration = circuitBreaker.getCurrentTimestamp() - start;
// 如果出現異常就調用onError(),執行onError策略的邏輯 circuitBreaker.onError(duration, circuitBreaker.getTimestampUnit(), exception);
throw exception; } }; }

 大體流程如下圖

關於vavr的詳情可以查看官網文檔:https://docs.vavr.io/

CircuitBreaker唯一的實現類CircuitBreakerStateMachine

CircuitBreakerStateMachine是一個有線狀態的狀態機。斷路器管理後端系統的狀態。斷路器通過具有五種狀態的有限狀態機實現:CLOSED、OPEN、HALF_OPEN、DISABLED 和 FORCED_OPEN。 CircuitBreakerStateMachine可以做到這些狀態的轉換,比如下面的幾個方法

@Override
 public void transitionToDisabledState() {
     stateTransition(DISABLED, currentState -> new DisabledState());
 }

 @Override
 public void transitionToMetricsOnlyState() {
     stateTransition(METRICS_ONLY, currentState -> new MetricsOnlyState());
 }

 @Override
 public void transitionToForcedOpenState() {
     stateTransition(FORCED_OPEN,
         currentState -> new ForcedOpenState(currentState.attempts() + 1));
 }

 @Override
 public void transitionToClosedState() {
     stateTransition(CLOSED, currentState -> new ClosedState());
 }

 @Override
 public void transitionToOpenState() {
     stateTransition(OPEN,
         currentState -> new OpenState(currentState.attempts() + 1, currentState.getMetrics()));
 }

 @Override
 public void transitionToHalfOpenState() {
     stateTransition(HALF_OPEN, currentState -> new HalfOpenState(currentState.attempts()));
 }

這些狀態的流轉是通過發佈事件來完成的,可以看下面都是CircuitBreakerStateMachine的事件

private void publishResetEvent() {
     final CircuitBreakerOnResetEvent event = new CircuitBreakerOnResetEvent(name);
    publishEventIfPossible(event);
}

private void publishCallNotPermittedEvent() {
    final CircuitBreakerOnCallNotPermittedEvent event = new CircuitBreakerOnCallNotPermittedEvent(
        name);
    publishEventIfPossible(event);
}

private void publishSuccessEvent(final long duration, TimeUnit durationUnit) {
    final CircuitBreakerOnSuccessEvent event = new CircuitBreakerOnSuccessEvent(name,
        Duration.ofNanos(durationUnit.toNanos(duration)));
    publishEventIfPossible(event);
}

private void publishCircuitErrorEvent(final String name, final long duration,
    TimeUnit durationUnit, final Throwable throwable) {
    final CircuitBreakerOnErrorEvent event = new CircuitBreakerOnErrorEvent(name,
        Duration.ofNanos(durationUnit.toNanos(duration)), throwable);
    publishEventIfPossible(event);
}

private void publishCircuitIgnoredErrorEvent(String name, long duration, TimeUnit durationUnit,
    Throwable throwable) {
    final CircuitBreakerOnIgnoredErrorEvent event = new CircuitBreakerOnIgnoredErrorEvent(name,
        Duration.ofNanos(durationUnit.toNanos(duration)), throwable);
    publishEventIfPossible(event);
}

private void publishCircuitFailureRateExceededEvent(String name, float failureRate) {
    final CircuitBreakerOnFailureRateExceededEvent event = new CircuitBreakerOnFailureRateExceededEvent(name,
        failureRate);
    publishEventIfPossible(event);
}

private void publishCircuitSlowCallRateExceededEvent(String name, float slowCallRate) {
    final CircuitBreakerOnSlowCallRateExceededEvent event = new CircuitBreakerOnSlowCallRateExceededEvent(name,
        slowCallRate);
    publishEventIfPossible(event);
}

private void publishCircuitThresholdsExceededEvent(Result result, CircuitBreakerMetrics metrics) {
    if (Result.hasFailureRateExceededThreshold(result)) {
        publishCircuitFailureRateExceededEvent(getName(), metrics.getFailureRate());
    }
    if (Result.hasSlowCallRateExceededThreshold(result)) {
        publishCircuitSlowCallRateExceededEvent(getName(), metrics.getSlowCallRate());
    }
}

 1.1.4 CircuitBreaker Demo

引入測試組件spring-cloud-starter-contract-stub-runner

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    <scope>test</scope>
</dependency>

Resilience4jTestHelper測試輔助類

    /**
     * get the CircuitBreaker status and metrics
     *
     * @param prefixName
     * @param circuitBreaker
     * @return circuitBreaker state
     */
    public static String getCircuitBreakerStatus(String prefixName, CircuitBreaker circuitBreaker) {

        CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics();
        float failureRate = metrics.getFailureRate();
        int failedCalls = metrics.getNumberOfFailedCalls();
        int successfulCalls = metrics.getNumberOfSuccessfulCalls();
        long notPermittedCalls = metrics.getNumberOfNotPermittedCalls();
        int bufferedCalls = metrics.getNumberOfBufferedCalls();
        float slowCallRate = metrics.getSlowCallRate();
        int slowCalls = metrics.getNumberOfSlowCalls();
        int slowFailedCalls = metrics.getNumberOfSlowFailedCalls();
        int slowSuccessfulCalls = metrics.getNumberOfSlowSuccessfulCalls();

        log.info(prefixName + " state=" + circuitBreaker.getState() + " , metrics[ failureRate=" + failureRate +
                ", failedCalls=" + failedCalls +
                ", successCalls=" + successfulCalls +
                ", notPermittedCalls=" + notPermittedCalls +
                ", bufferedCalls=" + bufferedCalls +
                ", \n\tslowCallRate=" + slowCallRate +
                ", slowCalls=" + slowCalls +
                ", slowFailedCalls=" + slowFailedCalls +
                ", slowSuccessfulCalls=" + slowSuccessfulCalls +
                " ]"
        );
        log.info(prefixName + " circuitBreaker tags:{}", circuitBreaker.getTags());
        return circuitBreaker.getState().name();
    }

    public static void circuitBreakerEventListener(CircuitBreaker circuitBreaker) {
        circuitBreaker.getEventPublisher()
                .onSuccess(event -> log.info("---------- CircuitBreakerEvent:{}  CircuitBreakerName:{}", event.getEventType(), event.getCircuitBreakerName()))
                .onError(event -> {
                    log.info("---------- CircuitBreakerEvent:{}  CircuitBreakerName:{}", event.getEventType(), event.getCircuitBreakerName());
                    Throwable throwable = event.getThrowable();
                    if (throwable instanceof TimeoutException) {
                        // TODO record to slow call
                    }
                })
                .onIgnoredError(event -> log.info("---------- CircuitBreakerEvent:{}  CircuitBreakerName:{}", event.getEventType(), event.getCircuitBreakerName()))
                .onReset(event -> log.info("---------- CircuitBreakerEvent:{}  CircuitBreakerName:{}", event.getEventType(), event.getCircuitBreakerName()))
                .onStateTransition(event -> log.info("---------- CircuitBreakerEvent:{}  CircuitBreakerName:{}", event.getEventType(), event.getCircuitBreakerName()))
                .onCallNotPermitted(event -> log.info("---------- CircuitBreakerEvent:{}  CircuitBreakerName:{}", event.getEventType(), event.getCircuitBreakerName()))
                .onFailureRateExceeded(event -> log.info("---------- CircuitBreakerEvent:{}  CircuitBreakerName:{}", event.getEventType(), event.getCircuitBreakerName()))
                .onSlowCallRateExceeded(event -> log.info("---------- CircuitBreakerEvent:{}  CircuitBreakerName:{}", event.getEventType(), event.getCircuitBreakerName()));
    }

Resilience4jTest測試類

    @Rule
    public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().port(8080));

    private WebTestClient testClient;

    private CircuitBreakerRegistry circuitBreakerRegistry;private CircuitBreaker circuitBreaker;

    private CircuitBreaker circuitBreakerWithTags;

    private CircuitBreakerConfig circuitBreakerConfig;private String PATH_200 = "/api/pancake/v1/yee/query";

    private String PATH_400 = "/api/hk/card/v1/er/query";

    private String PATH_408 = "/api/pancake/v1/coin/query";

    private String PATH_500 = "/api/hk/card/v1/card/query";

    @Before
    public void setup() {
        HttpClient httpClient = HttpClient.create().wiretap(true);
        testClient = WebTestClient.bindToServer(new ReactorClientHttpConnector(httpClient))
                .baseUrl("http://localhost:8080")
                .responseTimeout(Duration.ofDays(1))
                .build();

        circuitBreakerRegistry = new InMemoryCircuitBreakerRegistry();
        circuitBreakerConfig = CircuitBreakerConfig
                .custom()
                .failureRateThreshold(70)
                .slowCallRateThreshold(90)
                .slowCallDurationThreshold(Duration.ofMillis(1000 * 1))
                .minimumNumberOfCalls(10)
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
                .slidingWindowSize(10)
                .build();
        circuitBreaker = circuitBreakerRegistry.circuitBreaker("resilience4jTest", circuitBreakerConfig);
        Resilience4jTestHelper.circuitBreakerEventListener(circuitBreaker);

        stubFor(post(urlMatching(PATH_200))
                .willReturn(okJson("{}")));

        stubFor(post(urlMatching(PATH_400))
                .willReturn(badRequest()));

        stubFor(post(urlMatching(PATH_408))
                .willReturn(okJson("{\"message\":\"time out\"}").withFixedDelay(1000 * 2)));

        stubFor(post(urlMatching(PATH_500))
                .willReturn(serverError()));
    }


    @Test
    public void When_Test_CircuitBreaker_Expect_Close() {
        AtomicInteger count = new AtomicInteger();
        for (int i = 0; i < 10; i++) {
            Resilience4jTestHelper.recordResponseToCircuitBreaker(circuitBreaker, testClient, PATH_200);
            Resilience4jTestHelper.getCircuitBreakerStatus(">>>>>>>>>> end call " + count.incrementAndGet(), circuitBreaker);
        }
        assertEquals(CircuitBreaker.State.CLOSED.name(), circuitBreaker.getState().name());
    }

    @Test
    public void When_CircuitBreaker_Expect_Open() {
        circuitBreakerWithTags = circuitBreakerRegistry.circuitBreaker("circuitBreakerWithTags", circuitBreakerConfig, HashMap.of("resilience4jTest", "When_CircuitBreaker_Expect_Open"));
        Resilience4jTestHelper.circuitBreakerEventListener(circuitBreakerWithTags);

        AtomicInteger count = new AtomicInteger();
        for (int i = 0; i < 10; i++) {
            Resilience4jTestHelper.recordResponseToCircuitBreaker(circuitBreakerWithTags, testClient, PATH_400);
            Resilience4jTestHelper.getCircuitBreakerStatus(">>>>>>>>>> end call " + count.incrementAndGet(), circuitBreakerWithTags);
        }
        assertEquals(CircuitBreaker.State.OPEN.name(), circuitBreakerWithTags.getState().name());
    }

    @Test
    public void When_Test_CircuitBreaker_Expect_SlowCall() throws Throwable {
        AtomicInteger count = new AtomicInteger();
        for (int i = 0; i < 10; i++) {
            circuitBreaker.executeCheckedSupplier(() -> {
                Resilience4jTestHelper.recordSlowCallResponseToCircuitBreaker(circuitBreaker, testClient, PATH_408);
                return null;
            });
            Resilience4jTestHelper.getCircuitBreakerStatus(">>>>>>>>>> end call " + count.incrementAndGet(), circuitBreaker);
        }
        assertEquals(CircuitBreaker.State.OPEN.name(), circuitBreaker.getState().name());
    }

    @Test
    public void When_CircuitBreaker_Expect_Fallback() {
        AtomicInteger count = new AtomicInteger();
        for (int i = 0; i < 20; i++) {
            String path = PATH_500;
            CheckedFunction0<String> response =
                    circuitBreaker.decorateCheckedSupplier(() -> Resilience4jTestHelper.responseToCircuitBreaker(circuitBreaker, testClient, path));
            Try<String> result = Try.of(response).map(val -> {
                Resilience4jTestHelper.getCircuitBreakerStatus(">>>>>>>>>> call success " + count.incrementAndGet(), circuitBreaker);
                return val;
            }).recover(CallNotPermittedException.class, throwable -> {
                Resilience4jTestHelper.getCircuitBreakerStatus(">>>>>>>>>> open CircuitBreaker " + count.incrementAndGet(), circuitBreaker);
                return "hit CallNotPermittedException";
            }).recover(throwable -> {
                Resilience4jTestHelper.getCircuitBreakerStatus(">>>>>>>>>> call fallback " + count.incrementAndGet(), circuitBreaker);
                return "hit fallback";
            });
            log.info(">>>>>>>>>> result:{}", result.get());
            if (count.get() > 10) {
                assertEquals("hit CallNotPermittedException", result.get());
            }
        }
    }

1.2 Bulkhead

Bulkhead提供了兩種隔板模式的實現,可用於限制併發執行的數量

1. 使用信號量  SemaphoreBulkhead
2. 使用有界隊列和固定線程池  FixedThreadPoolBulkhead

其中線程池的方式屬於資源佔用型,在這個不做討論,如果感興趣可以去看看官方的樣例

1.2.1 BulkheadConfig 

BulkheadConfig是Bulkhead的配置類,使用BulkheadConfig配置類,自定義Blukhead配置。配置類BulkheadConfig有以下屬性

 

配置屬性 默認值 描述
maxConcurrentCalls 25 隔板允許的最大並行執行量
maxWaitDuration 0 嘗試進入飽和的Bulkhead時應阻塞線程的最長時間

1.2.2 BulkheadRegistry

和CircuitBreaker模塊一樣,BulkheadRegistry提供了一個內存中的實現類InMemoryBulkheadRegistry,可以使用它來管理(創建和獲取)Bulkhead實例。

1.2.3 Bulkhead

Bulkhead接口的靜態方法和CircuitBreaker方法命名類似,如下下面的decorateCheckedSupplier方法

    static <T> CheckedFunction0<T> decorateCheckedSupplier(Bulkhead bulkhead,
        CheckedFunction0<T> supplier) {
        return () -> {
            bulkhead.acquirePermission();
            try {
                return supplier.apply();
            } finally {
                bulkhead.onComplete();
            }
        };
    }

Bulkhead的靜態方法,中主要靠bulkhead.acquirePermission()和bulkhead.tryAcquirePermission()申請執行權限,靠bulkhead.onComplete()是釋放執行權限,當然還有一個方法bulkhead.releasePermission() 也可以釋放執行權限,兩者區別就是bulkhead.onComplete()多了一個觸發執行完成的事件publishBulkheadEvent(() -> new BulkheadOnCallFinishedEvent(name))。

如果我們不想用Bulkhead自帶的靜態方法也是可以的,比如我下面的demo, 僅僅使用bulkhead.tryAcquirePermission()和bulkhead.onComplete(),就可以模擬一個服務過載的場景

1.2.4 Bulkhead Demo

Resilience4jTestHelper測試輔助類

    public static String responseToBulkhead(Bulkhead bulkhead, WebTestClient testClient, String path) {
        WebTestClient.ResponseSpec responseSpec = testClient.post().uri(path).exchange();
        if (bulkhead.getMetrics().getAvailableConcurrentCalls() < 1) {
            throw BulkheadFullException.createBulkheadFullException(bulkhead);
        }
        try {
            responseSpec.expectStatus().is4xxClientError();
            throw new RuntimeException("<<<<< hit 4XX >>>>>");
        } catch (Throwable error) {
        }

        try {
            responseSpec.expectStatus().is5xxServerError();
            throw new RuntimeException("<<<<< hit 5XX >>>>>");
        } catch (Throwable error) {
        }
        responseSpec.expectStatus().is2xxSuccessful();
        return "hit 200";
    }

    /**
     * get the Bulkhead status and metrics
     * * @param prefixName
     *
     * @param bulkhead
     */
    public static void getBulkheadStatus(String prefixName, Bulkhead bulkhead) {
        Bulkhead.Metrics metrics = bulkhead.getMetrics();
        int availableCalls = metrics.getAvailableConcurrentCalls();
        int maxCalls = metrics.getMaxAllowedConcurrentCalls();
        log.info(prefixName + "bulkhead metrics[ availableCalls=" + availableCalls +
                ", maxCalls=" + maxCalls + " ],tags=" + bulkhead.getTags());
    }

    public static void bulkheadEventListener(Bulkhead bulkhead) {
        bulkhead.getEventPublisher()
                .onCallRejected(event -> log.info("---------- BulkheadEvent:{}  BulkheadName:{}", event.getEventType(), event.getBulkheadName()))
                .onCallFinished(event -> log.info("---------- BulkheadEvent:{}  BulkheadName:{}", event.getEventType(), event.getBulkheadName()));
    }


    static int[] container = new int[100];
    // 模擬一定概率的不釋放資源
    public static boolean releasePermission() {
        if (container[0] != 1) {
            for (int i = 0; i < 70; i++) {
                container[i] = 1;
            }
            for (int i = 70; i < 100; i++) {
                container[i] = 0;
            }
        }
        int index = (int) (Math.random() * 100);
        return container[index] == 1;
    }

Resilience4jTest測試類

private BulkheadRegistry bulkheadRegistry;private String PATH_200 = "/api/pancake/v1/yee/query";

    private String PATH_400 = "/api/hk/card/v1/er/query";

    private String PATH_408 = "/api/pancake/v1/coin/query";

    private String PATH_500 = "/api/hk/card/v1/card/query";


    @Before
    public void setup() {
        HttpClient httpClient = HttpClient.create().wiretap(true);
        testClient = WebTestClient.bindToServer(new ReactorClientHttpConnector(httpClient))
                .baseUrl("http://localhost:8080")
                .responseTimeout(Duration.ofDays(1))
                .build();

        bulkheadRegistry = new InMemoryBulkheadRegistry();

        stubFor(post(urlMatching(PATH_200))
                .willReturn(okJson("{}")));

        stubFor(post(urlMatching(PATH_400))
                .willReturn(badRequest()));

        stubFor(post(urlMatching(PATH_408))
                .willReturn(okJson("{\"message\":\"time out\"}").withFixedDelay(1000 * 2)));

        stubFor(post(urlMatching(PATH_500))
                .willReturn(serverError()));
    }


    @Test
    public void When_Test_CircuitBreaker_With_Bulkhead_Expect_Hit_BulkheadFullException() {
        AtomicInteger count = new AtomicInteger();
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        Bulkhead bulkhead1 = bulkheadRegistry.bulkhead("bulkhead1",
                BulkheadConfig
                        .custom()
                        .maxConcurrentCalls(20)
                        .maxWaitDuration(Duration.ofMillis(100))
                        .build());

        Resilience4jTestHelper.bulkheadEventListener(bulkhead1);
        for (int i = 0; i < 100; i++) {
            if (bulkhead1.tryAcquirePermission()) {
                log.info(">>>>>>>>>> acquire permission {}", count.incrementAndGet());
                Future<String> futureStr = executorService.submit(() -> Resilience4jTestHelper.responseToBulkhead(bulkhead1, testClient, PATH_200));
                Try.of(futureStr::get).andThen(val -> log.info(">>>>>>>>>> success {}: {}", count.get(), val)).recover(throwable -> {
                    if (throwable instanceof ExecutionException) {
                        Throwable cause = (ExecutionException) throwable.getCause();
                        if (cause instanceof BulkheadFullException) {
                            log.info(">>>>>>>>>> BulkheadFullException {}: {}", count.get(), throwable.getMessage());
                        } else {
                            log.info(">>>>>>>>>> ExecutionException {}: {}", count.get(), throwable.getMessage());
                        }
                    }
                    return "hit ExecutionException";
                });
                if (releasePermission()) {
                    bulkhead1.onComplete();
                    log.info("---------- release permission");
                }
                Resilience4jTestHelper.getBulkheadStatus(")))))))))) ", bulkhead1);
            } else {
                log.info(">>>>>>>>>> tryAcquirePermission false {}", count.incrementAndGet());
                continue;
            }
        }
        executorService.shutdown();
    }

1.3 RateLimiter

Resilience4j提供了一個RateLimiter作爲限速器,Ratelimiter限制了服務被調用的次數,每隔一段時間重置該次數,服務在超出等待時間之後返回異常或者fallback方法。跟CircuitBreaker的代碼結構一樣,核心類有RateLimiterRegistry和其實現類InMemoryRateLimiterRegistry,RateLimiterConfig 還有RateLimiter

其中RateLimiterConfig的屬性如下表

配置屬性 默認值 描述
timeoutDuration 5 [s] 線程等待權限的默認等待時間
limitRefreshPeriod 500 [ns] 限制刷新的週期。在每個週期之後,速率限制器將其權限計數設置回 limitForPeriod 值
limitForPeriod 50 一個限制刷新期間可用的權限數

所以如果你想限制某個方法的調用率不高於1000 req/s,可以做如下配置

RateLimiterConfig.custom()
     .timeoutDuration(Duration.ofMillis(1000*5))
     .limitRefreshPeriod(Duration.ofSeconds(1))
     .limitForPeriod(1000)
     .build());

1.3.1 RateLimiter Demo

Resilience4jTestHelper測試輔助類

    /**
     * get the RateLimiter status and metrics
     * * @param prefixName
     *
     * @param rateLimiter
     */
    public static void getRateLimiterStatus(String prefixName, RateLimiter rateLimiter) {
        RateLimiter.Metrics metrics = rateLimiter.getMetrics();
        int availablePermissions = metrics.getAvailablePermissions();
        int waitingThreads = metrics.getNumberOfWaitingThreads();
        log.info(prefixName + "rateLimiter metrics[ availablePermissions=" + availablePermissions +
                ", waitingThreads=" + waitingThreads + " ]"
        );
    }

    public static void rateLimiterEventListener(RateLimiter rateLimiter) {
        rateLimiter.getEventPublisher()
                .onSuccess(event -> log.info("---------- rateLimiter success:{}", event))
                .onFailure(event -> log.info("---------- rateLimiter failure:{}", event));
    }

    public static String responseToRateLimiter(RateLimiter rateLimiter,WebTestClient testClient, String path) {
        WebTestClient.ResponseSpec responseSpec = testClient.post().uri(path).exchange();
        try {
            responseSpec.expectStatus().is4xxClientError();
            rateLimiter.onError(new RuntimeException("<<<<< hit 4XX >>>>>"));
            throw new RuntimeException("<<<<< hit 4XX >>>>>");
        } catch (Throwable error) {
        }

        try {
            responseSpec.expectStatus().is5xxServerError();
            rateLimiter.onError(new RuntimeException("<<<<< hit 5XX >>>>>"));
            throw new RuntimeException("<<<<< hit 5XX >>>>>");
        } catch (Throwable error) {
        }
        responseSpec.expectStatus().is2xxSuccessful();
        rateLimiter.onSuccess();
        return "hit 200";
    }

Resilience4jTest測試類

    private RateLimiterRegistry rateLimiterRegistry;
    private RateLimiter rateLimiter;
    
        rateLimiterRegistry = new InMemoryRateLimiterRegistry();
        rateLimiter = rateLimiterRegistry.rateLimiter("resilience4jTest",
                RateLimiterConfig
                        .custom()
                        .timeoutDuration(Duration.ofMillis(100))
                        .limitRefreshPeriod(Duration.ofSeconds(1))
                        .limitForPeriod(20)
                        .build());
        Resilience4jTestHelper.rateLimiterEventListener(rateLimiter);
        
    @Test
    public void When_Test_CircuitBreaker_Expect_Hit_RateLimiter() throws Exception {
        AtomicInteger count = new AtomicInteger();
        ExecutorService executorService = Executors.newFixedThreadPool(50);
        String path = expectError() ? PATH_500 : PATH_200;
        for (int i = 0; i < 100; i++) {
            Future<String> futureStr = executorService.submit(() -> Resilience4jTestHelper.responseToRateLimiter(rateLimiter, testClient, path));
            try {
                Future<String> stringFuture = rateLimiter.executeCallable(() -> futureStr);
                Try.of(stringFuture::get).andThen(val -> {
                    log.info(">>>>>>>>>> success {}: {}", count.incrementAndGet(), val);
                }).recover(throwable -> {
                    log.info(">>>>>>>>>> exception {}: {}", count.incrementAndGet(), throwable.getMessage());
                    return "hit fallback";
                });
                Resilience4jTestHelper.getRateLimiterStatus(")))))))))) ", rateLimiter);
            } catch (RequestNotPermitted exception){
                assertEquals("RateLimiter 'resilience4jTest' does not permit further calls" , exception.getMessage());
            }
        }
        executorService.shutdown();
    }

1.4 Retry

Retry在服務調用返回失敗時提供了額外嘗試調用的功能,其中RetryConfig的屬性如下表

配置屬性 默認值 描述
maxAttempts 3 最大嘗試次數(包括首次調用作爲第一次嘗試)
waitDuration 500 [ms] 兩次重試的時間間隔
intervalFunction numOfAttempts -> waitDuration 自定義的IntervalFunction,可以根據當前嘗試的次數動態的修改重試的時間間隔
intervalBiFunction (numOfAttempts, Either<throwable, result>) -> waitDuration 根據嘗試次數和結果或異常修改失敗後等待間隔的函數。與 intervalFunction 一起使用時會拋出 IllegalStateException。
retryOnResultPredicate result -> false 自定義的Predicate,根據服務返回的結果判斷是否應該重試。如果需要重試Predicate應返回true,否則返回false
retryExceptionPredicate throwable -> true 自定義的Predicate,根據服務返回的異常判斷是否應該重試。如果需要重試Predicate應返回true,否則返回false
retryExceptions empty 異常列表,遇到列表中的異常或其子類則重試
注意:如果您使用 Checked Exceptions,則必須使用 CheckedSupplier
ignoreExceptions empty 異常列表,遇到列表中的異常或其子類則不重試。此參數支持子類型。
failAfterMaxRetries false 當重試達到配置的 maxAttempts 並且結果仍未通過 retryOnResultPredicate 時啓用或禁用拋出 MaxRetriesExceededException 的布爾值

1.4.1 Retry Demo

Resilience4jTestHelper測試輔助類

    /**
     * get the Retry status and metrics
     * * @param prefixName
     *
     * @param retry
     */
    public static void getRetryStatus(String prefixName, Retry retry) {

        Retry.Metrics metrics = retry.getMetrics();
        long successfulCallsWithRetryAttempt = metrics.getNumberOfSuccessfulCallsWithRetryAttempt();
        long successfulCallsWithoutRetryAttempt = metrics.getNumberOfSuccessfulCallsWithoutRetryAttempt();
        long failedCallsWithRetryAttempt = metrics.getNumberOfFailedCallsWithRetryAttempt();
        long failedCallsWithoutRetryAttempt = metrics.getNumberOfFailedCallsWithoutRetryAttempt();

        log.info(prefixName + " -> retry metrics[ successfulCallsWithRetry=" + successfulCallsWithRetryAttempt +
                ", successfulCallsWithoutRetry=" + successfulCallsWithoutRetryAttempt +
                ", failedCallsWithRetry=" + failedCallsWithRetryAttempt +
                ", failedCallsWithoutRetry=" + failedCallsWithoutRetryAttempt +
                " ]"
        );
    }

    public static void retryEventListener(Retry retry) {
        retry.getEventPublisher()
                .onSuccess(event -> log.info("))))))))))) retry service success:{}", event))
                .onError(event -> {
                    log.info("))))))))))) retry service failed:{}", event);
                    Throwable exception = event.getLastThrowable();
                    if (exception instanceof TimeoutException) {
                        // TODO
                    }
                })
                .onIgnoredError(event -> log.info("))))))))))) retry service failed and ignore:{}", event))
                .onRetry(event -> log.info("))))))))))) retry call service: {}", event.getNumberOfRetryAttempts()));

    }
    
    public static String responseToRetry(Retry retry, WebTestClient testClient, String path) {
        WebTestClient.ResponseSpec responseSpec = testClient.post().uri(path).exchange();
        try {
            responseSpec.expectStatus().is4xxClientError();
            return "HIT_ERROR_4XX";
        } catch (Throwable error) {
        }
        try {
            responseSpec.expectStatus().is5xxServerError();
            return "HIT_ERROR_5XX";
        } catch (Throwable error) {
        }
        responseSpec.expectStatus().is2xxSuccessful();
        return "HIT_200";
    }

Resilience4jTest測試類

    private RetryRegistry retryRegistry;
    private Retry retry;
    retryRegistry = new InMemoryRetryRegistry();
    retry = retryRegistry.retry("resilience4jTest",
            RetryConfig
                    .custom()
                    .maxAttempts(5)
                    .waitDuration(Duration.ofMillis(500))
                    .retryOnResult(val -> val.toString().contains("HIT_ERROR_"))
 //                     .retryExceptions(RuntimeException.class)
                    .build());

    Resilience4jTestHelper.retryEventListener(retry);
   @Test
    public void When_Test_CircuitBreaker_Expect_Retry() {
        AtomicInteger count = new AtomicInteger();
        for (int i = 0; i < 30; i++) {
            String path = expectError() ? PATH_200 : PATH_400;
            Callable<String> response = Retry.decorateCallable(retry, () -> Resilience4jTestHelper.responseToRetry(retry, testClient, path));
            Try.of(response::call).andThen(val -> log.info(">>>>>>>>>> result {}: {}", count.incrementAndGet(), val));
            Resilience4jTestHelper.getRetryStatus("))))))))))", retry);
        }
    }

1.5 TimeLimiter

TimeLImiter超時控制,和CircuitBreaker的slowCall相似,只是CircuitBreaker的slowCall觸發了超時只是將超時記錄在Metrics中不會拋出異常,而TimeLimiter觸發了超時會直接拋出異常。

而且TimeLimiter配置類很簡單

配置屬性 默認值 描述
timeoutDuration 5 [s] 超時時間,默認1s
cancelRunningFuture TRUE 當觸發超時時是否取消運行中的Future

1.5.1 TimeLimiter Demo

Resilience4jTestHelper測試輔助類

    public static void timeLimiterEventListener(TimeLimiter timeLimiter) {
        timeLimiter.getEventPublisher()
                .onSuccess(event -> log.info("---------- timeLimiter success:{}", event))
                .onError(event -> log.info("---------- timeLimiter error:{}", event))
                .onTimeout(event -> log.info("---------- rateLimiter timeout:{}", event));
    }

    public static String responseToTimeLimiter(TimeLimiter timeLimiter, CircuitBreaker circuitBreaker, WebTestClient testClient, String path) {
        WebTestClient.ResponseSpec responseSpec = testClient.post().uri(path).exchange();
        try {
            responseSpec.expectStatus().is4xxClientError();
            circuitBreaker.onError(0, TimeUnit.MILLISECONDS, new RuntimeException("<<<<< hit 4XX >>>>>"));
            timeLimiter.onError(new RuntimeException("<<<<< hit 4XX >>>>>"));
            throw new RuntimeException("<<<<< hit 4XX >>>>>");
        } catch (Throwable error) {
        }

        try {
            responseSpec.expectStatus().is5xxServerError();
            circuitBreaker.onError(0, TimeUnit.MILLISECONDS, new RuntimeException("<<<<< hit 5XX >>>>>"));
            timeLimiter.onError(new RuntimeException("<<<<< hit 5XX >>>>>"));
            throw new RuntimeException("<<<<< hit 5XX >>>>>");
        } catch (Throwable error) {
        }
        responseSpec.expectStatus().is2xxSuccessful();
        timeLimiter.onSuccess();
        return "hit 200";
    }

Resilience4jTest測試類

    private TimeLimiterRegistry timeLimiterRegistry;
    private TimeLimiter timeLimiter;
    
    timeLimiterRegistry = new InMemoryTimeLimiterRegistry();
    timeLimiter = timeLimiterRegistry.timeLimiter("resilience4jTest",
            TimeLimiterConfig
                    .custom()
                    .timeoutDuration(Duration.ofMillis(1000 * 1))
                    .cancelRunningFuture(true)
                    .build());
                    
    Resilience4jTestHelper.timeLimiterEventListener(timeLimiter);
    
    @Test
    public void When_Test_CircuitBreaker_Expect_Timeout() {
        AtomicInteger count = new AtomicInteger();
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 30; i++) {
            String path = expectError() ? PATH_408 : PATH_200;
            Future<String> futureStr =
                    executorService.submit(() -> Resilience4jTestHelper.responseToTimeLimiter(timeLimiter, circuitBreaker, testClient, path));
            Callable<String> stringCallable = timeLimiter.decorateFutureSupplier(() -> futureStr);
            Callable<String> response = circuitBreaker.decorateCallable(stringCallable);
            Try.of(response::call).andThen(val -> log.info(">>>>>>>>>> success {} {}", count.incrementAndGet(), val))
                .recover(CallNotPermittedException.class, throwable -> {
                Resilience4jTestHelper.getCircuitBreakerStatus(">>>>>>>>>> open CircuitBreaker " + count.incrementAndGet(), circuitBreaker);
                return "hit CircuitBreaker";
            }).recover(throwable -> {
                Resilience4jTestHelper.getCircuitBreakerStatus(">>>>>>>>>> call fallback " + count.incrementAndGet(), circuitBreaker);
                log.error(">>>>>>>>>> fallback:{}", throwable.getMessage());
                return "hit Fallback";
            });
        }
    }

到此Resilience4j組件的基本用法介紹完畢,上面的測試代碼我沒有截圖測試的結果,附上代碼地址各位看官可以在本地跑跑測試代碼

代碼地址:https://github.com/showkawa/springBoot_2017/tree/master/spb-demo/spb-gateway/src/test/java/com/kawa/spbgateway/circuitbreaker/resilience4j

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章