В прошлой статье мы разобрали паттерн Circuit Breaker, который помогает вовремя прекращать обращения к недоступному сервису и тем самым не расходовать ресурсы системы впустую. Но одного этого механизма недостаточно: после сбоя важно определить, как система должна вести себя дальше — повторить запрос, ограничить время ожидания или вернуть альтернативный результат. Для таких сценариев используются паттерны Retry, Timeout и Fallback. В этой статье рассмотрим, как они работают, и покажем примеры реализации на базе того же учебного проекта с сервисами User и Pet, который использовался в предыдущей статье.
Паттерн Retry
Представим ситуацию: мальчик Егор в магазине увидел вкусную конфету и попросил ее у папы. Папа сказал, что не купит. Прошло 2 минуты, и Егор снова попросил папу купить ему конфету. Папа отказал. Прошло еще 5 минут, и Егор снова попросил купить ему конфету. Папа снова отказал ему. Получив очередной отказ, Егор обиделся и сказал, что он съест конфету, которая у него есть дома. В этом примере повторные просьбы Егора можно сравнить с применением паттерна Retry: система не прекращает попытки сразу, а повторяет запрос через определенные промежутки времени, рассчитывая, что проблема носит временный характер.
Паттерн Retry используется для автоматического повторения сетевых запросов при временных сбоях и краткосрочных ошибках во внешних сервисах. При использовании данного паттерна необходимо учитывать, что сбой должен носить временный характер и повторные запросы через определенные интервалы позволят его преодолеть.
У Retry есть два важных параметра:
- тип ошибки: необходимо настроить Retry на определенные типы ошибок, при возникновении которых он будет срабатывать;
- стратегия повторных попыток: определяет, сколько раз и как часто будут повторяться попытки выполнения запроса.
Эти параметры позволяют контролировать интенсивность и продолжительность попыток, чтобы избежать излишней нагрузки на систему.
Если Retry исчерпал заданное количество попыток, вызов завершается с ошибкой, после чего может быть выполнен fallback-метод. Параметр waitDuration задаёт паузу между попытками и не ограничивает общее время выполнения. Общий тайм-аут настраивается отдельно, например с помощью TimeLimiter.
Рисунок 1 – Алгоритм работы паттерна Retry
Более подробная информация в официальной документации.
Основные параметры паттерна Retry:
- maxAttempts — устанавливает максимальное количество попыток, включая первоначальную перед тем, как выбросить исключение;
- waitDuration — определяет время ожидания между попытками;
- retryExceptions — определяет список исключений, при возникновении которых следует запускать повторные попытки;
- ignoreExceptions — определяет список игнорируемых исключений;
- intervalFunction — позволяет задать функцию для создания динамической задержки между попытками.
Таким образом, паттерн Retry повышает надежность микросервисной системы, позволяя автоматически повторять запросы при кратковременных ошибках и временной недоступности внешних сервисов.
Паттерн Timeout
Мальчик Егор смотрит свой любимый мультик. Папа говорит, что если Егор не закончит смотреть мультик через 5 минут, то папа выключит телевизор сам. Тут, все достаточно просто, папа применил паттерн Timeout и четко обозначил время наступления неблагоприятных последствий. Если бы папа просто просил закончить без указания времени, то это могло бы длиться до бесконечности.
Timeout используется в МСА для ограничения времени выполнения запроса. Он позволяет предотвратить блокировки и зависания МС при взаимодействии с внешними сервисами, повысить отзывчивость системы, обеспечить отказоустойчивость, если запросы не выполняются в определенный интервал. Когда установленное время ожидания истекает, TimeLimiter прекращает ожидание и возвращает ошибку тайм-аута, но фактическая отмена уже запущенной операции зависит от типа Future и настройки cancelRunningFuture, и может быть выполнено альтернативное действие, например, задействован паттерн Fallback или Retry.
Основной параметр паттерна Timeout:
- timeoutDuration – максимальное время ожидания завершения операции. Паттерн Timeout позволяет предотвратить каскадные сбои и изолировать отказы на уровне каждого отдельного сервиса.
Паттерн Fallback
Тут все достаточно просто с аналогией. Мальчик Егор просит папу найти ему любимую машинку, но папе некогда, и он отказывается искать. Тогда Егор сам находит трактор и играет с ним.
Fallback используется для обработки неудачных или прерванных операций. Когда основная операция не может быть выполнена (например, из-за сбоя, ошибки или тайм-аута), система выполняет резервную логику для обеспечения нормальной работы или возврата хотя бы частичного результата. Этот паттерн позволяет снизить вероятность полного отказа системы и улучшить пользовательский опыт, предоставляя альтернативные пути.
Fallback используется в связке с другими паттернами устойчивости к отказам, например с Circuit Breaker, Retry, Timeout и другими.
Рисунок 2 – Алгоритм работы паттерна Fallback
Реализация паттернов управления отказами на Java
Примеры, рассмотренные в статье, основаны на проекте, созданном во второй части. Далее этот проект расширяется за счёт добавления новых паттернов управления отказами.
Реализация Fallback
Паттерн Fallback позволяет в случае возникновения ошибки (например, при срабатывании Circuit Breaker) выполнять альтернативный сценарий. Для этого в сервис добавим метод с альтернативным сценарием, который будет возвращать пустой список (можно здесь создавать ранее подготовленный список и его возвращать) и выводить в лог сообщение и ошибку.
private List<PetDTO> errorGetAllPets(Throwable throwable) {
log.info("Pet service is down. Can't get all pets. Please retry later");
log.info("Error {}", throwable.getMessage());
return Collections.emptyList();
}
Далее в аннотации @CircleBreaker добавим параметр fallbackMethod c указанием имени метода.
@CircuitBreaker(name = "userServiceCircuitBreaker", fallbackMethod = "errorGetAllPets")
public List<PetDTO> getAllPets(){…}
Запускаем тот же сценарий и видим в логе, что при ошибке отрабатывает альтернативный сценарий паттерна Fallback.
Лог:
c.a.u.controller.UserController : Request: From front get all pets
c.a.userservice.service.UserService : user-service method getAllPets
c.a.userservice.service.UserService : Pet service is down. Can't get all pets. Please retry later
c.a.userservice.service.UserService : Error I/O error on GET request for "http://localhost:8082/api/pets/all": Connection refused: connect
c.a.u.controller.UserController : Request: From front get all pets
c.a.userservice.service.UserService : user-service method getAllPets
c.a.userservice.service.UserService : Pet service is down. Can't get all pets. Please retry later
c.a.userservice.service.UserService : Error I/O error on GET request for "http://localhost:8082/api/pets/all": Connection refused: connect
c.a.u.controller.UserController : Request: From front get all pets
c.a.userservice.service.UserService : user-service method getAllPets
c.a.userservice.service.UserService : Pet service is down. Can't get all pets. Please retry later
c.a.userservice.service.UserService : Error I/O error on GET request for "http://localhost:8082/api/pets/all": Connection refused: connect
c.a.u.controller.UserController : Request: From front get all pets
c.a.userservice.service.UserService : Pet service is down. Can't get all pets. Please retry later
c.a.userservice.service.UserService : Error CircuitBreaker 'userServiceCircuitBreaker' is OPEN and does not permit further calls
Паттерн fallback, также успешно выполняет свою функцию.
Реализация Retry
Если нет желания вручную организовывать повторную отправку запросов, можно использовать паттерн Retry. Для этого удалим аннотацию @CircuitBreaker и пометим метод аннотацией @Retry. Эти паттерны можно использовать совместно — важно лишь корректно настроить приоритет их срабатывания.
@Retry(name = "userServiceRetry")
public List<PetDTO> getAllPets(){
log.info("user-service method getAllPets");
List<PetDTO> petDTOList = restTemplate.getForEntity(petServiceUrl+petServicePath+petServiceGetAll, List.class).getBody();
log.info("Response: From pet-service got list with all pets {}", petDTOList);
return petDTOList;
}
В файле application.yaml укажем минимальные, необходимые настройки: maxAttempts – количесво попыток, установим 5; waitDuration – временная задержка между попытками, установим 2 сек.
resilience4j.retry:
instances:
userServiceRetry:
maxAttempts: 5
waitDuration: 2s
Запускаем МС, вызываем метод получения всех животных.
Лог:
c.a.u.controller.UserController : Request: From front get all pets
c.a.userservice.service.UserService : user-service method getAllPets
c.a.userservice.service.UserService : user-service method getAllPets
c.a.userservice.service.UserService : user-service method getAllPets
c.a.userservice.service.UserService : user-service method getAllPets
c.a.userservice.service.UserService : user-service method getAllPets
o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8082/api/pets/all": Connection refused: connect] with root cause
Как видно, выполняется 5 попыток отправки запроса, после чего возвращается ошибка соединения. Ранее уже упоминалось, что Retry можно комбинировать с паттерном CircuitBreaker. Также его можно использовать вместе с паттерном Fallback. Эти комбинации предлагаем попробовать самостоятельно.
Реализация Timeout с помощью Resilience4j TimeLimiter
Данный паттерн позволяет устанавливать время ожидания выполнения запроса. Особенностью данного паттерна является то, что он работает с асинхронными запросами. Для демонстрации его работы добавим в контроллер МС «Pet» Get-метод /all/slow, который будет медленно выполнять запросы.
@GetMapping("/all/slow")
public ResponseEntity<List<Pet>> getAllPetsSlow() {
log.info("Request Slow from user-service to get all pets");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.build();
}
List<Pet> petList = petService.getAllPets();
return ResponseEntity.ok(petList);
}
В контроллер МС «User», добавим метод для вызова медленного запроса.
@GetMapping("/pets/slow")
public CompletableFuture<ResponseEntity<List<PetDTO>>> getAllPetsSlow() {
log.info("Request slow: From front get all pets");
return userService.getAllPetsSlow().thenApply(ResponseEntity::ok);
}
В сервис также добавим новый метод и пометим его аннотацией @TimeLimiter.
@TimeLimiter(name = "slowMethodService")
public CompletableFuture<List<PetDTO>> getAllPetsSlow(){
log.info("user-service method getAllPetsSlow");
return CompletableFuture.supplyAsync(() -> {
ResponseEntity<List<PetDTO>> petDTOList = restTemplate.exchange(petServiceUrl + petServicePath + petServiceGetAll + "/slow",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<PetDTO>>() {}
);
log.info("Response slow: From pet-service slow got list with all pets {}", petDTOList.getBody());
return petDTOList.getBody();
}
);
}
В файл application.yaml добавим настройку по времени ожидания.
resilience4j.timelimiter:
instances:
slowMethodService:
timeoutDuration: 5s
Запускаем оба МС и вызываем Get-метод http://localhost:8081/api/users/pets/slow. В логах видно, что по истечении 5 секунд было выброшено исключение.
Лог:
2024-11-24T16:46:34.237+04:00 INFO 14624 --- [nio-8081-exec-1] c.a.u.controller.UserController : Request slow: From front get all pets
2024-11-24T16:46:34.253+04:00 INFO 14624 --- [nio-8081-exec-1] c.a.userservice.service.UserService : user-service method getAllPetsSlow
2024-11-24T16:46:39.262+04:00 ERROR 14624 --- [nio-8081-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.util.concurrent.ExecutionException: java.util.concurrent.TimeoutException: TimeLimiter 'slowMethodService' recorded a timeout exception.] with root cause
Какие есть недостатки у паттернов Retry, Timeout и Fallback?
Использование описанных выше паттернов, как в отдельности, так и в различных сочетаниях, позволяет увеличить надежность микросервисной архитектуры. Как и Circuit Breaker данные паттерны имеют недостатки:
- Retry:
- Увеличивает нагрузку на систему и сеть, если количество повторных попыток и время между ними настроены не оптимально;
- Приводит к задержкам — повторные попытки могут увеличить дополнительное время ожидания и задержки в обработке запросов, что может быть критично в системах, требующих высокого отклика;
- Не решает проблему постоянных ошибок — если ошибка является системной или постоянной, повторные попытки не приведут к ее решению, а только увеличат время ожидания пользователя, не принося пользы.
- TimeLimiter:
- Жесткие ограничения по времени могут привести к отмене задач, которые могли бы завершиться, если бы было больше времени. Это особенно критично для длительных операций.
- Fallback:
- Может предоставлять неполные или неверные данные — часто возвращается ограниченный набор данных или заранее подготовленные ответы, которые могут не удовлетворить все требования пользователя или отличаться от реальных.
- Увеличение сложности логики — для эффективного использования fallback необходимо продумывать альтернативные пути для каждого типа ошибки или отказа, что увеличивает сложность кода.
Заключение
Паттерны, описанные в этой и предыдущей статьях, значительно улучшают отказоустойчивость МСА, но необходимо учитывать следующие моменты при их применении:
- Усложнение архитектуры — все паттерны требуют настройки и внедрения дополнительной логики в систему, что может привести к увеличению сложности разработки и тестирования;
- Производительность — хотя паттерны повышают отказоустойчивость, они добавляют дополнительные накладные расходы на систему, например, при обработке исключений, выполнении повторных попыток или управлении состоянием цепи;
- Проблемы с конфигурацией — некорректная настройка параметров, таких как время ожидания, количество повторных попыток, порог отказов и т.д., может привести к неоптимальной работе паттернов, снижая их эффективность и увеличивая нагрузку на систему.
Не существует «серебряной пули», поэтому паттерны управления отказами необходимо применять сбалансированно, с учетом особенностей системы и требований к ее производительности.
Данная серия статей формирует основу для практического использования этих подходов. При этом рассматриваемые паттерны важны как для проектирования и разработки, так и для более широкого понимания того, как техническая устойчивость систем влияет на стабильность сервисов и связанных с ними процессов.
Если вы пропустили начало цикла, читайте первую статью «Почему один упавший сервис может обрушить всю систему — как управлять отказами в микросервисной архитектуре» и второй материал «Паттерн Circuit Breaker на Javaс помощью библиотеки Resilience4j».
А если вашему проекту необходим коммерческий опыт наших Java-инженеров для построения отказоустойчивых систем, вы можете привлечь Senior Java-разработчиков MediaSoft по модели аутстаффинга — наши менеджеры свяжутся с вами в ближайшее время для обсуждения задач.
статьи по теме
-
ЧитатьCircuit Breaker: как повысить устойчивость микросервисной системы. Часть 229.09.2026
-
ЧитатьПочему один упавший сервис может обрушить всю систему — как управлять отказами в микросервисной архитектуре. Часть 123.09.2026
-
ЧитатьКак выгодно подсветить ваш уровень владения ИИ на собеседовании08.09.2026
-
ЧитатьБольшая подборка выпусков MediaSoft Подкаст08.06.2026
-
ЧитатьJava, C#, Go, Python, PHP: что выбрать для разработки микросервисного бэкенда?02.06.2026
-
ЧитатьВ какой момент Hibernate из волшебной палочки превращается в источник проблем?26.02.2026


