Независимо от того, внедряете ли вы микросервисы или нет, есть вероятность, что вы вызываете конечные точки 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
.
Resilience4J
Я наткнулся на Resilience4J, когда вел свой доклад о шаблоне Circuit Breaker. Доклад включал демонстрацию, и она опиралась на Hystrix. Однажды я захотел обновить демоверсию до последней версии Hystrix и заметил, что разработчики отказались от нее в пользу Resilience4J.
Resilience4J основан на нескольких основных концепциях:
Один JAR-файл для каждой функции отказоустойчивости с дополнительными JAR-файлами для определенных интеграций, например, с Kotlin.
Статические фабрики
Композиция функций с помощью шаблона Decorator, примененного к функциям
Интеграция с функциональными интерфейсами Java, например,
Runnable
,Callable
,Function
, и т. д.Распространение исключений: можно использовать функциональный интерфейс, который выбрасывает исключение, и библиотека будет распространять его по цепочке вызовов.
Вот упрощенная диаграмма классов для Retry
.
Каждая функция отказоустойчивости построена на основе одного и того же шаблона, показанного выше. Можно создать конвейер из нескольких функций, используя композицию функций, каждая из которых вызывает другую.
Проанализируем пример:
1 | Декорируем базовую функцию |
2 | Используем конфигурацию по умолчанию |
3 | Создаем новую конфигурацию Circuit Breaker |
4 | Установим порог, выше которого вызов считается медленным |
5 | Подсчитаем скользящее окно из 2 вызовов |
6 | Минимальное количество вызовов для принятия решения о размыкании Circuit Breaker |
7 | Декорируем функцию повтора Circuit Breaker с приведенной выше конфигурацией. |
8 | Создаем значение fallback, которое будет возвращаться при отключении Circuit Breaker. |
9 | Список исключений для обработки: они не будут распространяться. Resilience4J генерирует исключение |
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
:
Я считаю, что основное отличие Failsafe от Resilience4J заключается в его конвейерном подходе.
API Resilience4J требует, чтобы вы сначала предоставили «базовую» функцию, а затем встроили ее в любую функцию обертку (wrapper function). Вы не можете использовать цепочку из различных базовых функций.
Failsafe разрешает это с помощью класса FailsafeExecutor
.
Вот как можно создать конвейер, т. е. экземпляр FailsafeExecutor
. Обратите внимание, что в коде нет ссылки на базовый вызов:
1 | Определяем список политик, применяемых от последней к первой по порядку. |
2 | Fallback значение |
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 в своем следующем проекте, чтобы получить больше опыта.