Java:Resilience4j CircuitBreaker入門指南

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

目錄

介紹

基於計數的滑動窗口

基於時間的滑動窗口

失敗率和慢調用率閾值

創建CircuitBreakerRegistry

創建和配置CircuitBreaker

裝配和執行函數式接口

消費發出的RegistryEvent

消費發出的CircuitBreakerEvent


介紹

CircuitBreaker是由一個有限狀態機實現的,其中包含三個一般性狀態:CLOSED, OPEN, HALF_OPEN(關閉、打開、半開)和兩個特定狀態:DISABLED, FORCED_OPEN(禁用、強開)。


CircuitBreaker使用滑動窗口(sliding window)存儲和統計調用的結果。你能在基於計數的滑動窗口(count-based sliding window)和基於時間的滑動窗口(time-based sliding window)中作選擇。基於計數的滑動窗口統計最近N次調用的結果。基於時間的滑動窗口統計最近N秒的調用結果。

基於計數的滑動窗口

基於計數的滑動窗口是由包含N個測量單元的循環數組實現的。若計數窗口的大小爲10,循環數組總會有10個測量單元。滑動窗口會增量更新總統計值。當新的調用結果被記錄後,總統計值就會被更新。當最老的測量單元被排除,這個測量單元就會從總統計值中減掉,佔用的桶就會被重置。(排除後減掉(Subtract-on-Evict))

獲取快照的時間恆定爲 O(1),因爲快照是已統計好的,並獨立於窗口大小。這種實現的空間需求(內存消耗)是O(n)。

基於時間的滑動窗口

基於時間的滑動窗口是由N個部分統計單元(桶)的循環數組實現的。
若滑動窗口大小爲10秒,循環數組含有10個部分統計單元(桶)。每個桶統計一個指定秒內的所有調用的結果(部分統計)。循環數組的頭部桶存儲當前秒內所有調用的結果。其它部分統計單元存儲以前秒內的調用結果。
滑動窗口不會單獨存儲所有調用結果,但會增量更新部分統計單元(桶)中的值和總統計值。
當新的調用結果被記錄後,總統計值會增量更新。當最老的桶被排除,這個桶中的部分統計值會從總統計值中減掉,這個桶也會被重置。(排除後減掉(Subtract-on-Evict))

獲取快照的時間恆定爲 O(1),因爲快照是已統計好的,並獨立於窗口大小。因爲調用結果不會各自單獨存儲,所以這種實現的空間需求(內存消耗)接近O(n)。是需要創建N個部分統計單元和一個總統計單元。
爲了給失敗調用次數、慢調用次數和總調用次數計數,部分統計單元是由3個integer組成。並且一個long字段存儲總調用時長。

失敗率和慢調用率閾值

當失敗率等於或大於閾值時,CircuitBreaker的狀態由CLOSED轉爲OPEN。比如,當大於50%被記錄的調用失敗了。

所有的異常默認都會記爲失敗。你可以定義一個可以當作失敗的異常列表。除此以外的其它異常,除非被忽略,不然都會被當作成功。異常也可以被忽略,這樣它既不被當作失敗也不被當作成功。

當慢調用的比例等於或大於閾值時,CircuitBreaker的狀態由CLOSED轉爲OPEN。比如,當大於50%被記錄的調用花的時間大於5秒。這將有助於在外部系統變得無響應鬮,降低它的負載。

只有當記錄到的調用次數達到一定的最小數量時,失敗率和慢調用率才能被計算出來。比如,若要求調用的最小次數是10,那最少必須記錄到10次調用,失敗率才能被計算出來。若只評估了9次調用,哪怕這9次調用都失敗,CircuitBreaker也不會打開。

當CircuitBreaker狀態爲OPEN時,調用將被拒絕,並將異常CallNotPermittedException返回給調用者。當經過一定的等待時間後,CircuitBreaker狀態會從OPEN變爲HALF_OPEN,並允許一定數量的調用去執行,以查看後端是仍然不可用還是已再次可用。除非所有被允許的調用都完成,不然其後的調用都會被拒絕並返回異常CallNotPermittedException。

若失敗率和慢調用率仍大於等於配置的閾值,狀態會改回OPEN。若失敗率和慢調用率低於閾值,狀態會改回CLOSED。

CircuitBreaker還支持兩種特殊的狀態:DISABLED(總是允許訪問), FORCED_OPEN(總是拒絕訪問)。處於這兩種狀態時,沒有CircuitBreaker事件(除了狀態轉換)會產生,也不會記錄任何度量值。從這兩種狀態退出的唯一辦法就是觸發狀態轉換或重置CircuitBreaker。

CircuitBreaker是線程安全的:

  • CircuitBreaker的狀態存放在AtomicReference中。
  • CircuitBreaker使用原子操作更新狀態,不會有負作用。
  • 記錄調用和從時間窗口中讀取快照都是同步處理的。

這意味着保證了原子性,在同一時刻只有一個線程可以更新狀態或滑動窗口。

但CircuitBreaker不會同步函數調用。也就是說函數調用本身不是臨界區的一個部分。否則CircuitBreaker會帶來巨大的性能下降,造成性能瓶頸。慢函數調用會對整個系統的性能/呑吐量造成巨大的負面影響。

若20個併發線程請求執行一個函數,並且CircuitBreaker狀態爲關閉,所有線程都會被允許執行這個函數。即使滑動窗口的大小爲15,並不意味着只能有15個調用併發執行。如果你想限制併發線程數,請使用Bulkhead。你可將Bulkhead與CircuitBreaker一起使用。

1個線程和例子

3個線程的例子

創建CircuitBreakerRegistry

Resilience4j會在內存中帶一個CircuitBreakerRegistry,這個CircuitBreakerRegistry基於ConcurrentHashMap,提供了線程安全性和原子性保證。你能使用CircuitBreakerRegistry管理(創建和獲取)CircuitBreaker實例。可以使用一個全局默認CircuitBreakerConfig爲所有CircuitBreaker實例創建一個CircuitBreakerRegistry。

CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();

創建和配置CircuitBreaker

你可以創建自己定製的全局CircuitBreakerConfig。爲了創建一個全局的CircuitBreakerConfig,可以使用CircuitBreakerConfig構建器。可以使用構建器配置如下屬性:

配置屬性 默認值 描述
failureRateThreshold 50 以百分率形式配置失敗率閾值。失敗率大於等於閾值時,CircuitBreaker轉變爲打開狀態,並使調用短路。
slowCallRateThreshold 100 以百分率形式配置慢調用率閾值。當調用執行的時長大於slowCallDurationThreshold時,CircuitBreaker會認爲調用爲慢調用。當慢調用佔比大於等於此閾值時,CircuitBreaker轉變爲打開狀態,並使調用短路。
slowCallDurationThreshold 60000 [ms] 配置調用執行的時長閾值。當超過這個閾值時,調用會被認爲是慢調用,並增加慢調用率。
permittedNumberOfCallsInHalfOpenState 10 當CircuitBreaker是半開狀態時,配置被允許的調用次數。
slidingWindowType

COUNT_BASED

/TIME_BASED

配置滑動窗口類型。當CircuitBreaker關閉時,這種類型的滑動窗口會記錄調用結果。滑動窗口要麼是基於計數的,要麼是基於時間的。若滑動窗口爲COUNT_BASED,則最近slidingWindowSize次的調用會被記錄和統計。若滑動窗口爲TIME_BASED,則最近slidingWindowSize秒中的調用會被記錄和統計。
slidingWindowSize 100 配置滑動窗口的大小。當CircuitBreaker關閉後用於記錄調用結果。
minimumNumberOfCalls 10 配置最小調用次數。在CircuitBreaker計算錯誤率前,要求(在每滑動窗口週期)用到這個值。例如,若minimumNumberOfCalls是10,爲計算失敗率,則最小要記錄10個調用。若只記錄了9個調用,即使9個都失敗,CircuitBreaker也不會打開。
waitDurationInOpenState 60000 [ms] CircuitBreaker狀態從打開轉化爲半開時,需要等待的時長。
automaticTransitionFromOpenToHalfOpenEnabled false 如果爲true,則CircuitBreaker會自動從打開狀態轉化爲半開狀態。不需要另外的調用來觸發這種轉換。
recordExceptions empty 這個異常列表用來存放被當作失敗的異常,這些異常發生時會增加失敗率。任何異常,只要不是在ignoreExceptions中被明確忽略的,如果匹配或繼承自異常列表中的異常,都會當作失敗。若設置了異常列表,不在異常列表中的異常,只要不在ignoreExceptions中存在,都會當作成功。
ignoreExceptions empty 這個異常列表用來存放可忽略的異常,這些異常即不當作成功也不當作失敗。任何異常,只要匹配或繼承自此異常列表中的異常,都不會當作成功或失敗,即使recordExceptions中存在這個異常。
recordException

throwable -> true

所有異常默認都當作失敗。

這是一個定製的Predicate,會評估是否把異常當作失敗。除非異常在ignoreExceptions中存在,不然,只有Predicate必須返回true時,才把異常當作失敗;只有Predicate必須返回false時,才把異常當作成功。
ignoreException

throwable -> false

所有異常默認都不會被忽略。

這是一個定製的Predicate,會評估異常是否應該被忽略,而不是把它當作失敗或成功。如果應忽略此異常,則Predicate必須返回true。如果把異常當作失敗,則Predicate必須返回false。
// Create a custom configuration for a CircuitBreaker
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
  .failureRateThreshold(50)
  .slowCallRateThreshold(50)
  .waitDurationInOpenState(Duration.ofMillis(1000))
  .slowCallDurationThreshold(Duration.ofSeconds(2))
  .permittedNumberOfCallsInHalfOpenState(3)
  .minimumNumberOfCalls(10)
  .slidingWindowType(SlidingWindowType.TIME_BASED)
  .slidingWindowSize(5)
  .recordException(e -> INTERNAL_SERVER_ERROR
                 .equals(getResponse().getStatus()))
  .recordExceptions(IOException.class, TimeoutException.class)
  .ignoreExceptions(BusinessException.class, OtherBusinessException.class)
  .build();

// Create a CircuitBreakerRegistry with a custom global configuration
CircuitBreakerRegistry circuitBreakerRegistry 
  CircuitBreakerRegistry.of(circuitBreakerConfig);

// Get or create a CircuitBreaker from the CircuitBreakerRegistry 
// with the global default configuration
CircuitBreaker circuitBreakerWithDefaultConfig = 
  circuitBreakerRegistry.circuitBreaker("name1");

// Get or create a CircuitBreaker from the CircuitBreakerRegistry 
// with a custom configuration
CircuitBreaker circuitBreakerWithCustomConfig = circuitBreakerRegistry
  .circuitBreaker("name2", circuitBreakerConfig);

可以添加被多個CircuitBreaker實例共享的配置。

CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
  .failureRateThreshold(70)
  .build();

circuitBreakerRegistry.addConfiguration("someSharedConfig", config);

CircuitBreaker circuitBreaker = circuitBreakerRegistry
  .circuitBreaker("name", "someSharedConfig");

也可以重寫配置。

 CircuitBreakerConfig defaultConfig = circuitBreakerRegistry
   .getDefaultConfig();

CircuitBreakerConfig overwrittenConfig = CircuitBreakerConfig
  .from(defaultConfig)
  .waitDurationInOpenState(Duration.ofSeconds(20))
  .build();

如果不想使用CircuitBreakerRegistry管理CircuitBreaker實例,可以直接創建它。

// Create a custom configuration for a CircuitBreaker
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
  .recordExceptions(IOException.class, TimeoutException.class)
  .ignoreExceptions(BusinessException.class, OtherBusinessException.class)
  .build();

CircuitBreaker customCircuitBreaker = CircuitBreaker
  .of("testName", circuitBreakerConfig);

裝配和執行函數式接口

可以在CircuitBreaker上裝配做任意Callable, Supplier, Runnable, Consumer, CheckedRunnable, CheckedSupplier, CheckedConsumer, CompletionStage

可以使用來自Vavr的Try.of(…​)、Try.run(…​)調用裝飾函數。這樣就可以使用map, flatMap, filter, recover, andThen將更多的函數串起來。這些串起來的函數只有在CircuitBreaker的狀態爲CLOSED或HALF_OPEN時,才能被調用。

在下面的例子中,如果函數調用成功,Try.of(…​)返回Success<String>。如果函數拋出異常,Failure<Throwable>被返回,並且不會調用map。

// Given
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");

// When I decorate my function
CheckedFunction0<String> decoratedSupplier = CircuitBreaker
        .decorateCheckedSupplier(circuitBreaker, () -> "This can be any method which returns: 'Hello");

// and chain an other function with map
Try<String> result = Try.of(decoratedSupplier)
                .map(value -> value + " world'");

// Then the Try Monad returns a Success<String>, if all functions ran successfully.
assertThat(result.isSuccess()).isTrue();
assertThat(result.get()).isEqualTo("This can be any method which returns: 'Hello world'");

消費發出的RegistryEvent

在CircuitBreakerRegistry上註冊事件消費者,這樣在CircuitBreaker被創建、替代、刪除時可以執行一些操作。

CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();
circuitBreakerRegistry.getEventPublisher()
  .onEntryAdded(entryAddedEvent -> {
    CircuitBreaker addedCircuitBreaker = entryAddedEvent.getAddedEntry();
    LOG.info("CircuitBreaker {} added", addedCircuitBreaker.getName());
  })
  .onEntryRemoved(entryRemovedEvent -> {
    CircuitBreaker removedCircuitBreaker = entryRemovedEvent.getRemovedEntry();
    LOG.info("CircuitBreaker {} removed", removedCircuitBreaker.getName());
  });

消費發出的CircuitBreakerEvent

CircuitBreakerEvent可以是狀態轉換、斷路器重置、成功的調用、已記錄的錯誤、已忽略的錯誤等事件。所有的事件都包含事件創建時間、調用處理時長等額外信息。若要消費事件,可以註冊一個事件消費者。

circuitBreaker.getEventPublisher()
    .onSuccess(event -> logger.info(...))
    .onError(event -> logger.info(...))
    .onIgnoredError(event -> logger.info(...))
    .onReset(event -> logger.info(...))
    .onStateTransition(event -> logger.info(...));
// Or if you want to register a consumer listening
// to all events, you can do:
circuitBreaker.getEventPublisher()
    .onEvent(event -> logger.info(...));

也可以使用CircularEventConsumer將事件存儲在固定容量的環形緩存中。

CircularEventConsumer<CircuitBreakerEvent> ringBuffer = 
  new CircularEventConsumer<>(10);
circuitBreaker.getEventPublisher().onEvent(ringBuffer);
List<CircuitBreakerEvent> bufferedEvents = ringBuffer.getBufferedEvents()

可以使用RxJava或RxJava2選配器將EventPublisher轉化爲Reactive Stream。

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