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 .