高可用框架Resilience4j使用指南

介紹

Hystrix停更之後,Netflix官方推薦移步至resilience4j,它是一個輕量、易用、可組裝的高可用框架,支持熔斷高頻控制隔離限流限時重試等多種高可用機制。

 

與Hystrix相比,它有以下一些主要的區別:

  1. Hystrix調用必須被封裝到HystrixCommand裏,而resilience4j以裝飾器的方式提供對函數式接口、lambda表達式等的嵌套裝飾,因此你可以用簡潔的方式組合多種高可用機制
  2. Hystrix的頻次統計採用滑動窗口的方式,而resilience4j採用環狀緩衝區的方式
  3. 關於熔斷器在半開狀態時的狀態轉換,Hystrix僅使用一次執行判定是否進行狀態轉換,而resilience4j則採用可配置的執行次數與閾值,來決定是否進行狀態轉換,這種方式提高了熔斷機制的穩定性
  4. 關於隔離機制,Hystrix提供基於線程池和信號量的隔離,而resilience4j只提供基於信號量的隔離

 

下面依次介紹各類高可用機制的使用方式【選譯官方使用文檔】。

 

熔斷 CircuitBreaker

初始化熔斷器

CircuitBreakerRegistry負責創建和管理熔斷器實例CircuitBreaker,它是線程安全的,提供原子性操作。

CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();

該方式使用默認的全局配置CircuitBreakerConfig創建熔斷器實例,你也可以選擇使用定製化的配置,可選項有:

  1. 觸發熔斷的失敗率閾值
  2. 熔斷器從打開狀態到半開狀態的等待時間
  3. 熔斷器在半開狀態時環狀緩衝區的大小
  4. 熔斷器在關閉狀態時環狀緩衝區的大小
  5. 處理熔斷器事件的定製監聽器CircuitBreakerEventListener
  6. 評估異常是否被記錄爲失敗事件的定製謂詞函數Predicate
// 創建定製化熔斷器配置
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .waitDurationInOpenState(Duration.ofMillis(1000))
    .ringBufferSizeInHalfOpenState(2)
    .ringBufferSizeInClosedState(2)
    .build();

// 使用定製化配置創建熔斷器註冊中心
CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(circuitBreakerConfig);

// 從註冊中心獲取使用默認配置的熔斷器
CircuitBreaker circuitBreaker2 = circuitBreakerRegistry.circuitBreaker("otherName");

// 從註冊中心獲取使用定製化配置的熔斷器
CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("uniqueName", circuitBreakerConfig);

你也可以選擇不經過註冊中心,直接創建熔斷器實例:

CircuitBreaker defaultCircuitBreaker = CircuitBreaker.ofDefaults("testName");

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

 

熔斷器使用方式

熔斷器採用裝飾器模式,你可以使用CircuitBreaker.decorateCheckedSupplier() / CircuitBreaker.decorateCheckedRunnable() / CircuitBreaker.decorateCheckedFunction() 裝飾 Supplier / Runnable / Function / CheckedRunnable / CheckedFunction,然後使用Vavr的Try.of(…​) / Try.run(…​) 調用被裝飾的函數,也可以使用map / flatMap / filter / recover / andThen鏈接更多的函數。函數鏈只有在熔斷器處於關閉或半開狀態時纔可以被調用。

// 創建熔斷器
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");

// 用熔斷器包裝函數
CheckedFunction0<String> decoratedSupplier = CircuitBreaker
        .decorateCheckedSupplier(circuitBreaker, () -> "This can be any method which returns: 'Hello");

// 鏈接其它的函數
Try<String> result = Try.of(decoratedSupplier)
                .map(value -> value + " world'");

// 如果函數鏈中的所有函數均調用成功,最終結果爲Success<String>
assertThat(result.isSuccess()).isTrue();
assertThat(result.get()).isEqualTo("This can be any method which returns: 'Hello world'");

 

函數鏈中可以包含被不同熔斷器包裝的多個函數:

// 兩個熔斷器
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
CircuitBreaker anotherCircuitBreaker = CircuitBreaker.ofDefaults("anotherTestName");

// 用兩個熔斷器分別包裝Supplier 和 Function
CheckedFunction0<String> decoratedSupplier = CircuitBreaker
        .decorateCheckedSupplier(circuitBreaker, () -> "Hello");

CheckedFunction1<String, String> decoratedFunction = CircuitBreaker
        .decorateCheckedFunction(anotherCircuitBreaker, (input) -> input + " world");

// 鏈接函數
Try<String> result = Try.of(decoratedSupplier)
        .mapTry(decoratedFunction::apply);

assertThat(result.isSuccess()).isTrue();
assertThat(result.get()).isEqualTo("Hello world");

 

下面我們模擬熔斷器被觸發的情況,在熔斷器打開的狀態,Try.of 返回 Failure<Throwable>,鏈接函數將不會被調用:

// 創建一個環狀緩衝區大小爲2的熔斷器
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
        .ringBufferSizeInClosedState(2)
        .waitDurationInOpenState(Duration.ofMillis(1000))
        .build();
CircuitBreaker circuitBreaker = CircuitBreaker.of("testName", circuitBreakerConfig);

// 模擬一次失敗調用
circuitBreaker.onError(0, new RuntimeException());
// 沒有觸發熔斷,熔斷器仍處於關閉狀態
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED);
// 模擬第二次失敗調用
circuitBreaker.onError(0, new RuntimeException());
// 失敗率超過百分之五十,熔斷器被觸發
assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);

// 由於熔斷器處於打開狀態,調用失敗
Try<String> result = Try.of(CircuitBreaker.decorateCheckedSupplier(circuitBreaker, () -> "Hello"))
        .map(value -> value + " world");

assertThat(result.isFailure()).isTrue();
assertThat(result.failed().get()).isInstanceOf(CircuitBreakerOpenException.class);

 

熔斷器支持RxJava/Reactor,下面是簡單的示例:

CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendName");
Observable.fromCallable(backendService::doSomething)
.lift(CircuitBreakerOperator.of(circuitBreaker))

CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendName");
Mono.fromCallable(backendService::doSomething)
    .transform(CircuitBreakerOperator.of(circuitBreaker))

 

熔斷器重置

熔斷器支持重置,重置之後所有狀態數據清空,恢復初始狀態。

circuitBreaker.reset();

 

熔斷器降級

你可以爲熔斷函數設置降級措施,鏈接Try.recover(),當Try.of() 返回 Failure<Throwable>時,使用降級措施:

// 創建熔斷器
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");

// 模擬失敗調用,並鏈接降級函數
CheckedFunction0<String> checkedSupplier = CircuitBreaker.decorateCheckedSupplier(circuitBreaker, () -> {
 throw new RuntimeException("BAM!");
});
Try<String> result = Try.of(checkedSupplier)
        .recover(throwable -> "Hello Recovery");

// 降級函數被調用,最終調用結果爲成功
assertThat(result.isSuccess()).isTrue();
assertThat(result.get()).isEqualTo("Hello Recovery");

 

熔斷器失敗判定

默認所有異常都被認定爲失敗事件,你可以定製Predicate的test檢查,實現選擇性記錄,只有該函數返回爲true時異常才被認定爲失敗事件。下例展示瞭如何忽略除WebServiceException外的所有異常:

CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
        .ringBufferSizeInClosedState(2)
        .ringBufferSizeInHalfOpenState(2)
        .waitDurationInOpenState(Duration.ofMillis(1000))
        .recordFailure(throwable -> Match(throwable).of(
                Case($(instanceOf(WebServiceException.class)), true),
                Case($(), false)))
        .build();

 

監聽熔斷器事件

熔斷器事件CircuitBreakerEvent包含狀態轉換、重置、成功、失敗異常、忽略異常事件,所有事件包含發生時間、處理時長的信息。你可以使用如下方式註冊事件監聽器:

// 分類監聽事件
circuitBreaker.getEventPublisher()
    .onSuccess(event -> logger.info(...))
    .onError(event -> logger.info(...))
    .onIgnoredError(event -> logger.info(...))
    .onReset(event -> logger.info(...))
    .onStateTransition(event -> logger.info(...));
// 監聽所有事件
circuitBreaker.getEventPublisher()
    .onEvent(event -> logger.info(...));

你也可以使用CircularEventConsumer將事件存儲在緩存中:

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

你還可以將EventPublisher轉換爲RxJava/Reactor的事件流,該方法的優勢在於,可以進一步指定調度器進行異步化處理:

RxJava2Adapter.toFlowable(circuitBreaker.getEventPublisher())
    .filter(event -> event.getEventType() == Type.ERROR)
    .cast(CircuitBreakerOnErrorEvent.class)
    .subscribe(event -> logger.info(...))

 

熔斷器狀態

CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics();
float failureRate = metrics.getFailureRate();
int bufferedCalls = metrics.getNumberOfBufferedCalls();
int failedCalls = metrics.getNumberOfFailedCalls();

 

高頻控制 RateLimiter

初始化高頻控制器

高頻控制的配置方式與熔斷類似,有對應的RateLimiterRegistry 和 RateLimiterConfig,自定義配置的可選項有:

  1. 頻次閾值
  2. 閾值刷新時間
  3. 限流後的冷卻時間
// 創建高頻控制配置,控制頻率爲1QPS
RateLimiterConfig config = RateLimiterConfig.custom()
    .limitForPeriod(1)
    .limitRefreshPeriod(Duration.ofMillis(1000))
    .timeoutDuration(Duration.ofMillis(500))
    .build();

// 創建高頻控制註冊中心
RateLimiterRegistry rateLimiterRegistry = RateLimiterRegistry.of(config);

// 從註冊中心創建高頻控制器實例
RateLimiter rateLimiterWithDefaultConfig = rateLimiterRegistry.rateLimiter("backend");
RateLimiter rateLimiterWithCustomConfig = rateLimiterRegistry.rateLimiter("backend#2", config);

// 直接創建高頻控制器實例
RateLimiter rateLimiter = RateLimiter.of("NASDAQ :-)", config);

 

高頻控制器使用方式

// 使用上面定義的高頻控制器裝飾函數調用
CheckedRunnable restrictedCall = RateLimiter
    .decorateCheckedRunnable(rateLimiter, () -> System.out.println("Do something"));

// 第一次調用成功,第二次調用被高頻限制
Try.run(restrictedCall)
    .andThenTry(restrictedCall)
    .onFailure(throwable -> System.out.println("Wait before call it again :)"));

你可以在運行時動態修改高頻控制器配置,但新的冷卻時間不會影響當前處於冷卻狀態的線程,新的閾值也不會影響處於當前一輪控制的線程:

// 在下一輪控制中,閾值變更爲100
rateLimiter.changeLimitForPeriod(100);

與熔斷類似,高頻控制也支持RxJava/Reactor.

RateLimiter rateLimiter = RateLimiter.ofDefaults("backendName");
Observable.fromCallable(backendService::doSomething)
    .lift(RateLimiterOperator.of(rateLimiter));

RateLimiter rateLimiter = RateLimiter.ofDefaults("backendName");
Mono.fromCallable(backendService::doSomething)
    .transform(RateLimiterOperator.of(rateLimiter));

 

監聽高頻控制事件

高頻控制器事件RateLimiterEvent包含允許執行和拒絕執行事件,所有事件包含發生時間、相關高頻控制器名稱的信息。

rateLimiter.getEventPublisher()
    .onSuccess(event -> logger.info(...))
    .onFailure(event -> logger.info(...));

將EventPublisher轉換爲RxJava/Reactor的事件流:

RxJava2Adapter.toFlowable(rateLimiter.getEventPublisher())
    .filter(event -> event.getEventType() == FAILED_ACQUIRE)
    .subscribe(event -> logger.info(...))

 

高頻控制器狀態

RateLimiter limit;
RateLimiter.Metrics metrics = limit.getMetrics();
int numberOfThreadsWaitingForPermission = metrics.getNumberOfWaitingThreads();
int availablePermissions = metrics.getAvailablePermissions();

AtomicRateLimiter atomicLimiter;
long nanosToWaitForPermission = atomicLimiter.getNanosToWait();

 

隔離&流控 Bulkhead

介紹

Bulkhead意指船舶中的隔艙板,它將船體分割爲多個船艙,在船部分受損時可避免沉船。框架中的Bulkhead通過信號量的方式隔離不同種類的調用,並進行流控,這樣可以避免某類調用異常危及系統整體。(注:不同於Hystrix,該框架不提供基於線程池的隔離)

 

初始化Bulkhead

Bulkhead的配置方式與熔斷類似,有對應的BulkheadRegistry 和 BulkheadConfig,自定義配置的可選項有:

  1. 最大並行度
  2. 嘗試進入飽和態的Bulkhead時,線程的最大阻塞時間
// 創建自定義的Bulkhead配置
BulkheadConfig config = BulkheadConfig.custom()
                                      .maxConcurrentCalls(150)
                                      .maxWaitTime(100)
                                      .build();

// 創建Bulkhead註冊中心
BulkheadRegistry registry = BulkheadRegistry.of(config);

// 從註冊中心獲取默認配置的Bulkhead
Bulkhead bulkhead1 = registry.bulkhead("foo");

// 從註冊中心獲取自定義配置的Bulkhead
Bulkhead bulkhead2 = registry.bulkhead("bar", config);

你也可以不經過註冊中心,直接創建Bulkhead:

Bulkhead bulkhead1 = Bulkhead.ofDefaults("foo");
Bulkhead bulkhead2 = Bulkhead.of(
                         "bar",
                         BulkheadConfig.custom()
                                       .maxConcurrentCalls(50)
                                       .build()
                     );

 

Bulkhead使用方式

與熔斷類似,不再贅述,下面是代碼示例:

// 創建Bulkhead實例
Bulkhead bulkhead = Bulkhead.of("testName", config);

// 用Bulkhead包裝函數調用
CheckedFunction0<String> decoratedSupplier = Bulkhead.decorateCheckedSupplier(bulkhead, () -> "This can be any method which returns: 'Hello");

// 鏈接其它函數
Try<String> result = Try.of(decoratedSupplier)
                        .map(value -> value + " world'");

assertThat(result.isSuccess()).isTrue();
assertThat(result.get()).isEqualTo("This can be any method which returns: 'Hello world'");
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);

 

函數鏈中可以包含被不同Bulkhead或熔斷器包裝的多個函數:

// 兩個Bulkhead
Bulkhead bulkhead = Bulkhead.of("test", config);
Bulkhead anotherBulkhead = Bulkhead.of("testAnother", config);

// 用兩個Bulkhead分別包裝Supplier 和 Function
CheckedFunction0<String> decoratedSupplier
    = Bulkhead.decorateCheckedSupplier(bulkhead, () -> "Hello");
CheckedFunction1<String, String> decoratedFunction
    = Bulkhead.decorateCheckedFunction(anotherBulkhead, (input) -> input + " world");

// 鏈接函數
Try<String> result = Try.of(decoratedSupplier)
                        .mapTry(decoratedFunction::apply);

assertThat(result.isSuccess()).isTrue();
assertThat(result.get()).isEqualTo("Hello world");
assertThat(bulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);
assertThat(anotherBulkhead.getMetrics().getAvailableConcurrentCalls()).isEqualTo(1);

 

下面我們模擬飽和態Bulkhead的行爲:

// 創建並行度爲2的Bulkhead
BulkheadConfig config = BulkheadConfig.custom().maxConcurrentCalls(2).build();
Bulkhead bulkhead = Bulkhead.of("test", config);
// 該方法會進入Bulkhead
bulkhead.isCallPermitted();
bulkhead.isCallPermitted();

// 經過上面兩次調用,Bulkhead已飽和
CheckedRunnable checkedRunnable = Bulkhead.decorateCheckedRunnable(bulkhead, () -> {throw new RuntimeException("BAM!");});
Try result = Try.run(checkedRunnable);

assertThat(result.isFailure()).isTrue();
assertThat(result.failed().get()).isInstanceOf(BulkheadFullException.class);

 

你可以在運行時動態修改Bulkhead配置,但新的最大阻塞時間不會影響當前正在等待的線程。

與熔斷類似,Bulkhead也支持RxJava/Reactor.

Bulkhead bulkhead = Bulkhead.ofDefaults("backendName");
Observable.fromCallable(backendService::doSomething)
          .lift(BulkheadOperator.of(bulkhead));

Bulkhead bulkhead = Bulkhead.ofDefaults("backendName");
Mono.fromCallable(backendService::doSomething)
          .transform(BulkheadOperator.of(bulkhead));

 

監聽Bulkhead事件

BulkHeadEvent包含允許執行、拒絕執行、執行結束事件,可以用如下方式監聽:

bulkhead.getEventPublisher()
    .onCallPermitted(event -> logger.info(...))
    .onCallRejected(event -> logger.info(...))
    .onCallFinished(event -> logger.info(...));

 

Bulkhead狀態

Bulkhead.Metrics metrics = bulkhead.getMetrics();
in remainingBulkheadDepth = metrics.getAvailableConcurrentCalls()

 

限時 TimeLimiter

使用方式

限時器與Future配合使用,限時器將Future Supplier轉換爲Callable,它將嘗試在給定時間內獲取Future的值,如果超時未獲取到,Future將會被取消:

// 創建限時器配置,最大執行時間爲60s,超時將取消Future
TimeLimiterConfig config = TimeLimiterConfig.custom()
    .timeoutDuration(Duration.ofSeconds(60))
    .cancelRunningFuture(true)
    .build();

// 從該配置創建限時器
TimeLimiter timeLimiter = TimeLimiter.of(config);

ExecutorService executorService = Executors.newSingleThreadExecutor();

// 將待執行任務提交到線程池,獲取Future Supplier
Supplier<Future<Integer>> futureSupplier = () -> executorService.submit(backendService::doSomething)

// 使用限時器包裝Future Supplier
Callable restrictedCall = TimeLimiter
    .decorateFutureSupplier(timeLimiter, futureSupplier);

// 若任務執行超時,onFailure會被觸發
Try.of(restrictedCall.call)
    .onFailure(throwable -> LOG.info("A timeout possibly occurred."));

你可以將限時器與熔斷器配合使用,在超時次數過多時觸發熔斷:

Callable restrictedCall = TimeLimiter
    .decorateFutureSupplier(timeLimiter, futureSupplier);

Callable chainedCallable = CircuitBreaker.decorateCallable(circuitBreaker, restrictedCall);

Try.of(chainedCallable::call)
    .onFailure(throwable -> LOG.info("We might have timed out or the circuit breaker has opened."));

 

重試 Retry

使用方式

可配置選項有:

  1. 最大重試次數
  2. 重試間隔
  3. 評估是否重試的Predicate
// 最多重試2次
// 重試間隔爲100ms
// 當執行出現WebServiceException時觸發重試
RetryConfig config = RetryConfig.custom()
    .maxAttempts(2)
    .waitDuration(Duration.ofMillis(100))
    .retryOnException(throwable -> API.Match(throwable).of(
            API.Case($(Predicates.instanceOf(WebServiceException.class)), true),
            API.Case($(), false)))
    .build();

// 從重試配置創建重試器
Retry retry = Retry.of("id", config); 
// 使用重試器包裝函數調用
CheckedFunction0<String> retryableSupplier = Retry.decorateCheckedSupplier(retry, () -> {throw new WebServiceException("BAM!");});
// 調用將會自動重試
Try<String> result = Try.of(retryableSupplier).recover((throwable) -> "Hello world from recovery function");

assertThat(result.get()).isEqualTo("Hello world from recovery function");

 

Maven依賴

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-circuitbreaker</artifactId>
    <version>0.13.1</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-ratelimiter</artifactId>
    <version>0.13.1</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-retry</artifactId>
    <version>0.13.1</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-bulkhead</artifactId>
    <version>0.13.1</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-cache</artifactId>
    <version>0.13.1</version>
</dependency>
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-timelimiter</artifactId>
    <version>0.13.1</version>
</dependency>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章