Отказоустойчивость микросервисов: шаблон Circuit Breaker

В этой статье речь пойдет об отказоустойчивости системы микросервисов и о том, как ее обеспечить. Мы рассмотрим шаблон проектирования Circuit Breaker  и сравним его с шаблоном Bulkhead.

Также реализуем Circuit Breaker в Spring Cloud микросервисе с помощью библиотеки Resilience4j.

Система микросервисов

Итак, допустим у нас система из трех микросервисов:

Пользователь обращается в браузере к Zoo, а тот в свою очередь к Random Animal и Ticket.

Если пользователь ввел

/animals/any

то Zoo обращается к Random Animal (и возвращает Animal).

Если пользователь ввел

/ticket

то Zoo обращается к Ticket (и возвращает String – билет).

Допустим, микросервис Random Animal упал.

Один микросервис упал – проблема для всей системы

Random Animal упал, значит по запросу /animals/any животные больше не возвращаются. Но проблема в том, что теперь по запросу /ticket билеты тоже могут перестать возвращаться.

Почему? Если число запросов

/animals/any

велико, а ответа от Random Animal приходится ждать долго, то запросы /animals/any быстро займут весь пул тредов, и на запросы /ticket свободных тредов не останется.

Ведь известно, что на каждый запрос выделяется отдельный тред.  Запрос /animals/any заставляет тред веб-сервера Zoo висеть в ожидании ответа от Random Animal – то есть не дает треду быстро освободиться. А пул тредов на веб-сервере ограничен и един: он обслуживает и запросы /animals/any, и запросы /ticket и любые другие.

Если настроить timeout для RestTemplate, который обращается к Random Animal, то это может в каком-то случае решить проблему, а в каком-то не решить. Весь вопрос в том, освобождаются ли треды быстрее, чем приходят новые запросы.

Кстати, необязательно все запросы идут из браузера. В системе могут быть обычные микросервисы, обращающиеся к Zoo. И они тоже не смогут общаться с Zoo из-за того, что весь его пул тредов занят. А все из-за того, что упал единственный микросервис Random Animal.

Таким образом, один упавший элемент системы может завалить всю систему. И это не есть отказоустойчивость.

Ниже рассмотрим способы, как сделать систему более отказоустойчивой. К ним относится:

  • Запуск нескольких экземпляров Random Animal
  • Circuit Breaker
  • Bulkhead
Эти способы не исключают друг друга. Наоборот, лучше использовать их совместно.

Несколько экземпляров Random Animal

Вариант запуска нескольких экземпляров мы уже рассмотрели. В статье про балансировку нагрузки мы запускали несколько экземпляров микросервиса. Да, второй живой экземпляр Random Animal спас бы ситуацию. Но допустим, они все упали.

Для такого случая есть совершенно другие подходы, которые мы и рассмотрим ниже – шаблоны Circuit Breaker и Bulkhead.

Но сначала о пара слов библиотеках, с помощью которых их можно реализовать.

Hystrix vs. Resilience4j

Есть две библиотеки – реализации шаблонов Circuit Breaker и Bulkhead:

  • Hystrix
  • Resilience4j

Hystrix уже не развивается и не совместим с последней версий Spring Cloud. Поэтому будем всё делать на Resilience4j.

Шаблон Circuit Breaker

Суть шаблона Circuit Breaker состоит в том, что если Zoo обнаруживает, что Random Animal работает плоховато (отвечает долго или выдает ошибки), то на некоторое время Zoo перестает обращаться к Random Animal, несмотря на внешние толчки. Как будто предохранитель срабатывает, схема открывается и все потоки перестают доходить.

Пользователь вводит в браузере этот злосчастный /animals/any, но Zoo не обращается к Random Animal, а выдает некоторый стандартный ответ (fallback). И это окно ничегонеделания длится несколько секунд (в зависимости от настроек), в течение которых Zoo может сделать один или несколько пробных запросов к Random Animal. Если Zoo замечает, что Random Animal ожил, схема закрывается и нормальная работа возобновляется. Все происходит автоматически, программисту нужно только пометить опасные методы и задать настройки.

Опасный метод у нас – это обращение к Random Animal, то есть:

restTemplate.getForEntity("http://random-animal/random", Animal.class)

Его и будем помечать.

Параметры Circuit Breaker

Но у Circuit Breaker должны быть точные критерии, по которым он судит, что “все плохо” и переходит в открытое состояние, при котором указанный выше метод перестает вызываться. Эти критерии и задаются параметрами.

Параметры:

  1. Circuit Breaker всегда оценивает последние вызовы опасного метода и по ним делает вывод. По умолчанию это количество вызовов (но может быть последние столько-то секунд). За значение количество вызовов отвечает параметр slidingWindowSize (значение по умолчанию=100).
  2. Если хотя бы 50% из последних вызовов выдают ошибку (параметр failureRateThreshold =50), то схема открывается. И плохой метод больше не вызывается.
  3. Есть также параметр slowCallDurationThreshold=60000 мс: по этому параметру оценивается “медленность метода”. Если некий процент вызовов медленнее этого параметра, то схема тоже закрывается. По умолчанию процент=100 (это параметр slowCallRateThreshold).
  4. Далее waitDurationInOpenState =6000 мс задает, сколько надо ждать перед тем, как снова проверить, не ожил ли микросервис.

Есть и другие параметры, для всех из них существует значение по умолчанию, поэтому можно их не настраивать вообще. А можно все перезаписать. Подбор параметров делается на практике.

Реализация шаблона Circuit Breaker в Spring Cloud приложении

Для начала, добавим в микросервис Zoo зависимость:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
Настройка параметров

Теперь настроим параметры:

@Configuration
public class Resilience4JConfig {
    @Bean
    Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
        return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .circuitBreakerConfig(
                        CircuitBreakerConfig.from(CircuitBreakerConfig.ofDefaults()).slidingWindowSize(3).build()
                )
                .build());
    }
}

Мы взяли параметры по умолчанию CircuitBreakerConfig.ofDefaults(), перечисленные выше А затем на основе их изменили параметр .slidingWindowSize(3), чтобы судить о статусе микросервиса Random Animal не по 100 последним вызовам к нему, а по 3.

Вызовы restTemplate будут выводиться в консоль благодаря настройке в applicatiom.properties:

logging.level.org.springframework.web.client.RestTemplate= DEBUG

Таким образом, можно будет заметить, что после трех обновлений браузера restTemplate перестанет совершать вызов к упавшему микросервису. Это чтобы не обновлять браузер сто раз.

Как пометить метод

Внедряем бин CircuitBreakerFactory в класс RandomAnimalClient.

И затем вместо обычного вызова:

public ResponseEntity<Animal> random() {
   return restTemplate.getForEntity("http://random-animal/random", Animal.class);
}

Делаем такой:

public ResponseEntity<Animal> random() {
    LOGGER.debug("Sending  request for animal {}");

    return circuitBreakerFactory.create("randomAnimal").run(
            () -> restTemplate.getForEntity("http://random-animal/random", Animal.class),
            throwable -> fallbackRandom());
}

здесь fallbackRandom() – метод, который выдает стандартный ответ в том случае, если микросервис Random Animal не работает, или когда обращение к нему вовсе не происходит. Он должен иметь ту же сигнатуру, что и реальный метод.

Таким образом, весь класс RandomAnimalClient:

@Component
public class RandomAnimalClient {
    private static final Logger LOGGER = LoggerFactory
            .getLogger(RandomAnimalClient.class);

    private final RestTemplate restTemplate;

    private final CircuitBreakerFactory circuitBreakerFactory;

    public RandomAnimalClient(RestTemplate restTemplate,  CircuitBreakerFactory circuitBreakerFactory) {
        this.restTemplate = restTemplate;
        this.circuitBreakerFactory = circuitBreakerFactory;
    }


    public ResponseEntity<Animal> random() {
        LOGGER.debug("Sending  request for animal {}");

        return circuitBreakerFactory.create("randomAnimal").run(
                () -> restTemplate.getForEntity("http://random-animal/random", Animal.class),
                throwable -> fallbackRandom());
    }

    public ResponseEntity<Animal> fallbackRandom() {
        return ResponseEntity.ok().body(new Animal("no animal"));
    }
}
Проверка

Теперь попробуем запустить Eureka, Zoo. А Random Animal не запускать.

В браузере получим стандартный ответ:

Причем если обновлять браузер, то в консоли мы увидим обращение к http://random-animal/random только три раза:

2021-01-08 20:23:40.359  INFO 20208 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
2021-01-08 20:23:40.390 DEBUG 20208 --- [pool-1-thread-1] o.s.web.client.RestTemplate              : HTTP GET http://random-animal/random
2021-01-08 20:23:40.391 DEBUG 20208 --- [pool-1-thread-1] o.s.web.client.RestTemplate              : Accept=[application/json, application/*+json]
2021-01-08 20:23:44.217 DEBUG 20208 --- [pool-1-thread-1] o.s.web.client.RestTemplate              : HTTP GET http://random-animal/random
2021-01-08 20:23:44.218 DEBUG 20208 --- [pool-1-thread-1] o.s.web.client.RestTemplate              : Accept=[application/json, application/*+json]
2021-01-08 20:23:47.200 DEBUG 20208 --- [pool-1-thread-1] o.s.web.client.RestTemplate              : HTTP GET http://random-animal/random
2021-01-08 20:23:47.201 DEBUG 20208 --- [pool-1-thread-1] o.s.web.client.RestTemplate              : Accept=[application/json, application/*+json]

При дальнейших обновления браузера RestTemplate ничего не делает, и этот же жестко закодированный ответ возвращается очень быстро, микросервис разгружен. Что и требовалось достигнуть.

Затем, если запустить отключенный микросервис Random Animal, то спустя небольшое количество секунд Zoo это замечает и работает нормально, выдавая реальный ответ.

Теперь перейдем к шаблону Bulkhead (реализовывать не будем, только описание).

Шаблон Bulkhead

Шаблон Bulkhead подразумевает ограничение числа тредов на данный метод.

Как уже говорилось, пул тредов на веб-сервере един для всех запросов.  Но с помощью библиотеки Resilience4j можно все же искусственно ограничить число тредов для метода. То есть разбить пул на несколько подпулов.

Мы задаем, что такой-то метод может занимать не более n тредов из пула, допустим 10 (а в веб-сервере Tomcat всего 100 тредов по умолчанию). Если приходит запрос, покушающийся на еще один тред, то возвращается ошибка.

 

Переборки (bulkheads)
Переборки (bulkheads)

Bulkhead переводится как “переборка”, “отсек”. Судно разделяют на отсеки, чтобы оно не затонуло. Если в одном отсеке пробоина, то вода только в нем. Также и пул тредов разделяется на подпулы-отсеки, тоже с целью не утопить весь веб-сервер. Отсюда название шаблона.

Bulkhead vs. Circuit Breaker

  1. Таким образом, как и для Circuit Breaker, для  BulkHead нужно тоже задать настройки (для BulkHead гораздо меньше).
  2. Нужно пометить опасные методы – в Circuit Breaker они временно перестанут вызываться, а в BulkHead на них будут уходить не все треды.

Итоги

Мы рассмотрели и реализовали шаблон Circuit Breaker в Sping Cloud приложении. Исходный код доступен на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *