介紹
Hystrix停更之後,Netflix官方推薦移步至resilience4j,它是一個輕量、易用、可組裝的高可用框架,支持熔斷、高頻控制、隔離、限流、限時、重試等多種高可用機制。
與Hystrix相比,它有以下一些主要的區別:
- Hystrix調用必須被封裝到HystrixCommand裏,而resilience4j以裝飾器的方式提供對函數式接口、lambda表達式等的嵌套裝飾,因此你可以用簡潔的方式組合多種高可用機制
- Hystrix的頻次統計採用滑動窗口的方式,而resilience4j採用環狀緩衝區的方式
- 關於熔斷器在半開狀態時的狀態轉換,Hystrix僅使用一次執行判定是否進行狀態轉換,而resilience4j則採用可配置的執行次數與閾值,來決定是否進行狀態轉換,這種方式提高了熔斷機制的穩定性
- 關於隔離機制,Hystrix提供基於線程池和信號量的隔離,而resilience4j只提供基於信號量的隔離
下面依次介紹各類高可用機制的使用方式【選譯官方使用文檔】。
熔斷 CircuitBreaker
初始化熔斷器
CircuitBreakerRegistry負責創建和管理熔斷器實例CircuitBreaker,它是線程安全的,提供原子性操作。
CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();
該方式使用默認的全局配置CircuitBreakerConfig創建熔斷器實例,你也可以選擇使用定製化的配置,可選項有:
- 觸發熔斷的失敗率閾值
- 熔斷器從打開狀態到半開狀態的等待時間
- 熔斷器在半開狀態時環狀緩衝區的大小
- 熔斷器在關閉狀態時環狀緩衝區的大小
- 處理熔斷器事件的定製監聽器CircuitBreakerEventListener
- 評估異常是否被記錄爲失敗事件的定製謂詞函數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,自定義配置的可選項有:
- 頻次閾值
- 閾值刷新時間
- 限流後的冷卻時間
// 創建高頻控制配置,控制頻率爲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,自定義配置的可選項有:
- 最大並行度
- 嘗試進入飽和態的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
使用方式
可配置選項有:
- 最大重試次數
- 重試間隔
- 評估是否重試的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>