Краткое сравнение библиотек отказоустойчивости на JVM

Kate

Administrator
Команда форума
Независимо от того, внедряете ли вы микросервисы или нет, есть вероятность, что вы вызываете конечные точки HTTP. С HTTP-вызовами многое может пойти не так. Опытные разработчики планируют это и проектируют не только успешные пути. В общем отказоустойчивость (Fault Tolerance) включает в себя следующие функции:

  • Retry (повтор попытки)
  • Timeout (тайм-аут)
  • Circuit Breaker (автоматический выключатель)
  • Fallback (откат)
  • Rate Limiter (ограничитель скорости), чтобы избежать кода ответа 429 от сервера
  • Bulkhead: ограничитель скорости ограничивает количество вызовов в определенный период времени, а Bulkhead ограничивает количество одновременных вызовов.
Несколько библиотек реализуют эти функции на JVM. В этом посте мы рассмотрим Microprofile Fault Tolerance, Failsafe и Resilience4J.

Отказоустойчивость Microprofile​

Отказоустойчивость Microprofile является частью зонтичного проекта Microprofile. Она отличается от двух других тем, что это спецификация, которая полагается на среду выполнения для обеспечения этих возможностей. Например, Open Liberty является одной из таких сред выполнения. SmallRye Fault Tolerance — еще одна. В свою очередь, другие компоненты, такие как Quarkus и WildFly, включают SmallRye.

Microprofile определяет аннотации для каждой функции: @Timeout, @Retry Policy, @Fallback, @Circuit Breakerи @Bulkhead. Он также определяет аннотацию @Asynchronous.

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

Можно указать аннотацию @Fallback, и она будет вызываться, если генерируется исключение TimeoutException. Если используется аннотация @Timeout вместе с @Retry, TimoutException вызовет @Retry. Когда используется @Timeout с @CircuitBreakerи если происходит TimeoutException, отказ будет вызывать @Circuit Breaker.
Timeout Usage

Resilience4J​

Я наткнулся на Resilience4J, когда вел свой доклад о шаблоне Circuit Breaker. Доклад включал демонстрацию, и она опиралась на Hystrix. Однажды я захотел обновить демоверсию до последней версии Hystrix и заметил, что разработчики отказались от нее в пользу Resilience4J.

Resilience4J основан на нескольких основных концепциях:

  • Один JAR-файл для каждой функции отказоустойчивости с дополнительными JAR-файлами для определенных интеграций, например, с Kotlin.
  • Статические фабрики
  • Композиция функций с помощью шаблона Decorator, примененного к функциям
  • Интеграция с функциональными интерфейсами Java, например, Runnable, Callable, Function, и т. д.
  • Распространение исключений: можно использовать функциональный интерфейс, который выбрасывает исключение, и библиотека будет распространять его по цепочке вызовов.
Вот упрощенная диаграмма классов для Retry.

530a8073ed1957b61b8f707593f78d50.png

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

Проанализируем пример:

7882582d31c9d9c89296e069c3114f3f.png

1Декорируем базовую функцию server.call() с помощью Retry: эта функция должна быть protected.
2Используем конфигурацию по умолчанию
3Создаем новую конфигурацию Circuit Breaker
4Установим порог, выше которого вызов считается медленным
5Подсчитаем скользящее окно из 2 вызовов
6Минимальное количество вызовов для принятия решения о размыкании Circuit Breaker
7Декорируем функцию повтора Circuit Breaker с приведенной выше конфигурацией.
8Создаем значение fallback, которое будет возвращаться при отключении Circuit Breaker.
9Список исключений для обработки: они не будут распространяться. Resilience4J генерирует исключение CallNotPermittedException когда цепь разомкнута.
10В случае возникновения какого-либо из настроенных исключений вызываем эту функцию
Может быть трудно разобраться с порядком, в котором расположены эти функции. Поэтому проект предлагает класс Decorators, объединяющий их с помощью гибкого API. Вы можете найти его в модуле resilience4j-all. Можно переписать приведенный выше код как:

var pipeline = Decorators.ofSupplier(() -> server.call())
.withRetry(Retry.ofDefaults("retry"))
.withCircuitBreaker(CircuitBreaker.of("circuit-breaker", config))
.withFallback(
List.of(IllegalStateException.class, CallNotPermittedException.class),
e -> "fallback"
);
Это делает цель более ясной.

Failsafe​

Я наткнулся на Failsafe не так давно. Его принципы аналогичны Resilience4J: статические фабрики, композиция функций и распространение исключений.

В то время как функция отказоустойчивости Resilience4J не имеет общей иерархии классов, Failsafe предоставляет концепцию Policy:

c50bfc8acdd0918b55d5e8fd8d223ebc.png

Я считаю, что основное отличие Failsafe от Resilience4J заключается в его конвейерном подходе.

API Resilience4J требует, чтобы вы сначала предоставили «базовую» функцию, а затем встроили ее в любую функцию обертку (wrapper function). Вы не можете использовать цепочку из различных базовых функций.

Failsafe разрешает это с помощью класса FailsafeExecutor.

2baf21277e5ac55c665e166f660591e2.png

Вот как можно создать конвейер, т. е. экземпляр FailsafeExecutor. Обратите внимание, что в коде нет ссылки на базовый вызов:

1998337c5b680ee2bde1c8378f260caa.png

1Определяем список политик, применяемых от последней к первой по порядку.
2Fallback значение
3Если время вызова превышает 2000 мс, генерируется исключение TimeoutExceededException.
4Политика повторных попыток по умолчанию
Теперь можно обернуть вызов:

pipeline.get(() -> server.call());
Failsafe также предоставляет удобный API. С его помощью можно переписать приведенный выше код так:

var pipeline = Failsafe.with(Fallback.of("fallback"))
.compose(RetryPolicy.ofDefault())
.compose(Timeout.ofDuration(Duration.of(2000, MILLIS)));

Вывод​

Все три библиотеки предоставляют более или менее равноценные функции. Если вы не используете среду выполнения, совместимую с CDI, такую как сервер приложений или Quarkus, забудьте о Microprofile Fault Tolerance.

Failsafe и Resilience4J основаны на композиции функций и очень похожи. Если вам нужно определить цепочку функций независимо от вызова базовых, отдайте предпочтение Failsafe. В противном случае можно выбрать любой из них.

Поскольку я лучше знаком с Resilience4J, я, вероятно, буду использовать Failsafe в своем следующем проекте, чтобы получить больше опыта.

 
Сверху