«1. Обзор

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

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

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

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

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

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

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

3. Circuit Breaker

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

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

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

Давайте посмотрим, как мы можем добиться этого с помощью Resilience4j.

Во-первых, нам нужно определить используемые настройки. Самый простой способ — использовать настройки по умолчанию:

Также можно использовать пользовательские параметры:

CircuitBreakerRegistry circuitBreakerRegistry
  = CircuitBreakerRegistry.ofDefaults();

Здесь мы установили порог скорости 20% и минимальное количество попыток вызова 5.

CircuitBreakerConfig config = CircuitBreakerConfig.custom()
  .failureRateThreshold(20)
  .ringBufferSizeInClosedState(5)
  .build();

Затем мы создаем объект CircuitBreaker и вызываем через него удаленную службу:

Наконец, давайте посмотрим, как это работает, с помощью теста JUnit.

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

Мы попытаемся вызвать службу 10 раз. Мы должны быть в состоянии убедиться, что вызов был предпринят минимум 5 раз, а затем остановлен, как только 20% вызовов не удалось:

3.1. Состояния и настройки автоматического выключателя

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

Автоматический выключатель может находиться в одном из трех состояний:

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

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

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

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

Как и в предыдущем разделе, для этой функции требуется устойчивость зависимость 4j-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 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

Для модуля Cache требуется зависимость resilience4j-cache.

Инициализация выглядит немного иначе, чем в других модулях:

Здесь кэширование выполняется с помощью используемой реализации JSR-107 Cache, и Resilience4j предоставляет способ ее применения.

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

Обратите внимание, что не существует API для декорирования функций (таких как Cache.decorateFunction(Function)), API поддерживает только типы Supplier и Callable.

8. TimeLimiter

Для этого модуля мы должны добавить зависимость resilience4j-timelimiter.

Можно ограничить время, затрачиваемое на вызов удаленной службы, с помощью TimeLimiter.

Чтобы продемонстрировать, давайте настроим TimeLimiter с настроенным тайм-аутом в 1 миллисекунду:

Затем давайте проверим, что Resilience4j вызывает Future.get() с ожидаемым тайм-аутом:

long ttl = 1;
TimeLimiterConfig config
  = TimeLimiterConfig.custom().timeoutDuration(Duration.ofMillis(ttl)).build();
TimeLimiter timeLimiter = TimeLimiter.of(config);

Мы можем также комбинируйте его с CircuitBreaker:

Future futureMock = mock(Future.class);
Callable restrictedCall
  = TimeLimiter.decorateFutureSupplier(timeLimiter, () -> futureMock);
restrictedCall.call();

verify(futureMock).get(ttl, TimeUnit.MILLISECONDS);

9. Дополнительные модули

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

Resilience4j также предлагает ряд дополнительных модулей, упрощающих интеграцию с популярными платформами и библиотеками.

Некоторые из наиболее известных интеграций:

Spring Boot — модуль resilience4j-spring-boot Ratpack — модуль resilience4j-ratpack Retrofit — модуль resilience4j-retrofit Vertx — модуль resilience4j-vertx модуль Dropwizard – модуль resilience4j-metrics Prometheus – модуль resilience4j-prometheus

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

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

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

«