原文:https://resilience4j.readme.io/docs/circuitbreaker
目錄
介紹
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。