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

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 .