[Java] Как использовать шаблон Circuit Breaker в приложении Spring Boot (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
26-Фев-2021 16:33

В этом посте я покажу, как мы можем использовать шаблон 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 позволяет сохранять эти события с метриками, которые можно отслеживать с помощью системы мониторинга.Рекомендации для прочтения
===========
Источник:
habr.com
===========

===========
Автор оригинала: Yogesh Mali
===========
Похожие новости: Теги для поиска: #_java, #_spring_boot, #_patterns, #_circuit_breaker, #_resilience, #_java
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 10:36
Часовой пояс: UTC + 5