Перейти к основному содержимому

Руководство по Resilience4j

· 6 мин. чтения

1. Обзор

В этом уроке мы поговорим о библиотеке Resilience4j .

Библиотека помогает внедрять отказоустойчивые системы, управляя отказоустойчивостью удаленных коммуникаций.

Библиотека вдохновлена Hystrix , но предлагает гораздо более удобный API и ряд других функций, таких как ограничитель скорости (блокирует слишком частые запросы), Bulkhead (избегает слишком большого количества одновременных запросов) и т. д.

2. Настройка Мавена

Для начала нам нужно добавить целевые модули в наш pom.xml (например, здесь мы добавляем прерыватель цепи) :

<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>0.12.1</version>
</dependency>

Здесь мы используем модуль автоматического выключателя . Все модули и их последние версии можно найти на Maven Central .

В следующих разделах мы рассмотрим наиболее часто используемые модули библиотеки.

3. Автоматический выключатель

Обратите внимание, что для этого модуля нам нужна зависимость resilience4j-circuitbreaker, показанная выше.

Шаблон прерывателя цепи помогает нам предотвратить каскад сбоев, когда удаленная служба не работает.

После ряда неудачных попыток мы можем считать сервис недоступным/перегруженным и жадно отклонять все последующие запросы к нему. Таким образом, мы можем сэкономить системные ресурсы для вызовов, которые могут завершиться неудачно.

Давайте посмотрим, как мы можем добиться этого с помощью 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 может находиться в одном из трех состояний:

  • ЗАКРЫТ – все в порядке, короткого замыкания нет
  • OPEN — удаленный сервер не работает, все запросы к нему замыкаются накоротко
  • HALF_OPEN — настроенное количество времени с момента входа в состояние OPEN истекло, и CircuitBreaker разрешает запросы на проверку того, снова ли удаленная служба подключена к сети.

Мы можем настроить следующие параметры:

  • порог частоты отказов, выше которого CircuitBreaker открывается и начинает замыкать вызовы
  • продолжительность ожидания, которая определяет, как долго CircuitBreaker должен оставаться открытым, прежде чем он переключится на полуоткрытый
  • размер кольцевого буфера, когда CircuitBreaker наполовину открыт или закрыт
  • пользовательский CircuitBreakerEventListener , который обрабатывает события CircuitBreaker
  • пользовательский предикат , который оценивает, следует ли считать исключение сбоем, и, таким образом, увеличивает частоту сбоев.

4. Ограничитель скорости

Как и в предыдущем разделе, для этой функции требуется зависимость resilience4j-ratelimiter .

Как следует из названия, этот функционал позволяет ограничить доступ к какому-либо сервису . Его API очень похож на CircuitBreaker — есть классы 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 .

Можно ограничить количество одновременных обращений к определенной службе.

Давайте посмотрим на пример использования Bulkhead API для настройки максимального количества одновременных вызовов:

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));
}

Мы также можем настроить следующее:

  • максимальное количество попыток
  • продолжительность ожидания перед повторными попытками
  • пользовательская функция для изменения интервала ожидания после сбоя
  • пользовательский предикат , который оценивает, должно ли исключение привести к повторной попытке вызова

7. Кэш

Для модуля 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. Ограничитель времени

Для этого модуля мы должны добавить зависимость 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
  • Retrofit – модуль resilience4j-retrofit
  • Vertx — модуль resilience4j-vertx
  • Dropwizard — модуль resilience4j-метрики
  • Prometheus — модуль resilience4j-prometheus

10. Заключение

В этой статье мы рассмотрели различные аспекты библиотеки Resilience4j и узнали, как использовать ее для решения различных проблем отказоустойчивости при межсерверном взаимодействии.

Как всегда, исходный код приведенных выше примеров можно найти на GitHub .