Spring Boot 애플리케이션에서 회로 차단기를 사용하는 방법

Pixabay로부터 입수 된 Jürgen Diermaier 님의 이미지입니다.

이 게시물에서는 Spring Boot 애플리케이션에서 Circuit Breaker 패턴을 사용하는 방법을 보여줍니다. 회로 차단기 패턴이라고하면 건축 패턴입니다. Netflix는 회로 차단기를 처리하기 위해 Hysterix 라이브러리를 게시했습니다. 이 게시물의 일부로 Spring Boot 애플리케이션에서 resilence4j 라이브러리를 사용하여 회로 차단기 패턴을 사용하는 방법을 보여줄 것 입니다.

다른 소식으로 최근에 Simplifying Spring Security를 발표했습니다 . Spring Security에 대해 배우고 싶다면 여기에서 구입할 수 있습니다 .

회로 차단기 란?

회로 차단기의 개념은 전기 공학에서 비롯됩니다. 대부분의 전기 네트워크에서 회로 차단기는 전류 과부하 또는 단락으로 인한 손상으로부터 네트워크를 보호하는 스위치입니다.

마찬가지로 소프트웨어에서 회로 차단기는 해당 원격 서비스에 대한 호출이 실패하거나 시간 초과 될 것임을 알고 있으면 원격 서비스에 대한 호출을 중지합니다. 이것의 장점은 리소스를 절약하고 원격 프로 시저 호출 문제를 사전에 해결하는 것입니다.

회로 차단기는 이전 통화 내역을 기반으로 통화 중지를 결정합니다. 그러나 호출을 처리 할 수있는 다른 방법이 있습니다. 일반적으로 이전 통화를 추적합니다. 5 개 통화 중 4 개가 실패했거나 시간이 초과되었다고 가정하면 다음 통화가 실패합니다. 이를 통해 서비스 호출시 오류를보다 적극적으로 처리하고 호출자 서비스가 다른 방식으로 응답을 처리 할 수 ​​있으므로 사용자가 오류 페이지와 다른 방식으로 애플리케이션을 경험할 수 있습니다.

원격 서비스 호출이 특정 시간 동안 실패 할 경우 회로 차단기가 작동 할 수있는 또 다른 방법입니다. 회로 차단기가 열리고 원격 서비스가 오류를 개선 할 때까지 다음 호출을 허용하지 않습니다.

Resilience4J 라이브러리

원격 서비스라고 부르는 코드가 있습니다. resilience4j라이브러리 의 회로 차단기 모듈 supplier에는 원격 서비스 호출 또는 원격 서비스 호출에서 값 검색에 대한 람다식이 있습니다. 나는 이것을 예제의 일부로 보여줄 것입니다. 회로 차단기는 응답을 추적하고 상태를 전환 할 수 있도록이 원격 서비스 호출을 장식합니다.

Resilience4j 라이브러리의 다양한 구성

회로 차단기 개념을 이해하기 위해이 라이브러리가 제공하는 다양한 구성을 살펴 보겠습니다.

slidingWindowType()-이 구성은 기본적으로 회로 차단기가 어떻게 작동할지 결정하는 데 도움이됩니다. 두 가지 종류가 있습니다 COUNT_BASEDTIME_BASED. COUNT_BASED회로 차단기 슬라이딩 창은 원격 서비스에 대한 호출 수를 고려하고 TIME_BASED회로 차단기 슬라이딩 창은 특정 기간 동안 원격 서비스에 대한 호출을 고려합니다.

failureRateThreshold()-실패율 임계 값을 백분율로 구성합니다. x %의 호출이 실패하면 회로 차단기가 열립니다.

slidingWindowSize() -이 설정은 회로 차단기를 닫을 때 고려할 통화 수를 결정하는 데 도움이됩니다.

slowCallRateThreshold()-느린 통화 속도 임계 값을 백분율로 구성합니다. x %의 통화가 느린 경우 회로 차단기가 열립니다.

slowCallDurationThreshold -통화가 느린 것으로 간주되는 시간 기간 임계 값.

minimumNumberOfCalls() -회로 차단기가 오류율을 계산하기 전에 필요한 최소 호출 수.

ignoreException() -이 설정을 사용하면 회로 차단기가 무시할 수있는 예외를 구성 할 수 있으며 원격 서비스 호출의 성공 또는 실패에 포함되지 않습니다.

waitDurationInOpenState()-회로 차단기가 반 개방 상태로 전환되기 전에 개방 상태를 유지해야하는 기간입니다. 기본값은 60 초입니다.

카운트 기반 회로 차단기

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");

CircuitBreakerRegistry 회로 차단기를 만드는 공장입니다.

시간 기반 회로 차단기

이제 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");

Spring Boot 애플리케이션의 회로 차단기 예

회로 차단기에 대한 필수 개념을 다루었습니다. 이제 Spring Boot 애플리케이션에서 회로 차단기를 사용할 수 있음을 보여 드리겠습니다.

한쪽에는 BooksApplication기본적으로 도서관 도서의 세부 정보를 저장 하는 REST 애플리케이션 이 있습니다. 반면에 Circuitbreakerdemo.NET을 사용하여 REST 애플리케이션을 호출 하는 애플리케이션 이 RestTemplate있습니다. 회로 차단기를 통해 REST 호출을 장식합니다.

BooksApplication책에 대한 정보를 MySQL 데이터베이스 테이블에 저장합니다 librarybooks. 이 애플리케이션의 REST 컨트롤러에는 GETPOST메서드가 있습니다.

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<Book>> getAllBooks(@RequestParam(required = false) String bookTitle)
    {
        try
        {
            List<Book> 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<Book> getBookById(@PathVariable("id") long id)
    {
        try
        {
            Optional<Book> 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<Book> 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<Book> updateABook(@PathVariable("id") long id, @RequestBody Book book)
    {
        Optional<Book> 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<HttpStatus> 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);
        }
    }
}

데모 목적으로, 저는 서비스 클래스에서 사용할 별도의 빈에 CircuitBreaker를 정의했습니다.

@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)
            .waitDurationInOpenState(Duration.ofSeconds(10))
            .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 timeCircuitBreaker;

    @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<Book>> booksSupplier =
                timeCircuitBreaker.decorateSupplier(() -> bookManager.getAllBooksFromLibrary());

        LOGGER.info("Going to start calling the REST service with Circuit Breaker");
        List<Book> 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";
    }
}

에 대한 빈을 autowired했습니다 countCircuitBreaker. 데모 목적으로-모든 책을 가져 오기 위해 루프에서 REST 서비스를 15 번 호출 할 것입니다. 이렇게하면 REST 서비스 측에서 중단을 시뮬레이션 할 수 있습니다.

우리의 회로 차단기는 원격 서비스에 대한 REST 호출을 수행하는 공급자를 장식하고 공급자는 원격 서비스 호출의 결과를 저장합니다.

이 데모에서는 REST 서비스를 순차적으로 호출하지만 원격 서비스 호출도 병렬로 발생할 수 있습니다. 회로 차단기는 순차적 또는 병렬 호출에 관계없이 결과를 계속 추적합니다.

데모

이제 라이브 데모에서 회로 차단기가 어떻게 작동하는지 살펴 보겠습니다. 내 REST 서비스는 포트 8443에서 실행되고 내 Circuitbreakerdemo응용 프로그램은 포트 8743에서 실행됩니다.

처음에는 두 응용 프로그램을 모두 시작하고 응용 프로그램의 홈 페이지에 액세스합니다 Circuitbreakerdemo. 홈 페이지에는 상점의 모든 책을 볼 수있는 링크가 있습니다.

이제 몇 가지 오류를 시뮬레이션하기 위해 REST 호출 결과를 반환하기 전에 기본적으로 3 초 동안 절전 모드를 유지하는 RestTemplate 호출에 다음 코드를 추가했습니다.

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

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

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

CallNotPermittedException회로 차단기가 OPEN상태 에있을 때 예외가 발생하기 시작했음을 알 수 있습니다. 또한 10 번의 호출이 수행 될 때 회로 차단기가 열렸습니다. 슬라이딩 윈도우 크기가 10이기 때문입니다.

또 다른 방법으로 REST 서비스 또는 데이터베이스 서비스를 종료하여 오류를 시뮬레이션 할 수 있습니다. 이렇게하면 REST 호출이 필요한 것보다 오래 걸릴 수 있습니다.

이제 COUNT_BASED회로 차단기를 TIME_BASED회로 차단기로 전환 해 보겠습니다 . 에서 TIME_BASED회로 차단기, 우리는 초 후에 우리의 REST 서비스를 전환 할 것이며, 우리는 클릭합니다 here홈 페이지에서 링크. 지난 10 초 동안 호출의 70 %가 실패하면 회로 차단기가 열립니다.

REST 서비스가 종료되었으므로 Circuitbreakdemo애플리케이션에 다음 오류가 표시 됩니다.

회로 차단기가 OPEN상태 가되기 전에 오류 수를 볼 수 있습니다 .

하나의 구성은 회로 차단기를 개방 상태로 유지하려는 기간을 항상 추가 할 수 있습니다. 데모를 위해 회로 차단기가 10 초 동안 열린 상태에있을 것이라고 추가했습니다.

개방 회로 차단기를 처리하는 방법은 무엇입니까?

한 가지 질문이 생깁니다. 개방형 회로 차단기를 어떻게 처리합니까? 다행히도 유틸리티 resilience4j가 포함 된 대체 구성을 제공 Decorators합니다. 대부분의 경우 사용자가 계속해서 응용 프로그램을 사용할 수 있도록 이전에 성공한 결과에서 결과를 얻도록 항상이를 구성 할 수 있습니다.

결론

이 게시물에서는 Spring Boot 애플리케이션에서 회로 차단기를 사용하는 방법을 다루었습니다. 이 데모의 코드는 여기에서 확인할 수 있습니다 .

이 데모에서는 resilience4j라이브러리가 모니터링 시스템으로 모니터링 할 수있는 메트릭과 함께 이러한 이벤트를 저장할 수 있도록 허용 하므로 이러한 회로 차단기 이벤트를 모니터링하는 방법을 다루지 않았습니다 .

이 게시물이 마음에 드 셨다면 여기 에서 내 블로그를 구독 해보세요 .

2021 년 2 월 21 일 https://betterjavacode.com처음 게시되었습니다 .

Suggested posts

적응 형 동시성 제어를 사용하여 NGINX로 부하 차단 — 2 부

적응 형 동시성 제어를 사용하여 NGINX로 부하 차단 — 2 부

Author 's Note :이 시리즈는 NGINX와 함께 적응 형 동시성 제어를 사용하여 서비스에서로드 차단을 구현하고 운영하는 방법에 대한 2 부작 시리즈입니다. 1 부에서는 컨텍스트 / 배경을 설정하고 2 부에서는 구현에 중점을 둡니다.

적응 형 동시성 제어를 사용하여 NGINX로 부하 차단 — 1 부

적응 형 동시성 제어를 사용하여 NGINX로 부하 차단 — 1 부

Author 's Note :이 시리즈는 NGINX와 함께 적응 형 동시성 제어를 사용하여 서비스에서로드 차단을 구현하고 운영하는 방법에 대한 2 부작 시리즈입니다. 1 부에서는 컨텍스트 / 배경을 설정하고 2 부에서는 구현에 중점을 둡니다.