brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 Oct 05. 2019

서킷 브레이커 with Hystrix, Feign

스프링 환경에서 구현한 Circuit Breaker(서킷 브레이커)에 대해서 공유합니다. Hystrix 와 Feign 등의 기술을 함께 사용하였습니다. 잘못된 내용은 댓글로 의견 부탁드립니다. 


스프링 애플리케이션 시스템에서 Hystrix 는 일반적으로 Spring Cloud 환경에서 함께 구축됩니다. 그래서, Hystrix 만 단독으로 사용하는 경우보다는 Eureka, Config, Ribbon, Zuul 등 스프링 클라우드 인프라 환경에서 함께 구축됩니다. 이 글에서는, 스프링 클라우드 기반의 MSA 인프라에 대해서 자세하게 다루지 않을 예정이며, Spring Cloud Hystrix 에 대해서 집중해서 글을 작성하였습니다. 


Circuit Breaker Pattern (서킷 브레이커)


마이크로서비스 아키텍처 패턴 중 Circuit Breaker Design 에 대해서 간단하게 알아보자. 


참고자료

https://brunch.co.kr/@springboot/41

https://microservices.io/patterns/reliability/circuit-breaker.html

https://docs.microsoft.com/ko-kr/azure/architecture/patterns/circuit-breaker


서킷브레이커 패턴

웹서비스는 여러 개의 애플리케이션의 조합으로 구축되는데, 특히 마이크로서비스 아키텍처에서 더욱 그렇다. 클라이언트에서 A 라는 서비스를 호출하는데, A 는 B 라는 서비스에서 데이터를 조회한다.  

만약 B 라는 서비스가 문제가 발생하면 어떻게 될까? B 서버에서 응답지연이 발생하고 있다면, 아래 그림과 같이 응답 지연은 클라이언트까지 전달될 것이다. 이런 아키텍처 환경에서는, 서비스 장애는 전파된다. 

B 서비스가 장애가 났을 때 장애가 전파되지 않도록 하기 위해서 Circute Breaker 를 A 와 B 사이에 추가할 수 있다. 물론, 장애가 나지 않는 상황에서도 아래 그림과 같이 서킷브레이커를 통해서 B 를 호출한다. 

만약, B 서비스가 장애로 인해서 응답지연이 발생하면, 서킷브레이커는 B 로의 응답을 기다리지 않고 서킷브레이커에서 중간에서 응답해버린다. 아래 그림과 같다. 

이런 아키텍처 환경으로 클라이언트에는 응답 지연이 발생하지 않는다. 즉, 서비스 장애 전파를 막을 수 있다. 하지만, B 서비스의 장애가 장시간 지속되면 어떻게 될까? 서비스 B에 향하는 호출 응답 지연은 계속 발생할 것이다. 물론, 서킷 브레이커에서 중간에 응답을 하기 때문에 클라이언트까지 장애가 전파되지는 않지만, 서킷브레이커에 설정된 타임아웃 시간까지는 기다릴 수 밖에 없는 상황이고, B 서비스가 장애가 났음에도 일단 B를 계속 호출하게 된다. 설정에 의해 정한 임계치를 넘어서는 순간 서킷브레이커는 OPEN 상태가 된다. 아래 그림과 같이, OPEN 상태에서 A 서비스는 B 서비스로 호출을 하지 않는다. 즉, 모든 요청을 서킷브레이커에서 응답해버린다. 

물론, 서킷 브레이커는 영원히 OPEN 상태를 유지하지는 않는다. 정해진 시간이 지나면, B 서비스로 호출해보고 정상 응답을 하면 서킷을 CLOSE 한다. 만약 계속 B 서비스 응답이 없으면 계속 OPEN 상태를 유지할 것이고, 주기적으로 서비스 정상 여부를 체크할 것이다. 


간단하게 설명해봤는데, 사실 상용 서비스에서 서킷브레이커는 더 복잡한 상황이 많이 발생한다. 


Hystrix


MSA 기반 아키텍처로 가장 유명한 회사는 어디일까? 바로, 넷플릭스가 떠오를 것이다. Hystrix 는 넷플릭스에서 개발한 오픈소스이다. 


Hystrix

자세한 설명은 생략한다.

https://github.com/Netflix/Hystrix


Hystrix, Circuit Breaker 테스를 위한 시스템 구성도 

간단하게 스프링 기반 애플리케이션에서 Hystrix 를 구현해보자. 클라이언트는 Rest API 호출하는데, Rest API 는 Contents API 에서 데이터를 조회한다. 아래 그림과 같다. Rest API 에서는 RestTemplate 를 사용하고, Circuit Breaker 가 구현을 한다. Hystrix 라이브러리가 Circuit Breaker 역할을 한다. 


Contents API

Contents API 는 아주 심플한 API 서버이다. RestController 만 존재하고 포트는 8081 로 구성하였다. 

서버 지연을 흉내내기 위해서, 강제로 지연 시간을 설정하였다. 

localhost:8081/api/coffees  를 호출하면 아래와 같이 응답을 하는 API 서버이다. 커피의 리스트를 조회하는 기능을 담당한다. 

단, 400ms 지연시간이 설정하였기 때문에 응답 시간이 느린 편이다. 


반면에, 아래 컨트롤러는 지연시간이 없다. 30ms 지연시간으로 아주 빠르지는 않지만 적당한 시간에 응답을 한다. 참고로 "쥬스"의 리스트를 조회하는 기능이다. 

localhost:8081/api/juices  

https://github.com/sieunkr/spring-cloud/tree/master/test-c


Rest API 기본 구성

Contents API 의 데이터를 조회해서, 사용자에게 직접 제공하는 Rest API 서버를 만들어보자. 해당 Rest-API 서버에 Hystrix 및 서킷 브레이커가 연동된다. 스프링 부트 2.1.8.RELEASE 버전 환경이며, 스프링 부트 스타터 웹, hystrix, lombok 등의 디펜던시를 추가한다. 추가로, httpclient 는 RestTemplate 의 ThreadPool 설정을 위해서 추가하였다. 

RestTemplate 를 역할에 맞게 Bean 컨피그를 구성한다. 각각의 메서드에서 coffeeRestTemplate 와 juiceRestTemplate 라는 이름의 빈(Bean)을 생성한다. 이때 중요한 것은, Pool 설정을 커스텀하게 설정하였다. 따로 설정하지 않으면 RestTemplate의 기본 PoolSize 를 사용하기 때문에 성능 이슈 및 병목 현상이 발생할 수 있다. 

물론, 쓰레드풀 사이즈는 너무 많아도, 너무 적어도 문제다. 너무 적은 설정은 병목현상이 발생할 수 있지만, 오히려 너무 높게 설정하면 서버에 심각한 장애를 유발할 수도 있다. 부하테스를 하면서 적절한 수치를 찾아야 한다. 

아무튼, Rest API 컨트롤러 엔드포인트를 작성하자. 

localhost:8080/test/delay  조회하면 400 ms 응답시간으로, 데이터가 조회된다. Contents API 에서 400 ms 지연시간이 발생하기 때문에, Rest API 에서도 지연 시간이 전파된다. 


마이크로서비스 환경에서 API 서버 통신에서 응답 지연이 발생하면, 시스템 성능을 전체적으로 저하시킨다. 아래와 같이 Contents API 에서의 지연시간은 RestAPI, 클라이언트 까지 전파된다. 


만약, Contents API 에서 지연시간이 발생하는 경우, 장애 전파를 방지할 수 있을까? 이런 경우에 Hystrix 를 사용하면 된다. 

참고로, Hystrix 는 더 많은 기능이 있다. 필자의 이 글에서 소개하는 기능이 전부가 아니라는 점을 이해하길 바란다. 각자 공부를 해서 배워가길 바란다. 


Rest API & Hystrix, Circuit Breaker

Hystrix 를 연동해서 서킷 브레이커를 구축하자. @EnableCircuitBreaker 어노테이션을 추가해서 서킷 브레이커가 작동할 수 있도록 초기화한다.

application.yml 에 hystrix 설정을 추가한다. 타임아웃 설정을 200 ms 로 설정하였다. Hystrix 는 200ms 이상으로 지연되는 경우 서킷의 fallback 으로 전환할 것이다. 물론, 단 한번의 fallback 으로 인해서 서킷이 열리지는 않는다. 몇번 에러가 쌓이게 되면 서킷은 오픈될 것이다. 에러 발생률에 따른 서킷 오픈 여부 설정도 application.yml 에 설정할 수 있다. 

실제로 사용은 아래 샘플과 같이, RestTemplate 를 사용하는 곳에서 @HystrixCommand 어노테이션을 추가하면, Hystrix 를 연동할 수 있다. 

사실, HystrixCommand  를 그대로 사용할수도 있지만, HystrixCommand  를 구현해서 커스텀하게 구현할수도 있다. 또한 HystrixCommand 의 commandProperties  속성을 추가해서 시스템 상황 및 요구사항에 맞게, 설정을 변경할 수 있다. 자세한 내용을 이 글에 전부 작성하기에는 무리가 있으니 관련 자료를 찾아보길 바란다. 


어쨋든, Hystrix 설정 후 애플리케이션을 다시 구동하면 어떻게 될까? 초반 수차례 fallback 로 전환되다가 어느순간이 지나면 서킷이 오픈될 것이다.서킷이 오픈되면 장애가 발생한 서버로 호출을 아예하지 않고, 서킷 브레이커에서 모든 요청을 반환해버린다.  물론, 시간이 어느정도 지나면 서킷을 닫아도 되는지 다시 체크할 것이다. 서비스 장애가 해결되어서 지연시간이 없어지면 서킷은 다시 닫히고, 정상적으로 서비스 요청을 하게 된다.  


실제로, API 를 호출해보면 아래와 같이 fallback 으로 전환하는 것을 확인할 수 있다. 로그를 확인해보니 HystrixTimeOutException 에러를 확인할 수 있다. 

하지만, 아직 서킷이 오픈되지는 않았다. 요청을 겁나게 많이 떄려보면, 서킷이 오픈될 것이다. 

아래 그림과 같이 서킷이 오픈되었기 때문에, Rest API 에서 Contents API 로 요청은 하지 않는다. 서킷브레이커가 막아준다. 

어느정도 시간이 지난 후에, 타겟 서버가 정상임이 확인되면 다시 서킷을 Close 할 것이다. 서킷이 Close 가 되었다는 의미는 다시 서비스가 정상적으로 시작할 수 있다는 의미다. 참고로, 해당 시스템은 400ms 의 지연시간을 강제로 설정한 상황이기 때문에, 서킷은 계속 오픈된 상태로 유지될 것이다. 왜냐하면 RestAPI 서버에서는 200ms 이상이면 FallBack 를 전환하도록 설정했기 때문이다. 

https://github.com/sieunkr/spring-cloud/tree/master/spring-cloud-hystrix


Spring Cloud OpenFeign 에서의 Hystrix


스프링 클라우드 환경에서 Feign 은 기본적으로 Hystrix 를 함께 사용한다. 프로퍼티 설정에서 Hystrix 를 사용하지 않도록 설정할 수도 있지만, 대부분의 Feign Client 에서는 Hystrix 를 함께 사용한다. 물론, Hystrix 뿐만 아니라, Eureka, Config Server, Zuul 등 MSA 기반 다양한 모듈이 함께 사용되지만, 이 글에서는 복잡한 설정은 빼고, 간단하게 Feign 과 Hystrix 에 대해서만 정리한다. 또한, 이 글에서는 CompletableFuture 를 사용해서 위 샘플 사례보다 코드는 더 복잡해졌다. 


RestTemplate 대신 Feign Client 를 사용

위 샘플에서는 RestTemplate 를 사용하였지만, 이번 샘플은 Feign Client 를 사용한다. 


기본 셋팅

spring-cloud-starter-openfeign 디펜던시가 추가되었다. 또한 hystrix 대시보드를 사용하기 위해서 spring-cloud-starter-netflux-hystrix-dashboard 디펜던시도 함께 추가하였다. 

hystrix 를 사용하기 위해서, @EnableHystrix 어노테이션을 선언해주면 된다. 


Feign Client 설정

feign client 에 대해서는 지난번에 작성한 필자의 글을 읽어보면 된다. 

https://brunch.co.kr/@springboot/202

아래와 같이 코드를 작성한다.


지금부터는 글도 복잡해지고, 코드도 클린하지 않다. 글이 지저분하다고 생각된다면 이 글은 이제 그만 읽어도 된다. 이 글을 읽는 개발자는, 이 글을 통해서 서킷 브레이커 및 Hystrix  가 무엇인지 이해했다면 그정도로 충분하다. 


이어지는 코드는... 읽는 것을 추천하고 싶지가 않다. 하지만, 심심한 개발자는 한번 읽어보고 피드백을 해주는 것도 필자에게 도움이 될 것이다. 


Service 구현

CompletableFuture 를 사용해서 비동기 메서드를 작성한다. 이 글에서는 getAll 메서드만 사용하는데 커피, 쥬스 등 모든 데이터를 조합해서 제공한다.

데이터 조합을 하기 위해서 CompletableFuture 의 allOf 를 사용한다. 

비동기 논블록킹 메서드를 제공하기 위해서, CompletableFuture<타입> 을 리턴한다. CompletableFuture 에 대한 설명은 역시 필자의 지난 글을 참고하길 바란다.

https://brunch.co.kr/@springboot/267

컨트롤러 엔드포인트에서 데이터를 조회해서 제공한다. 

참고로, join 메서드를 사용했기 때문에 블록킹이 발생한다. 만약, 서블릿 단계까지 넌블록킹으로 구현하고 싶다면, 스프링 웹플럭스를 사용하고 Flux 또는 Mono 로 반환해주면 된다. 자세한 내용은 생략하며, 나중에 웹플럭스 관련 글을 작성할 때 다시 얘기하겠다. 조금 허접하지만, 관심이 있는 개발자는 역시 필자의 예전 글을 읽어라. 

https://brunch.co.kr/@springboot/96

아참, 추가로 웹플럭스를 이해하기 위해서는 리액티브 스트림 및 Project Reactor 에 대해서 먼저 알아야 한다.

 https://brunch.co.kr/magazine/reactor


뭐가 이렇게 알아야하는게 많은지...... 힘들다


암튼, Rest API 를 호출하면 잘 응답하고, 기능은 잘 동작한다. 

CompletableFuture의 allOf 는 모든 CompletableFuture 가 컴플리트 되면 동작한다. 즉, 쥬스를 조회하는 juiceClient.getJuice 가 30ms 내로 응답하지만, coffeeClient.getCoffees 가 400ms 로 응답 지연되기 때문에, 전체 데이터를 조회하는 시간은 400ms 가 넘는 시간이 걸릴 수 밖에 없다. 물론, 병렬 처리를 하기 때문에 400 ms + 30ms 가 아니라 딱 400ms 가 걸린다. 순차적으로 호출하게 되면 응답 시간은 더 길어질 것이다. 그래서, 필자는 병렬 처리를 적용하기 위해서, CompletableFuture 를 사용해서 쓰레드풀하게 동작하도록 구현한 것이다. 비록, 쓰레드풀이 너무 많아지면, CPU 가 겁나 높아지고, 메모리 사용률이 증가하기 떄문에 주의가 필요하다. 또한, 요청이 많지도 않은데 많은 쓰레드풀을 설정하는것도 idle 쓰레드풀을 생성하기 때문에 매우 좋지 않은 설계이다. 판단하기 어렵지만, 가장 좋은 방법은 성능테스트를 해서 적합한 풀 사이즈를 찾는 것이 좋다. CompletableFuture 의 supplyAsync 를 실행할 때 ThreadPoolTaskExecutor 을 설정하였는데, 해당 설정은 따로 Config 클래스로 빼서 Bean 을 생성해준다. 

아래 샘플 코드와 같이, Feign CLient 를 호출하는 supplyAsync 구문에 ThreadPoolTaskExecutor 를 설정하였다. 만약 설정하지 않으면 ForkJoinPool의 commonPool 를 사용한다. commonPool 은 제한된 풀 설정으로 인해서 성능이 나지 않을 가능성이 높으니 좋은 방법은 아니다. 필자가 구현한 것처럼, 별도로 커스텀하게 풀을 설정해주는게 좋다. 

추가로, allOf 의 thenApply 메서드도 thenApplyAsync 라는 메서드로 대체할수도 있다. thenAPplyAsync 메서드 역시 쓰레드풀 설정을 별도로 안해주면 commonPool 를 사용한다. 하지만, 해당 구문에서는 thenApply 는 CompletableFuture 가 완료된 이후에 데이터를 조합하는 역할만 하기 때문에 별도로 쓰레드를 생성할 만큼 부담가는 작업은 아니다. 그래서 thenApply 메서드를 사용하였고, thenApply 메서드는 해당 구문을 실행하는 이전 실행의 쓰레드를 그대로 사용할 것이다. 즉, 위에서 실행한 supplyAsync 에서 사용했던 쓰레드풀을 그대로 사용한다. 아마 로그를 찎어보면 필자가 선언한 CustomThreadPoolTaskExecutor 풀이 찍힐 것이다. 어쨋든, 이 글이 점점 잡다해지고 복잡해지고 있는데, 최종적으로 CompletableFUture 를 사용한 이유는 병렬 호출로 Feign 에 많은 요청을 실행하기 위해서이다. 


자, 이제 성능테스트를 해보자.  잼있는 상황이 발생할 것이다. 

https://github.com/sieunkr/spring-cloud/tree/master/spring-cloud-openfeign-hystrix


성능테스트 및 Thread Pool 

비록 400ms 지연시간이 있지만, CompletableFuture 로 병렬 프로그래밍을 구현하였고, FeignClient 의 풀 설정도 넉넉하기 때문에 어느정도 성능이 나올것 같다는 추측이었지만 실제로 성능이 좋지 않았다. 초반 몇번 호출을 하다가 바로 에러가 발생한다. 

Hystrix 대시보드에서 확인해보면 더 명확하게 확인이 가능하다. getCoffees 메서드는 open 과 close 를 반복하면서 아주 상태가 안좋다는 것을 알 수 있다. 

결론부터 얘기하면, hystrix 의 thread pool 사이즈가 기본 10이라서 발생하는 문제이다. hystrix 를 사용하는 경우 반드시 쓰레드풀 설정을 확인해야한다. 기본 10이라는 수치는 실서비스 환경에서 사용하기에는 너무 낮은 수치일 수 있다. 이 경우 할 수 있는 방법은 간단하다. 타겟 서버의 응답시간을 빠르게 개선하여 10개의 쓰레드로도 충분히 처리 가능하도록 하거나, 아니면 쓰레드풀 사이즈를 늘리거나, 


해당 상황을 개선하기 위해서, Hystrix 쓰레드풀 설정을 변경하자. 아래와 같이 변경할 수 있다. 

쓰레드풀 사이즈를 100으로 설정한 이후 테스트를 해보면, 동시접속자가 많아져도 나름 잘 버티고 고, Active Thread 가 거의 최대 수치인 100에 가까워지고 있다. TPS 도 200 까지 나오는 것을 확인할 수 있다. 


사실 JMeter 는 100% 믿기는 어려운건 함정...  괜찮은 APM 모니터링 툴을 사용해서 정확히 측정하길 바란다. JMeter의 수치를 100% 신뢰할 수 없다.




더 많은 트래픽을 주면 어떻게 될까?

동시접속자수를 더 높이면 어떻게 될까? 동시접속자를 300으로 하고 Loop Count 를 Forever 로 설정해서 트래픽 부하를 날려보자. 참고로 필자의 노트북 성능이 좋지는 않다. 

에러가 많이 되고 있다. 하지만, 서킷이 열리는 상황까지는 가지 않는다.

서킷이 오픈되지 않는 이유는, 서킷 오픈을 위한 에러율이 기준을 넘지 않았기 때문이다. 서킷 오픈을 위한 에러율을 조정해보자. 아마도 기본 설정은 50% 일 것이다. 필자의 해당 테스트 환경에서는 에러가 50% 이상 발생하지는 않고 있다. 

https://github.com/Netflix/Hystrix/wiki/Configuration#circuitbreakererrorthresholdpercentage

에러율을 30% 로 변경하고 같은 조건으로 부하를 주기 시작하면, 서킷이 Open,Close 를 반복한다. 

hystrix coreSize 를 100 으로 하고, 부하를 주면 30% 이상 오류가 발생하기 때문에 서킷이 오픈이 되는 상황이다. 필자의 노트북 CPU 는 77% 를 달리고 있고, 많이 힘들어한다. 


더 복잡한 상황...의문, 어떻게 구축하는게 좋을까?

CPU가 꽤 높게 올라가는 상황이므로 더이상 쓰레드를 올리는 건 무리가 있어 보이지만, 재미삼아 한번 더 올려보자 어떻게 될까? 코어 사이즈를 250으로 올려보자. 

이번에는 CompletableFuture 에서 Exception 을 발생시키고 있다. 

이번에는 CompletableFuture 의 Pool 설정에서 Reject 를 하고 있는 상황이다. 


Hystrix 에서 많은 부하를 받을 수 있도록 coreSize 를 올리니깐, CompletetableFuture 에 더 많은 요청이 오게 되고 결국 CompletableFuture 구문에서 Reject 가 발생하게 된 것이다. 이런 상황에서 더 많은 처리를 하기 위해서는 일단 Contents-API 가 더 많은 요청을 받을 수 있도록 임베디드 톰캣의 쓰레드 설정을 추가해야 한다. 물론, 해당 상황에서 CompletableFuture 의 쓰레드풀 사이즈를 늘리면 되지만, CPU 가 70%가 넘어가는 상황이고, 임베디드 톰캣에서 받아줄 수 있는 소켓의 한계도 넘어가게 될 것이다. 필자의 노트북에서는 이정도 수준이 최대치라고 생각되어 더이상 성능을 좋게하기 위해서 Thread 를 늘리는 것은 의미가 없어 보인다. 사실, 현실적인 방법으로는 노드를 늘리는 방법이 더 효율적일 것이다. 


참고로, CompletableFuture 의 쓰레드풀 설정에서, Reject 되지 않고 대기하는 방식으로 설정을 변경할 수도 있다. THreadPool 관련해서는 나중에 각잡고 글을 다시 작성하겠다. 


생각보다는 상황이 복잡해서, 글로 설명하기 많이 어려운 내용이다. 간략하게 정리해보면 CompletableFuture 에서 호출하는 Feign 호출 방식은, CompletableFuture 에 설정한 쓰레드풀 사이즈와 Hystrix 의 풀사이즈와 매우 깊은 관계가 있다는 사실을 알 수 있다. 


해당 상황에 대한 정확한 해답을 찾지 못하였다. 아이디어가 있는 개발자는 알려주길 바란다. 


글 마무리


서킷브레이커를 검토하기 위해서 Hystrix 를 사용하고 부하를 주기 위해서 CompletableFuture 를 사용하였는데, 글이 많이 복잡해졌다. CompletableFuture , Feign, Hystrix 를 함께 연동해서 안정적인 시스템을 구축하는 방법에 대해서 고민 중이다. 경험이 많은 개발자는 꼭 피드백을 남겨주길 바란다. 

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari