CircuitBreaker를 이용한 외부 API 장애 관리

Pir·2022년 4월 3일
26
post-thumbnail

CircuitBreaker는 서비스메시의 쿠버네티스 Istio를 이용해서 인프라 레벨에서 적용가능하나, 이번 포스팅에선 Resilience4j를 이용한 어플리케이션 레벨에서 적용하겠습니다.

1. CircuitBreaker가 필요한 이유?

🔍 개요

  • 어플리케이션의 각각의 도메인이나 기능들을 모듈로 나누어 분산 서버로 아키텍처링하는 서버 구성이 점차 늘어나고 있습니다.
  • 이처럼 구성하면, 새로운 서비스 추가와 변경에도 용이하고 트래픽이 늘어나도 유연하게 대응할 수 있게됩니다.
  • MSA(Micro Service Architecture)에선 서로의 모듈이 의존함에 따라 한 모듈이 장애가 나면 다른 모듈에도 장애로 이어지는 것을 막기 위해 MSA 회복성 패턴 중 하나인 CircuitBreaker를 사용합니다.

🍀 기존 서버 구성

  • 아래와 같이 결제 서비스를 위한 아키텍처라고 가정하고, 정산 서버가 장애가 났을 경우 Pir 서버는 타 서버와의 통신이 영향을 받을까요?

  • 정답은 Yes입니다.
  • Pir 서버는 주문, 정산, 결제, 상품 서버와 모두 통신을 해야하는데, 아래 그림과 같이 정산 서버에서 장애가 남으로써 Connection Time, Read Time 등 Latency가 증가하기 때문에 Pir 서버가 가지고 있는 쓰레드는 정산 서버와 통신하는데에 많이 사용되는 것을 볼 수 있습니다.
  • 최종적으로, 모든 쓰레드는 정산 서버와의 통신에 몰리게 될 것이고, 정산 서버의 장애는 모든 서버와의 장애로 이어지게 됩니다.

2. CircuitBreaker란?

✏️ CircuitBreaker란?

  • 서킷브레이커는 해석 그대로 누전 차단기라는 뜻을 지닙니다. 누전 차단기는 전기 회로에서 과부하가 걸리거나 단락으로 인한 피해를 막기 위해 자동으로 회로를 정지시키는 장치라고 위키백과에서 표현하고 있습니다.
  • 서버에서 사용하는 서킷브레이커도 외부 API 통신의 장애 전파를 막기 위해 장애를 탐지하면 외부와의 통신을 차단하는 역할을 합니다.
  • 서킷브레이커가 실행(오픈)되면 Fail Fast 함으로써 외부 서비스가 장애가 나더라도 빠르게 에러를 응답 받을 수 있는 장점이 있으며 개발자가 지정한 행위를 리턴 받을 수 있습니다.

CircuitBreaker 구조도

  • Pir 서버에 서킷브레이커를 구성하여 주문&정산&결제&상품 서버와의 장애 여부를 확인하여 해당 서버와의 통신을 차단할 수 있습니다.
  • 아래와 같이 정산 서버가 장애일 경우, 그림에서는 "X" 표시를 해두었지만 실제로는 서킷 브레이커가 "오픈"한다 라는 표현을 사용합니다. 정산서버쪽의 서킷브레이커가 오픈함으로써 주문,결제,상품 서버와의 통신이 원활하게 진행될 수 있습니다.

CircuitBreaker 구성

  1. 외부 API 통신 시도
  2. 외부 통신이 실패함으로써 서킷브레이커 Open
  3. Open과 동시에 외부 서버에 요청을 날리지 않고, Fail Fast로 빠른응답 리턴
  4. 서킷브레이커가 오픈하면 일정 시간 후에 반오픈(Half-Open) 상태
  5. 반오픈 상태에서 다시 외부 서비스를 호출해서 장애를 확인하면 Open, 정상 응답이면 닫힘
  • 위에서 "장애 확인"이라고 표현한 것은, 총(n)번 통신 중 실패율(n%)를 지정할 수 있습니다.
  • ex) 10번 중 50% => 10번 중 6번이 에러 발생하면 서킷브레이커 Open

3. Resilience4j를 이용한 CircuitBreaker 적용

Resilience4j 선택 이유?

  • CircuitBreaker를 제공하는 라이브러리 중에 Netflix Hystrix와 Resilience4j를 찾아볼 수 있었습니다.
  • Netflix Hystrix는 공식적으로 2018년 앞으로 개발을 중단하고 유지보수 상태라는 글이 명시되어 있으며
  • Hystrix는 Java 6을 기반으로 만들어졌지만 Resilience4j는 Java 8을 기반이며, Hystrix와는 다르게 다른 라이브러의 의존성이 없어서 가볍습니다.

    Hystrix is no longer in active development, and is currently in maintenance mode.

CircuitBreaker 적용

💡 의존성 설정

ext{
	resilience4jVersion = '1.7.1'
}

dependencies {
	compile('org.springframework.boot:spring-boot-starter-webflux')
	compile('org.springframework.boot:spring-boot-starter-actuator')
	compile('org.springframework.boot:spring-boot-starter-aop')

	compile("io.github.resilience4j:resilience4j-spring-boot2:${resilience4jVersion}")
	compile("io.github.resilience4j:resilience4j-all:${resilience4jVersion}") 
	compile("io.github.resilience4j:resilience4j-reactor:${resilience4jVersion}")
}

💡 application.yml 설정

resilience4j:
  circuitbreaker:
    configs:
      default:
        slidingWindowSize: 10
        failureRateThreshold: 50
        waitDurationInOpenState : 10s
    instances:
      hgsssss:
        baseConfig: default
  • resilience4j에 관한 기능들을 application.yml에 선언형으로 설정할 수 있습니다.
  • 'hgsssss'라는 인스턴스 이름 지정
  • slidingWindowSize : 서킷브레이커가 닫힌 상태에서 기록할 sliding window 크기 설정
  • failureRateThreshold : 실패 비율 임계치를 백분율로 설정
  • waitDurationInOpenState : 서킷브레이커가 Open후 waitDurationInOpenState 시간만큼 지난 후 Half-Open 상태도 전환
  • 정리 : 10번 요청에서 실패율이 50%가 넘으면 서킷브레이커가 Open하고 Open 10초 후에 다시 Half-Open 상태로 전환

💡 Service 설정

@Slf4j
@Service
@RequiredArgsConstructor
public class CircuitService {
    private final Call call;
    private static final String DEFAULT_NAME = "hgsssss";
    private static final String FALLBACK_DEFAULT = "helloFallback";

    @CircuitBreaker(name = DEFAULT_NAME, fallbackMethod = FALLBACK_DEFAULT)
    public Mono<String> getHello(String name){
        return call.getApiHello(name);
    }

    private Mono<String> helloFallback(String name, Throwable t){
        log.error("Fallback : "+ t.getMessage());
        return Mono.just("fallback data");
    }
}
  • 외부 API를 호출하는 메서드 위에 @CircuitBreaker 어노테이션을 작성하여 application.yml에서 선언한 "hgsssss" 인스턴스 명 삽입합니다.
  • 서킷브레이커가 Open하면 실행하는 메서드를 fallbackMethod로 선언합니다.
  • fallbackMethod는 해당하는 메서드 파리미터도 fallbackMethod 파라미터로 같이 지정해줘야합니다.
  • ex) getHello(String name), helloFallback(String name)

💡 결과

  • 외부 서비스를 호출하다가 10번 중 50%가 넘는 실패 비율 임계치를 초과했기 때문에 6번째 요청엔 서킷브레이커가 Open하고 제가 지정한 fallbackMethod를 호출하는 것을 확인할 수 있습니다.
  • 위 캡처에 나오진 않았지만 서킷브레이커는 10초 후에 다시 Half-Open상태로 localhost:8081/hello/fail 요청을 수행하게 됩니다.

⭐ spring-actuator를 활용한 서킷브레이커 모니터링

  • 서킷브레이커가 닫혀있는 평소 상태

  • 서킷브레이커가 오픈된 상태

    failedCalls : 외부 API 호출을 실패하여 fallbackMethod가 실행된 횟수
    notPermittedCalls : 메서드 호출은 됐지만 서킷브레이커가 Open된 상태기 때문에 외부 API 호출을 하지 않은 횟수


느낀 점

💡

요즘은 개인 공부보다 회사 서비스를 더 효율적으로 개선하고 관리하는 데에 관심을 두고 있어서 재미있는 나날들을 보내고 있습니다.

각 모듈마다 장애 전파를 막을 수 있기 때문에 분산 서버를 운영한다면 꼭 필요한 기술이라고 생각합니다. 서킷브레이커를 공부하면서 저희팀에도 꼭 필요할 것 같아서, 공부 후에 팀원들에게 공유하고 리뷰해서 추후에 도입하기로 결정하였습니다.

서킷브레이커를 도입하려면 서비스의 외부 API를 호출하는 모든 기능을 정의 후에 어떤 메서드에서 사용하고, 어떠한 fallbackMethod를 구성할지 벌써부터 매우 흥미롭습니다. 아직 프로젝트에 적용해보지 않았기 때문에 실제 프로젝트에 적용 후 시행착오와 매니징 프로세스 구성 후 서킷브레이커를 어떻게 모니터링하고, 어떻게 관리할 지 추가적으로 포스팅할 예정입니다.

profile
흉내내는 사람이 아닌, 이해하는 사람이 되자

4개의 댓글

comment-user-thumbnail
2022년 4월 5일

제가 맡은 서비스에서도, API 에서 장애가 생기면, MVC 서빙되는 서버도 장애가 나고, DB 에서도 장애가 났던 경험이 있는데요.. CircuitBreaker 를 사용하면 문제점을 보완할 수 있을 것 같네요. 포스팅 감사합니다!

답글 달기
comment-user-thumbnail
2022년 4월 5일

항상 좋은 글 감사합니다. 한번 적용해봐야겠어요! 감사합니다. 적용하면서 모르는 부분있으면 연락드리겠습니다.

답글 달기
comment-user-thumbnail
2022년 7월 8일

안녕하세용. circuitbreaker에 대해 덕분에 많이 배우게 되었습니다. 깃허브를 통해서도 코드 보았는데요 혹시 getHelloName과 getIllegael 메소드가 정확히 어디쓰이는지 설명해주실 수 있을까요?

답글 달기
comment-user-thumbnail
2023년 4월 28일

감사합니다. 잘 사용하게 되었습니다.

답글 달기