Отказоустойчивость
Модуль для создания отказоустойчивого приложения с использованием таких подходов как прерыватель, резервный метод, повторитель и ограничитель выполнения по времени с помощью аннотаций аспектов в декларативном стиле.
Подключение¶
Зависимость build.gradle
:
Модуль:
Зависимость build.gradle.kts
:
Модуль:
Прерыватель¶
Прерыватель (CircuitBreaker
) – это прокси, который контролирует поток к запросам конкретного метода
и может прекращать временно выполнение этого метода если метод бросает много исключений соответствующих заданным требованиям фильтра (CircuitBreakerPredicate
).
Цель применения прерывателя — дать системе время на исправление ошибки, которая вызвала сбой, прежде чем разрешить приложению попытаться выполнить операцию еще раз.
Шаблон прерыватель обеспечивает стабильность, пока система восстанавливается после сбоя и снижает влияние на производительность.
Прерыватель может находиться в нескольких состояниях в зависимости от поведения (OPEN, CLOSED, HALF_OPEN
)
CLOSED
: Запрос приложения перенаправляется на операцию. Прокси ведет подсчет числа недавних сбоев в рамках установленного кол-ва операций (slidingWindowSize
) поступающих через прокси, и если вызов операции не завершился успешно, прокси увеличивает это число. Если число запросов превысило установленный минимальный потолок необходимый для подсчетов (minimumRequiredCalls
) и число недавних сбоев превышает заданный порог (failureRateThreshold
) в течение заданного периода времени, прокси переводится в состояниеOPEN
.OPEN
: Во время нахождения в таком статусе запрос от приложения немедленно завершает с ошибкой и исключение возвращается в приложение. На этом этапе прокси запускает таймер времени ожидания (waitDurationInOpenState
), и по истечении времени этого таймера прокси переводится в состояниеHALF-OPEN
.HALF-OPEN
: Ограниченному числу запросов (permittedCallsInHalfOpenState
) от приложения разрешено проходить через операцию и вызывать ее. Если эти запросы выполняются успешно, предполагается, что ошибка, которая ранее вызывала сбой, устранена, а автоматический выключатель переходит в состояниеCLOSED
(счетчик сбоев сбрасывается). Если какой-либо запрос завершается со сбоем, автоматическое выключение предполагает, что неисправность все еще присутствует, поэтому он возвращается в состояниеOPEN
и перезапускает таймер времени ожидания (waitDurationInOpenState
), чтобы дать системе дополнительное время на восстановление после сбоя.
Состояние HALF-OPEN
помогает предотвратить быстрый рост запросов к сервису. Т.к. после начала работы сервиса, некоторое время он может быть способен обрабатывать ограниченное число запросов до полного восстановления.
Изначально имеет состояние CLOSED
.
Декларативный подход¶
Конфигурация¶
Существует конфигурация по умолчанию, которая применяется ко всем прерывателям при создании
и затем применяются именованные настройки конкретного прерывателя для переопределения настроек по умолчанию.
Можно изменить настройки по умолчанию для всех прерывателей одновременно изменив конфигурацию по умолчанию (default
).
Пример полной конфигурации, описанной в классе CircuitBreakerConfig
(указаны примеры значений или значения по умолчанию):
resilient {
circuitbreaker {
default {
slidingWindowSize = 100 //(1)!
minimumRequiredCalls = 50 //(2)!
failureRateThreshold = 50 //(3)!
waitDurationInOpenState = "25s" //(4)!
permittedCallsInHalfOpenState = 10 //(5)!
}
}
}
- Предельное кол-во запросов в рамках которых рассчитывается
failureRateThreshold
для определения состояния (обязательный) - Минимальное кол-во запросов необходимое для начала расчета состояния (обязательный)
- Процент неуспешных запросов который необходим для перехода в состояния
OPEN
(имеет значения от 1 до 100) (обязательный) - Время ожидания в статусе
OPEN
, после которого осуществляется переход в статусHALF-OPEN
(обязательный) - Необходимое кол-во запросов в статусе
HALF-OPEN
которые должны завершится успехом для перехода вCLOSED
(обязательный)
resilient:
circuitbreaker:
default:
slidingWindowSize: 100 #(1)!
minimumRequiredCalls: 50 #(2)!
failureRateThreshold: 50 #(3)!
waitDurationInOpenState: "25s" #(4)!
permittedCallsInHalfOpenState: 10 #(5)!
- Предельное кол-во запросов в рамках которых рассчитывается
failureRateThreshold
для определения состояния (обязательный) - Минимальное кол-во запросов необходимое для начала расчета состояния (обязательный)
- Процент неуспешных запросов который необходим для перехода в состояния
OPEN
(имеет значения от 1 до 100) (обязательный) - Время ожидания в статусе
OPEN
, после которого осуществляется переход в статусHALF-OPEN
(обязательный) - Необходимое кол-во запросов в статусе
HALF-OPEN
которые должны завершится успехом для перехода вCLOSED
(обязательный)
Пример переопределения именованных настроек для определенного прерывателя:
Фильтрация исключений¶
Для регистрации какие ошибки следует записывать как ошибки со стороны прерывателя, можно переопределить фильтр по умолчанию,
требуется реализовать CircuitBreakerPredicate
и зарегистрировать свой компонент в контексте и указать в конфигурации прерывателя его имя возвращаемое в методе name()
.
По умолчанию прерыватель учитывает все ошибки.
Конфигурация:
Императивный подход¶
Можно использовать прерыватель в императивном коде, для этого понадобиться внедрить как зависимость CircuitBreakerManager
и взять из него прерыватель по имени конфигурации которая указывалась бы в аннотации:
@Component
public final class SomeService {
private final CircuitBreakerManager manager;
public SomeService(CircuitBreakerManager manager) {
this.manager = manager;
}
public String doWork() {
var circuitBreaker = manager.get("custom");
return circuitBreaker.accept(this::doSomeWork);
}
private String doSomeWork() {
// do some work
}
}
Повторитель¶
Повторитель (Retry
) - предоставляет возможность настраивать политику повторного вызова проаннотированных методов.
Позволяет указать когда требуется повторить попытку выполнения метода, настроить параметры повторения,
в случае если методом было брошено исключение соответствующая заданным требованиям фильтра (RetryPredicate
).
Декларативный подход¶
Конфигурация¶
Существует конфигурация по умолчанию, которая применяется ко всем повторителям при создании
и затем применяются именованные настройки конкретного повторителя для переопределения настроек по умолчанию.
Можно изменить настройки по умолчанию для всех повторителей одновременно изменив конфигурацию по умолчанию (default
).
Пример полной конфигурации, описанной в классе RetryConfig
(указаны примеры значений или значения по умолчанию):
Фильтрация исключений¶
Для регистрации какие ошибки следует записывать как ошибки со стороны повторителя, можно переопределить фильтр по умолчанию,
требуется реализовать RetryPredicate
и зарегистрировать свой компонент в контексте и указать в конфигурации повторителя его имя возвращаемое в методе name()
.
По умолчанию повторитель учитывает все ошибки.
Конфигурация:
Императивный подход¶
Можно использовать повторитель в императивном коде, для этого понадобиться внедрить как зависимость RetryManager
и взять из него повторитель по имени конфигурации которая указывалась бы в аннотации:
Ограничитель времени¶
Ограничитель времени (Timeout
) - предоставляет возможность задавать максимальное время работы проаннотированного метода.
Декларативный подход¶
Конфигурация¶
Существует конфигурация по умолчанию, которая применяется к ограничителю при создании
и затем применяются именованные настройки конкретного ограничителя для переопределения настроек по умолчанию.
Можно изменить настройки по умолчанию для всех ограничителей одновременно изменив конфигурацию по умолчанию (default
).
Пример полной конфигурации, описанной в классе TimeoutConfig
(указаны примеры значений или значения по умолчанию):
- Предельное время работы операции после которого будет брошен
TimeoutExhaustedException
(обязательный)
Императивный подход¶
Можно использовать ограничитель времени в императивном коде, для этого понадобиться внедрить как зависимость TimeoutManager
и взять из него ограничитель по имени конфигурации которая указывалась бы в аннотации:
@Component
public final class SomeService {
private final TimeoutManager manager;
public SomeService(TimeoutManager manager) {
this.manager = manager;
}
public String doWork() {
var timeout = manager.get("custom");
return timeout.execute(this::doSomeWork);
}
private String doSomeWork() {
// do some work
}
}
Резервный метод¶
Резервный метод (Fallback
) - предоставляет возможность указания метода который будет вызван в случае
если исключение брошенное проаннотированным методом будет удовлетворено фильтрам (FallbackPredicate
).
Метод должен совпадать по сигнатуре возвращаемого результата.
Декларативный подход¶
Пример резервного метода без аргументов:
Пример резервного метода с аргументами:
@Component
public class SomeService {
@Fallback(value = "custom", method = "getFallback(arg3, arg1)") //(1)!
public String getValue(String arg1, Integer arg2, Long arg3) {
return "value";
}
protected String getFallback(Long argLong, String argString) {
return "fallback";
}
}
- Передает аргументы проаннотированного метода в указанном порядке в резервный метод
@Component
open class SomeService {
@Fallback(value = "custom", method = "getFallback(arg3, arg1)") //(1)!
fun getValue(arg1: String, arg2: Int, arg3: Long): String = "value"
fun getFallback(argLong: Long, argString: String): String = "fallback"
}
- Передает аргументы проаннотированного метода в указанном порядке в резервный метод
Конфигурация¶
Существует конфигурация по умолчанию, которая применяется ко всем резервным метода при создании
и затем применяются именованные настройки конкретного резервного метода для переопределения настроек по умолчанию.
Можно изменить настройки по умолчанию для всех резервных методов одновременно изменив конфигурацию по умолчанию (default
).
Пример полной конфигурации, описанной в классе FallbackConfig
(указаны примеры значений или значения по умолчанию):
Фильтрация исключений¶
Для регистрации какие ошибки следует записывать как ошибки со стороны резервного метода, можно переопределить фильтр по умолчанию,
требуется реализовать FallbackPredicate
и зарегистрировать свой компонент в контексте
и указать в конфигурации резервного метода его имя возвращаемое в методе name()
.
Императивный подход¶
Можно использовать резервный метод в императивном коде, для этого понадобиться внедрить как зависимость FallbackManager
и взять из него резервный метод по имени конфигурации которая указывалась бы в аннотации:
@Component
public final class SomeService {
private final FallbackManager manager;
public SomeService(FallbackManager manager) {
this.manager = manager;
}
public String doWork() {
var fallback = manager.get("custom");
return fallback.fallback(this::doSomeWork, () -> "BackupValue");
}
private String doSomeWork() {
// do some work
}
}
Комбинация¶
Можно совмещать одновременно над одним методом все вышеперечисленные аннотации.
Порядок применения аннотаций зависит от порядка объявления аннотаций. Вы можете поменять порядок по своему желанию и комбинировать его с другими аннотациями, которые точно также применяются в порядке объявления.
@Component
public class SomeService {
@Fallback(value = "default", method = "getFallback(arg1)") // 4
@CircuitBreaker("default") // 3
@Retry("default") // 2
@Timeout("default") // 1
public String getValueSync(String arg1) {
return "result-" + arg1;
}
protected String getFallback(String arg1) { // 4
return "fallback-" + arg1;
}
}
@Component
open class SomeService {
@Fallback(value = "default", method = "getFallback(arg1)") // 4
@CircuitBreaker("default") // 3
@Retry("default") // 2
@Timeout("default") // 1
fun getValueSync(arg1: String): String = "result-$arg1"
protected fun getFallback(arg1: String): String = "fallback-$arg1" // 4
}
В примере выше:
- Применяется
@Timeout
который, говорит что метод не должен выполняться дольше времени указанного в конфигурации - Применяется
@Retry
который будет пытаться повторить выполнение метода указанное в конфигурации кол-во раз в случае, если метод бросил исключение по цепочке (включая@Timeout
) - Применяется
@CircuitBreaker
который будет работать согласно конфигурации и состоянию в зависимости успешного результата метода или если метод бросил исключение по цепочке (включая@Timeout
&@Retry
) - Применяется
@Fallback
который будет вызватьgetFallback
метод с аргументомarg1
в случае если метод бросил исключение по цепочке (включая@Timeout
&@Retry
&@CircuitBreaker
)
Порядок вызова аспектов соответствует порядку аннотаций над методом, сверху внизу.
Пример конфигурации всех аспектов:
Сигнатуры¶
Доступные сигнатуры для методов которые поддерживают аннотации из коробки:
Класс не должен быть final
, чтобы аспекты работали.
Под T
подразумевается тип возвращаемого значения, либо Void
.
T myMethod()
Optional<T> myMethod()
CompletionStage<T> myMethod()
CompletionStageMono<T> myMethod()
Project Reactor (надо подключить зависимость)Flux<T> myMethod()
Project Reactor (надо подключить зависимость)
Класс должен быть open
, чтобы аспекты работали.
Под T
подразумевается тип возвращаемого значения, либо T?
, либо Unit
.
myMethod(): T
suspend myMethod(): T
Kotlin Coroutine (надо подключить зависимость какimplementation
)myMethod(): Flow<T>
Kotlin Coroutine (надо подключить зависимость какimplementation
)