公司的閘道器(基於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個 -> CircuitBreaker,Bulkhead,RateLimiter,Retry 和 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元件的基本用法介紹完畢,上面的測試程式碼我沒有截圖測試的結果,附上程式碼地址各位看官可以在本地跑跑測試程式碼