Как использовать шаблон Circuit Breaker в приложении Spring Boot

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В этом посте я покажу, как мы можем использовать шаблон Circuit Breaker в приложении Spring Boot. Когда я говорю «шаблон Circuit Breaker» имеется в виду архитектурный шаблон автоматического выключателя. Netflix опубликовал библиотеку Hysterix для работы с автоматическими выключателями. В рамках этого поста я покажу, как мы можем использовать шаблон автоматического выключателя, используя библиотеку resilence4j в приложении Spring Boot.

Изображение с сайта Pixabay - Автор Jürgen Diermaier

Что такое автоматический выключатель?

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

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

Автоматический выключатель принимает решение об остановке вызова на основе предыдущей истории вызовов. Но есть альтернативные способы обработки вызовов. Обычно автоматический выключатель отслеживает предыдущие звонки. Предположим, что 4 из 5 вызовов завершились неудачно или истекло время, тогда следующий вызов завершится ошибкой. Это помогает более активно обрабатывать ошибки при вызове сервиса, а сервис вызывающего абонента может обрабатывать ответ по-другому, предоставляя пользователям приложения другой вариант, чем просто страницу с ошибкой.

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

Библиотека Resilience4J

У нас есть код, в котором мы вызываем удаленный сервис. Модуль автоматического выключателя из resilience4j библиотеки будет иметь лямбда-выражение для вызова удаленный сервис supplier для получения значений из вызова удаленного сервиса. Я покажу это на примере. Автоматический выключатель украшает этот вызов удаленного обслуживания таким образом, чтобы он мог отслеживать ответы и состояния переключателя.

Различные конфигурации библиотеки Resilience4j

Чтобы понять концепцию автоматического выключателя, мы рассмотрим различные конфигурации, предлагаемые этой библиотекой.

  • slidingWindowType() — эта конфигурация называется скользящее окно. В основном именно она помогает принять решение о том, как будет работать автоматический выключатель. Есть два типа скользящих окон: COUNT_BASED и TIME_BASED. Скользящее окно автоматического выключателя COUNT_BASED будет учитывать количество вызовов удаленного сервиса, в то время как TIME_BASED скользящее окно автоматического выключателя будет учитывать вызовы удаленного сервиса в течение определенного периода времени.

  • failureRateThreshold() — настраивает порог частоты отказов в процентах. Если x процентов вызовов не работают, выключатель отключается.

  • slidingWindowSize() — эта настройка помогает определить количество вызовов, которые следует учитывать при включении автоматического выключателя.

  • slowCallRateThreshold() — настраивает порог низкой скорости вызова в процентах. Если x процентов вызовов являются медленными, автоматический выключатель отключается.

  • slowCallDurationThreshold — настраивает порог продолжительности времени, при котором вызовы считаются медленными.

  • minimumNumberOfCalls() — минимальное необходимое количество вызовов, перед которым автоматический выключатель может рассчитать частоту ошибок.

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

  • waitDurationInOpenState() — Продолжительность, в течение которой автоматический выключатель должен оставаться в разомкнутом состоянии перед переходом в полуоткрытое состояние. Значение по умолчанию - 60 секунд.

COUNT-BASED автоматический выключатель

При использовании resilience4j библиотеки всегда можно использовать конфигурации по умолчанию, которые предлагает автоматический выключатель. Конфигурации по умолчанию основаны на типе COUNT-BASED  скользящего окна.

Так как же нам создать автоматический выключатель для скользящего окна типа COUNT-BASED?

CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
    .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
    .slidingWindowSize(10)
    .slowCallRateThreshold(65.0f)
    .slowCallDurationThreshold(Duration.ofSeconds(3))
    .build();

CircuitBreakerRegistry circuitBreakerRegistry =
		CircuitBreakerRegistry.of(circuitBreakerConfig);

CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("BooksSearchServiceBasedOnCount");

В приведенном выше примере мы создаем конфигурацию автоматического выключателя, которая включает тип скользящего окна COUNT_BASED. Этот автоматический выключатель записывает результат 10 вызовов для переключения автоматического выключателя в closed состояние. Если 65% вызовов являются медленными, а продолжительность медленных вызовов превышает 3 секунды, автоматический выключатель отключается.

CircuitBreakerRegistry — фабрика по созданию выключателя.

Time-Based автоматический выключатель

Теперь об Time-Based автоматическом выключателе.

CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
		.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED)
    .minimumNumberOfCalls(3)
    .slidingWindowSize(10)
    .failureRateThreshold(70.0f)
    .build();

CircuitBreakerRegistry circuitBreakerRegistry =
    CircuitBreakerRegistry.of(circuitBreakerConfig);

CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("BookSearchServiceBasedOnTime");

В приведенном выше примере мы создаем конфигурацию автоматического выключателя, которая включает в себя скользящее окно типа TIME_BASED. Автоматический выключатель фиксирует отказ вызовов после минимум 3 вызовов. Если 70 процентов вызовов терпят неудачу, срабатывает автоматический выключатель.

Пример автоматического выключателя в приложении Spring Boot

Мы рассмотрели необходимые понятия об автоматическом выключателе. Теперь я покажу, что мы можем использовать автоматический выключатель в приложении Spring Boot.

С одной стороны, у нас есть REST приложение, BooksApplication которое хранит основные сведения о библиотечных книгах. С другой стороны, у нас есть приложение, Circuitbreakerdemo которое вызывает приложение REST с помощью RestTemplate. Декорируем наш REST-вызов с помощью автоматического выключателя.

BooksApplication хранит информацию о книгах в таблице базы данных MySQL librarybooks. В REST контроллере этого приложения есть GET и POST методы.

package com.betterjavacode.books.controllers;

import com.betterjavacode.books.daos.BookDao;
import com.betterjavacode.books.models.Book;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@CrossOrigin("https://localhost:8443")
@RestController
@RequestMapping("/v1/library")
public class BookController
{
    @Autowired
    BookDao bookDao;

    @GetMapping("/books")
    public ResponseEntity<List> getAllBooks(@RequestParam(required = false) String bookTitle)
    {
        try
        {
            List listOfBooks = new ArrayList<>();
            if(bookTitle == null || bookTitle.isEmpty())
            {
                bookDao.findAll().forEach(listOfBooks::add);
            }
            else
            {
                bookDao.findByTitleContaining(bookTitle).forEach(listOfBooks::add);
            }

            if(listOfBooks.isEmpty())
            {
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

            return new ResponseEntity<>(listOfBooks, HttpStatus.OK);
        }
        catch (Exception e)
        {
            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @GetMapping("/books/{id}")
    public ResponseEntity getBookById(@PathVariable("id") long id)
    {
        try
        {
            Optional bookOptional = bookDao.findById(id);

            return new ResponseEntity<>(bookOptional.get(), HttpStatus.OK);
        }
        catch (Exception e)
        {
            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @PostMapping("/books")
    public ResponseEntity addABookToLibrary(@RequestBody Book book)
    {
        try
        {
            Book createdBook = bookDao.save(new Book(book.getTitle(), book.getAuthor(),
                    book.getIsbn()));
            return new ResponseEntity<>(createdBook, HttpStatus.CREATED);
        }
        catch (Exception e)
        {
            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @PutMapping("/books/{id}")
    public ResponseEntity updateABook(@PathVariable("id") long id, @RequestBody Book book)
    {
        Optional bookOptional = bookDao.findById(id);

        if(bookOptional.isPresent())
        {
            Book updatedBook = bookOptional.get();
            updatedBook.setTitle(book.getTitle());
            updatedBook.setAuthor(book.getAuthor());
            updatedBook.setIsbn(book.getIsbn());
            return new ResponseEntity<>(bookDao.save(updatedBook), HttpStatus.OK);
        }
        else
        {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    @DeleteMapping("/books/{id}")
    public ResponseEntity deleteABook(@PathVariable("id") long id)
    {
        try
        {
            bookDao.deleteById(id);
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        }
        catch (Exception e)
        {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}

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

В демонстрационных целях я определил CircuitBreaker в отдельном компоненте, который я буду использовать в своем service классе.

@Bean
public CircuitBreaker countCircuitBreaker()
    {
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
                .slidingWindowSize(10)
                .slowCallRateThreshold(65.0f)
                .slowCallDurationThreshold(Duration.ofSeconds(3))
                .build();

        CircuitBreakerRegistry circuitBreakerRegistry =
                CircuitBreakerRegistry.of(circuitBreakerConfig);

        CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("BooksSearchServiceBasedOnCount");

        return cb;
    }

@Bean
public CircuitBreaker timeCircuitBreaker()
    {
        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED)
                .minimumNumberOfCalls(3)
                .slidingWindowSize(10)
                .failureRateThreshold(70.0f)
                .build();

        CircuitBreakerRegistry circuitBreakerRegistry =
                CircuitBreakerRegistry.of(circuitBreakerConfig);

        CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker("BookSearchServiceBasedOnTime");
        return cb;
    }

Я определил два компонента: один для автоматического выключателя на основе счетчика, а другой - для автоматического выключателя на основе времени.

BookStoreService будет содержать вызывающее приложение BooksApplication и отображать доступные книги. Этот сервис будет выглядеть так:

@Controller
public class BookStoreService
{

    private static final Logger LOGGER = LoggerFactory.getLogger(BookStoreService.class);

    @Autowired
    public BookManager bookManager;

    @Autowired
    private CircuitBreaker countCircuitBreaker;

    @RequestMapping(value = "/home", method= RequestMethod.GET)
    public String home(HttpServletRequest request, Model model)
    {
        return "home";
    }

    @RequestMapping(value = "/books", method=RequestMethod.GET)
    public String books(HttpServletRequest request, Model model)
    {
        Supplier<List> booksSupplier =
                countCircuitBreaker.decorateSupplier(() -> bookManager.getAllBooksFromLibrary());

        LOGGER.info("Going to start calling the REST service with Circuit Breaker");
        List books = null;
        for(int i = 0; i < 15; i++)
        {
            try
            {
                LOGGER.info("Retrieving books from returned supplier");
                books = booksSupplier.get();
            }
            catch(Exception e)
            {
                LOGGER.error("Could not retrieve books from supplier", e);
            }
        }
        model.addAttribute("books", books);

        return "books";
    }
}

Поэтому, когда пользователь кликает на ссылку на главной странице, мы получаем книги из нашей REST-сервиса BooksApplication.

Я автоматически подключил бин для countCircuitBreaker. В демонстрационных целях я буду вызывать REST сервис 15 раз подряд, чтобы получить все книги. Таким образом, я смогу имитировать прерывание на стороне моей REST-сервиса.

Наш автоматический выключатель декорирует сервис supplier, который выполняет REST-вызов удаленного сервиса и сохраняет результат нашего удаленного вызова.

В этой демонстрации мы вызываем наш REST-сервис последовательно, но вызовы удаленного сервиса также могут происходить параллельно. Автоматический выключатель по-прежнему будет отслеживать результаты независимо от последовательных или параллельных вызовов.

Демо

Давайте теперь посмотрим на живой демонстрации, как автоматический выключатель будет работать.  Мой REST-сервис работает на порту 8443, а мое Circuitbreakerdemo приложение - на порту 8743.

Сначала я запускаю оба приложения и открываю домашнюю страницу Circuitbreakerdemo приложения. На главной странице есть ссылка для просмотра всех книг из магазина.

Теперь, чтобы смоделировать некоторые ошибки, я добавил следующий код в свой вызов RestTemplate, который в основном спит в течение 3 секунд, прежде чем вернуть результат вызова REST.

public List getAllBooksFromLibrary ()
    {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);

        ResponseEntity<List> responseEntity;
        long startTime = System.currentTimeMillis();
        LOGGER.info("Start time = {}", startTime);
        try
        {
            responseEntity= restTemplate.exchange(buildUrl(),
                    HttpMethod.GET, null, new ParameterizedTypeReference<List>()
                    {});
            if(responseEntity != null && responseEntity.hasBody())
            {
                Thread.sleep(3000);
                LOGGER.info("Total time to retrieve results = {}",
                        System.currentTimeMillis() - startTime);
                return responseEntity.getBody();
            }
        }
        catch (URISyntaxException | InterruptedException e)
        {
            LOGGER.error("URI has a wrong syntax", e);
        }

        LOGGER.info("No result found, returning an empty list");
        return new ArrayList<>();
    }

Короче говоря, мой контур автоматического выключателя будет вызывать удаленный сервис достаточное количество раз, чтобы преодолеть порог в 65 процентов медленных вызовов продолжительностью более 3 секунд. Как только я нажму на ссылку here, я получу результат, но мой автоматический выключатель будет разомкнут и не будет разрешать дальнейшие вызовы, пока он будет в состоянии half-open либо closed.

Вы сожете заметить, что мы начали получать исключение, CallNotPermittedException, когда автоматический выключатель был в состоянии OPEN. Кроме того, выключатель был отключен при выполнении 10 вызовов. Это потому, что размер нашего скользящего окна равен 10.

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

Теперь давайте переключим COUNT_BASED автоматический выключатель на TIME_BASEDавтоматический выключатель. В TIME_BASED автоматическом выключателе мы отключим наш REST-сервис через секунду, а затем щелкнем hereссылку с домашней страницы. Если 70 процентов вызовов за последние 10 секунд не работают, наш автоматический выключатель сработает.

Поскольку REST-сервис закрыт, мы увидим следующие ошибки в Circuitbreakdemo приложении

Мы увидим несколько ошибок до того, как автоматический выключатель будет в OPENсостоянии.

Еще одна конфигурация, которую мы всегда можем добавить, как долго мы хотим держать выключатель в разомкнутом состоянии. Для демонстрации я добавил, что автоматический выключатель будет в разомкнутом состоянии в течение 10 секунд.

Как обращаться с OPEN выключателями?

Возникает вопрос, как обращаться с OPEN выключателями? К счастью, resilience4jпредлагает резервную конфигурацию с утилитой Decorators. В большинстве случаев вы всегда можете настроить ее так, чтобы получить результат предыдущих успешных результатов, чтобы пользователи могли работать с приложением.

Вывод

В этом посте я рассказал, как использовать автоматический выключатель в приложении Spring Boot. Код для этой демонстрации доступен здесь .

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

Рекомендации для прочтения

  1. Библиотека Resilience4J

  2. Автоматический выключатель с Resilience4j -  Circuit Breaker

Источник: https://habr.com/ru/post/544074/


Интересные статьи

Интересные статьи

Мы провели исследование микроконтроллера Espressif ESP32 на предмет устойчивости к атакам, выполняемым методом внесения сбоев в работу чипов (Fault Injection). Мы постепенно шли к том...
Когда в IT-компании работают 6 человек, которые пилят одну систему и обсуждают её в кулуарах, описание системы и документация кажутся ненужными. Но когда систем уже более 100, без опи...
Всем привет, меня зовут Олег, я техлид в ДомКлике. В нашей команде ядром стека является Kotlin и Spring Boot. Хочу поделиться с вами своим опытом по взаимодействию и особенностях ра...
Привет, Хабр. В этой статье я хочу рассказать о своем опыте создания учебной среды для экспериментов с микросервисами. При изучении каждого нового инструмента мне всегда хотелось его попробова...
source Близится сезон долгожданный сезон летних отпусков, и многие уже выбрали для себя то самое желанное туристическое направление, которое давало силы месяцами продираться сквозь дебри дед...