1. Обзор
В этом руководстве мы поговорим о библиотеке Resilience4j .
-
Библиотека помогает внедрять отказоустойчивые системы, управляя отказоустойчивостью для удаленной связи
Библиотека вдохновлена https://www.baeldung.com/introduction-to-hystrix[Hystrix], но предлагает гораздо более удобный API и ряд других функций, таких как ограничение скорости (блокировка слишком частых запросов), переборка (избегайте слишком много одновременных запросов)
2. Maven Setup
Для начала нам нужно добавить целевые модули в наш pom.xml (например, здесь мы добавляем автоматический выключатель) :
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>0.12.1</version>
</dependency>
Здесь мы используем модуль _circuitbreaker _ . Все модули и их последние версии можно найти на Maven Central .
В следующих разделах мы рассмотрим наиболее часто используемые модули библиотеки.
3. Выключатель
Обратите внимание, что для этого модуля нам нужна зависимость resilience4j-circuitbreaker , показанная выше.
Https://martinfowler.com/bliki/CircuitBreaker.html[Circuit Breaker pattern]помогает нам предотвратить каскад сбоев, когда удаленная служба не работает.
-
После нескольких неудачных попыток мы можем считать, что сервис недоступен/перегружен, и охотно отклоняем все последующие запросы ** к нему. Таким образом, мы можем сохранить системные ресурсы для вызовов, которые могут быть неудачными.
Давайте посмотрим, как мы можем достичь этого с Resilience4j.
Во-первых, нам нужно определить настройки для использования. Самый простой способ - использовать настройки по умолчанию:
CircuitBreakerRegistry circuitBreakerRegistry
= CircuitBreakerRegistry.ofDefaults();
Также можно использовать пользовательские параметры:
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(20)
.ringBufferSizeInClosedState(5)
.build();
Здесь мы установили порог скорости в 20% и минимум 5 попыток вызова.
Затем мы создаем объект CircuitBreaker и через него вызываем удаленный сервис:
interface RemoteService {
int process(int i);
}
CircuitBreakerRegistry registry = CircuitBreakerRegistry.of(config);
CircuitBreaker circuitBreaker = registry.circuitBreaker("my");
Function<Integer, Integer> decorated = CircuitBreaker
.decorateFunction(circuitBreaker, service::process);
Наконец, давайте посмотрим, как это работает с помощью теста JUnit.
Мы попытаемся позвонить в службу 10 раз. Мы должны быть в состоянии проверить, что попытка вызова была предпринята минимум 5 раз, а затем остановлена, как только 20% вызовов не удалось:
when(service.process(any(Integer.class))).thenThrow(new RuntimeException());
for (int i = 0; i < 10; i++) {
try {
decorated.apply(i);
} catch (Exception ignore) {}
}
verify(service, times(5)).process(any(Integer.class));
3.1. Состояния автоматического выключателя и настройки
CircuitBreaker может находиться в одном из трех состояний:
-
CLOSED - все хорошо, короткое замыкание не требуется
-
OPEN - удаленный сервер не работает, все запросы к нему замкнуты
-
HALF OPEN__ - заданное количество времени с момента перехода в состояние ОТКРЫТО
истек, и CircuitBreaker позволяет запросам проверить, вернулся ли удаленный сервис в оперативный режим
Мы можем настроить следующие параметры:
-
порог частоты отказов, выше которого открывается CircuitBreaker и
начинает короткое замыкание звонков ** продолжительность ожидания, которая определяет, как долго должен CircuitBreaker
оставайтесь открытыми, прежде чем он переключится на полуоткрытый ** размер кольцевого буфера, когда CircuitBreaker наполовину открыт или
закрыто ** пользовательский CircuitBreakerEventListener , который обрабатывает CircuitBreaker
События ** пользовательский Predicate , который оценивает, должно ли исключение считаться
отказ и, следовательно, увеличить частоту отказов
4. Ограничитель скорости
Как и в предыдущем разделе, для этой функции требуется зависимость resilience4j-ratelimiter .
Как следует из названия, эта функциональность позволяет ограничить доступ к некоторому сервису . Его API очень похож на CircuitBreaker’s - есть классы Registry , Config и Limiter .
Вот пример того, как это выглядит:
RateLimiterConfig config = RateLimiterConfig.custom().limitForPeriod(2).build();
RateLimiterRegistry registry = RateLimiterRegistry.of(config);
RateLimiter rateLimiter = registry.rateLimiter("my");
Function<Integer, Integer> decorated
= RateLimiter.decorateFunction(rateLimiter, service::process);
Теперь все вызовы на оформленном сервисном блоке при необходимости соответствуют конфигурации ограничителя скорости.
Мы можем настроить параметры как:
-
период обновления лимита
-
лимит разрешений на период обновления
-
ожидание разрешения по умолчанию
5. Переборка
Здесь нам сначала понадобится зависимость resilience4j-bulkhead .
Можно ограничить количество одновременных вызовов определенной службой.
Давайте рассмотрим пример использования API Bulkhead для настройки максимального количества одновременных вызовов:
BulkheadConfig config = BulkheadConfig.custom().maxConcurrentCalls(1).build();
BulkheadRegistry registry = BulkheadRegistry.of(config);
Bulkhead bulkhead = registry.bulkhead("my");
Function<Integer, Integer> decorated
= Bulkhead.decorateFunction(bulkhead, service::process);
Чтобы проверить эту конфигурацию, мы вызовем метод фиктивной службы
Затем мы гарантируем, что Bulkhead не разрешает любые другие вызовы:
CountDownLatch latch = new CountDownLatch(1);
when(service.process(anyInt())).thenAnswer(invocation -> {
latch.countDown();
Thread.currentThread().join();
return null;
});
ForkJoinTask<?> task = ForkJoinPool.commonPool().submit(() -> {
try {
decorated.apply(1);
} finally {
bulkhead.onComplete();
}
});
latch.await();
assertThat(bulkhead.isCallPermitted()).isFalse();
Мы можем настроить следующие параметры:
-
максимальное количество параллельных выполнений, разрешенных переборкой
-
максимальное количество времени ожидания потока при попытке войти
насыщенная переборка
6. Повторить
Для этой функции нам нужно добавить в проект библиотеку resilience4j-retry .
Мы можем автоматически повторить неудачный вызов , используя Retry API:
RetryConfig config = RetryConfig.custom().maxAttempts(2).build();
RetryRegistry registry = RetryRegistry.of(config);
Retry retry = registry.retry("my");
Function<Integer, Void> decorated
= Retry.decorateFunction(retry, (Integer s) -> {
service.process(s);
return null;
});
Теперь давайте эмулируем ситуацию, когда во время вызова удаленной службы возникает исключение, и обеспечиваем, чтобы библиотека автоматически повторяла неудачный вызов:
when(service.process(anyInt())).thenThrow(new RuntimeException());
try {
decorated.apply(1);
fail("Expected an exception to be thrown if all retries failed");
} catch (Exception e) {
verify(service, times(2)).process(any(Integer.class));
}
Мы также можем настроить следующее:
-
Максимальное количество попыток
-
продолжительность ожидания перед повторными попытками
-
пользовательская функция для изменения интервала ожидания после сбоя
-
пользовательский Predicate , который оценивает, должно ли исключение привести к
повторить звонок
7. Cache
Модуль Cache требует зависимости resilience4j-cache .
Инициализация выглядит немного иначе, чем у других модулей:
javax.cache.Cache cache = ...;//Use appropriate cache here
Cache<Integer, Integer> cacheContext = Cache.of(cache);
Function<Integer, Integer> decorated
= Cache.decorateSupplier(cacheContext, () -> service.process(1));
Здесь кеширование выполняется с помощью используемой реализации JSR-107 Cache , а Resilience4j предоставляет способ ее применения.
Обратите внимание, что API для украшения функций не существует (например, Cache.decorateFunction (Function) ), API поддерживает только типы Supplier и Callable .
8. TimeLimiter
Для этого модуля мы должны добавить зависимость resilience4j-timelimiter .
Можно ограничить время, затрачиваемое на вызов удаленной службы с использованием TimeLimiter.
Чтобы продемонстрировать, давайте настроим TimeLimiter с настроенным таймаутом в 1 миллисекунду:
long ttl = 1;
TimeLimiterConfig config
= TimeLimiterConfig.custom().timeoutDuration(Duration.ofMillis(ttl)).build();
TimeLimiter timeLimiter = TimeLimiter.of(config);
Далее, давайте проверим, что Resilience4j вызывает Future.get () с ожидаемым временем ожидания:
Future futureMock = mock(Future.class);
Callable restrictedCall
= TimeLimiter.decorateFutureSupplier(timeLimiter, () -> futureMock);
restrictedCall.call();
verify(futureMock).get(ttl, TimeUnit.MILLISECONDS);
Мы также можем объединить его с CircuitBreaker :
Callable chainedCallable
= CircuitBreaker.decorateCallable(circuitBreaker, restrictedCall);
9. Дополнительные модули
Resilience4j также предлагает ряд дополнительных модулей, которые облегчают его интеграцию с популярными фреймворками и библиотеками.
Некоторые из наиболее известных интеграций:
-
Spring Boot - модуль resilience4j-spring-boot
-
Ratpack - модуль resilience4j-ratpack
-
Модификация - модуль resilience4j-retrofit
-
Vertx - модуль resilience4j-vertx
-
Dropwizard - модуль resilience4j-metrics
-
Прометей - модуль resilience4j-прометей
10. Заключение
В этой статье мы рассмотрели различные аспекты библиотеки Resilience4j и узнали, как использовать ее для решения различных проблем отказоустойчивости в межсерверных коммуникациях.
Как всегда, исходный код для примеров выше можно найти over на GitHub .