Circuit Breaker: как повысить устойчивость микросервисной системы. Часть 2

Circuit Breaker: как повысить устойчивость микросервисной системы

В распределенных системах есть десятки паттернов для управления отказами, но на практике особенно важны те, которые помогают системе оставаться стабильной под нагрузкой. В этой статье разберем паттерн Circuit Breaker и покажем пример его реализации в коде. В следующем материале посмотрим на Fallback, Retry и Timeout — это базовые инструменты, которые помогают сделать микросервисную архитектуру устойчивее и снизить влияние сбоев на бизнес.

Чтобы не превращать материал в сухую теорию, пойдем от простого и понятного примера. Объясним, как работают эти паттерны, через аналогию с взаимодействием  одного папы и его сына Егора. P.S. все герои вымышлены, совпадения случайны, а выводы — вполне рабочие.

Паттерн Circuit Breaker

Представьте: детская площадка мальчик Егор играет в песочнице с другими детьми. Его папа стоит в сторонке и наблюдает за процессом. Все прекрасно, все дети счастливы и рады. Но вдруг, между Егором и другим мальчиком начинается конфликт из-за трактора. Они никак не могут его поделить. Дети начинают злиться кричать, толкаться. Чтобы избежать дальнейшего развития конфликта, папа вмешивается и забирает Егора играть на горку. Через некоторое время, когда Егор успокоился, папа отпускает Егора снова играть в песочницу. При этом папа следит, не начнется ли снова конфликт из-за этого трактора или чего-то еще. В данном примере папа был, в роли, Circuit Breaker, который также следит за успешным взаимодействием между микросервисами.

Circuit Breaker — паттерн отказоустойчивости, который временно прекращает обращения к внешнему сервису при превышении заданного порога ошибок или медленных вызовов. Он не повторяет запросы: повторные попытки относятся к Retry.

Circuit Breaker может находиться в одном из трех состояний:

  1. Closed — стандартное состояние Circuit Breaker. Все запросы проходят к внешнему сервису. Если ошибки не превышают допустимый порог, состояние не меняется. Но если ошибок становится слишком много, Circuit Breaker переключается в состояние Open.
  2. Open — Circuit Breaker разрывает цепь. Запросы к внешнему сервису больше не отправляются, а система сразу возвращает ошибку. Это защищает и сам сервис, и инфраструктуру от лишней нагрузки. В таком состоянии Circuit Breaker находится заданное время.
  3. Half-Open — после истечения waitDurationInOpenState Circuit Breaker разрешает определенное количество запросов для проверки доступности внешнего сервиса. Если они проходят успешно, Circuit Breaker возвращается в состояние Closed. Если ошибки продолжаются, он снова переходит в Open.

Алгоритм работы паттерна Circuit Breaker

Рисунок 1. Алгоритм работы паттерна Circuit Breaker

Более подробная информация в официальной документации.

Основные параметры для настройки Circuit Breaker

Чтобы Circuit Breaker действительно защищал систему, его важно правильно настроить. Ниже — ключевые параметры, которые определяют, когда именно он должен сработать и как вести себя после сбоя:

  1. failureRateThreshold — процент ошибок, после которого Circuit Breaker переходит в состояние Open. 
  2. slowCallRateThreshold — процент медленных вызовов, при достижении которого Circuit Breaker переключается в состояние Open.
  3. slowCallDurationThreshold — порог времени, после которого запрос считается медленным. Если сервис формально отвечает, но делает это слишком долго, такие вызовы тоже начинают влиять на решение Circuit Breaker.
  4. permittedNumberOfCallsInHalfOpenState — количество разрешенных вызовов в состоянии Half-Open.
  5. waitDurationInOpenState — время нахождения CIRCUIT BREAKER в состоянии Open перед переключением в состояние Half-Open для проверки доступности сервиса.
  6. slidingWindowType — устанавливает по какой величине рассчитывается процент ошибок. Возможные значения:
    COUNT_BASED — рассчитывает ошибки на основе фиксированного количества запросов.
    TIME_BASED — рассчитывает ошибки в рамках временного интервала.
  7. slidingWindowSize — количество запросов (при COUNT_BASED) или продолжительность интервала (при TIME_BASED) для расчета процента ошибок.

Грамотно настроенный Circuit Breaker помогает системе работать стабильнее: снижает риск перегрузки, ускоряет реакцию на сбои и делает поведение сервисов более предсказуемым. А значит, бизнес получает не просто техническую защиту, а более надежный пользовательский опыт и меньше потерь из-за недоступности сервисов.

Реализация паттерна управления отказами Circuit Breaker на Java

Далее — практический разбор реализации Circuit Breaker в микросервисной архитектуре на Java с помощью библиотеки Resilience4j.

Для примера выбрана именно Resilience4j: библиотека хорошо подходит для задач, связанных с устойчивостью сервисов, и при этом достаточно проста в использовании. 

Среди ее особенностей:

  • поддержка основных паттернов управления отказами, что позволяет закрывать несколько задач одной библиотекой;
  • удобная интеграция со Spring Boot;
  • простая настройка через YAML/Properties-файлы;
  • модульная архитектура, благодаря которой можно подключать только нужные компоненты.

В качестве примера рассмотрим простую учебную задачу организации взаимодействия между двумя микросервисами — User и Pet. Микросервис User будет отправлять запрос в микросервис Pet для получения списка всех животных.

Чтобы сохранить фокус на теме статьи, основное внимание будет уделено именно настройке и демонстрации работы паттерна Circuit Breaker. Все аспекты, не относящиеся напрямую к теме управления отказами, будут сознательно опущены.

Подготовка микросервиса

С помощью Spring Initializr создадим основу для каждого из МС. Добавим зависимости Spring Web и Lombok. В результате pom.xml-файлы МС «User» и МС «Pet»  будут идентичны.


<dependencies>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
       <optional>true</optional>
    </dependency>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <scope>test</scope>
    </dependency>
 </dependencies>

С точки зрения проектирования данный запрос логичнее направлять напрямую в микросервис Pet. Но в рамках учебного проекта, чтобы максимально упростить структуру программы и сосредоточиться на паттернах ОУ будем получать всех животных через МС «User». Код контроллера МС «User».


@RestController
 @RequestMapping("/api/users")
 @RequiredArgsConstructor
 @Log4j2
 public class UserController {
     private final UserService userService;

     @GetMapping("/pets")
     public ResponseEntity<List<PetDTO>> getAllPets(){
         log.info("Request: From front get all pets");
List<PetDTO> petDTOList = 
userService.getAllPets();
     }
 }

Код PetDTO представлен ниже:


@Data
 public class PetDTO {
     private String name;
     private String petType;
     private int age;
 }

UserController при получении Get /api/users/pets обращается в UserService, который в свою очередь будет отправлять Get-запрос в МС «Pet» для получения списка все животных.

Код класс UserService:


@Service
 @Log4j2
 @RequiredArgsConstructor
 public class UserService {
     private final RestTemplate restTemplate;

     @Value("${pet-service.url}")
     private String petServiceUrl;

     @Value("${pet-service.path}")
     private String petServicePath;

     @Value("${pet-service.getAll}")
     private String petServiceGetAll;

     public List<PetDTO> 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;
     }
 }

Для корректной работы МС «User» в файл application.yaml пропишем настройки порта (МС «User» работать на порту 8081) и адрес МС «Pet», по которому необходимо отправлять запрос.

Код application.yaml:


server:
   port: 8081

 pet-service:
   url: http://localhost:8082
   path: /api/pets
   getAll: /all

    Для отправки запросов в МС «Pet» потребуется bean RestTemplate, который создается в конфигурационном файле.

@Configuration
 public class AppConfig {
     @Bean
     public RestTemplate restTemplate(){
         return new RestTemplate();
     }
 }

Наш МС «User» готов к работе. Теперь напишем код для МС «Pet». Как было сказано ранее зависимости будут использоваться аналогичные зависимостям МС «User»: Spring Web, Lombok.

Код сущности Pet:

Далее создадим PetController, который будет обрабатывать Get-запрос и выдавать список всех животных.


@RestController
 @RequestMapping("/api/pets")
 @RequiredArgsConstructor
 @Log4j2
 public class PetController {
     private final PetService petService;

     @GetMapping("/all")
     public ResponseEntity<List<Pet>> getAllPets(){
        log.info("Request from user-service to get all pets");
        List<Pet> petList = petService.getAllPets();
        return ResponseEntity.status(HttpStatus.OK).body(petList);
     }
 }

Хотя это может быть излишним для данного примера, будем придерживаться канонической структуры MVC-приложения и разделять логику контроллера, бизнес-логику и логику работы с БД (хотя БД в моем примере нет). Контроллер при получении запроса, вызывает метод getAllPets сервиса, который в свою очередь обращается к методу репозитория для получения списка животных.

Код PetService:


@Service
 @RequiredArgsConstructor
 public class PetService {
     private final PetRepository petRepository;

     public List<Pet> getAllPets(){
         return petRepository.getAllPets();
     }
 }

Так как БД не используется, в коде репозитория создадим список животных, который и будем возвращать.

Код PetRepository:


​​​​​​​@Repository
 public class PetRepository {
     private static List<Pet> petList;

     public PetRepository() {
         petList = new ArrayList<>();
         Pet pet = new Pet("Barsik", "Cat", 2);
         petList.add(pet);
         pet = new Pet("Sharik", "Dog", 5);
         petList.add(pet);
         pet = new Pet("Vasya", "Snake", 11);
         petList.add(pet);
     }

     public List<Pet> getAllPets(){
         return petList;
     }
 }

В файл application.yaml пропишем настройки порта 8082.

Код application.yaml:


​​​​​​​​​​​​​​server:
   port: 8082

МС «Pet» готов. Теперь надо проверить, что все настроено и работает корректно. Для этого запускаем оба МС и отправляем Get-запрос по адресу http://localhost:8081/api/users/pets. Получаем ответ от МС «Pet», содержащий списков всех животных.

Лог МС «User»:


​​​​​​​​​​​​​​c.a.u.controller.UserController          : Request: From front get all pets
c.a.userservice.service.UserService      : Response: From pet-service got list with all pets [{name=Barsik, petType=Cat, age=2}, {name=Sharik, petType=Dog, age=5}, {name=Vasya, petType=Snake, age=11}]

Отлично! Все работает, МС обмениваются информацией.

Реализация Circuit Breaker

Теперь перейдем к самому интересному. Представим ситуацию, что МС «Pet» по каким-то причинам стал недоступен. МС «User» об этом не знает и постоянно пытается отправить запрос на получение списка животных. Подобный сценарий нельзя считать корректным: повторяющиеся безуспешные запросы лишь увеличивают нагрузку и замедляют обработку. Здесь как раз и помогает паттерн Circuit Breaker, который позволяет определить, доступен ли внешний сервис или нет. 

Для реализации паттерна необходимо в МС «User» в pom-файле подключить зависимости.


​​​​​​​​​​​​​​<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot3</artifactId>
    <version>2.2.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>3.3.5</version>
</dependency>

Метод getAllPets в сервисе UserService, который осуществляет отправку  запросов, используем аннотацией @CircuitBreaker, чтобы активизировать данный паттерн.


@CircuitBreaker(name = "userServiceCircuitBreaker")
 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. Минимально необходимыми параметрами являются:

  • slidingWindowType — отвечает за тип подсчитываемых величин для вычисления процента сбоев, выбираем — COUNT_BASED (количество); 
  • slidingWindowSize – размер выборки по которой  подсчитываются ошибки, устанавливаем – 3; 
  • waitDurationInOpenState — время нахождения в открытом состоянии, ставим 20 сек.  

resilience4j.circuitbreaker:
   instances:
     userServiceCircuitBreaker:
       slidingWindowType: COUNT_BASED
       slidingWindowSize: 3
       waitDurationInOpenState: 20s

Запускаем МС «User», МС «Pet» не должен быть запущен. Делаем четыре отдельных попытки получить список всех животных. Первые три вызова завершаются неуспешно, Circuit Breaker регистрирует три неудачных результата и после этого переходит в Open. Четвертый вызов отклоняется Circuit Breaker без обращения к МС «Pet».

Log:


​​​​​​​1. c.a.u.controller.UserController          : Request: From front get all pets
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
2. c.a.u.controller.UserController          : Request: From front get all pets
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
3. c.a.u.controller.UserController          : Request: From front get all pets
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
4. c.a.u.controller.UserController          : Request: From front get all pets
o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: io.github.resilience4j.circuitbreaker.CallNotPermittedException: CircuitBreaker 'userServiceCircuitBreaker' is OPEN and does not permit further calls] with root cause

В логах видно, что четвертый запрос не был выполнен, Circle Breaker заблокировал отправку запроса. По истечении 20 секунд, нахождения в открытом состоянии, МС будет снова пытаться отправлять запросы. После этого Circuit Breaker переходит в Half-Open и разрешает ограниченное количество тестовых вызовов.

Вывод

Использование паттерна Circuit Breaker позволяет сделать микросервисную архитектуру более надежной и устойчивой. Однако у данного паттерна есть и недостатки, которые важно учитывать при его применении:

  • Вызывает ложные срабатывания — при неверных настройках может быстро переходить в состояние Open (например, слишком низкий процент ошибок для срабатывания), что приводит к излишним отказам сервисов, которые на самом деле могут работать стабильно.
  • Прерывает нормальные запросы — когда CircuitBreaker находится в состоянии Open, все запросы к внешнему сервису будут отклоняться, включая те, которые могли бы быть успешными, если внешняя система уже восстановилась.

В первой статье разобрали, что такое управление отказами: с какими типичными проблемами сталкиваются разработчики и какие цели стоит ставить перед собой при построении устойчивых систем. Также коротко прошлись по методам и паттернам, которые помогают при создании надежных микросервисов. Переходите и читайте: «Почему один упавший сервис может обрушить всю систему: управление отказами в микросервисной архитектуре».

А в следующей статье рассмотрим паттерны Fallback, Retry и Timeout, которые помогают повысить надежность микросервисных систем. 

Остаемся на связи, команда Java-разработки MediaSoft!