장애를 위한 Retry & CircuitBreaker

YEON·2024년 1월 5일

before

서비스를 개발하다보면 일시적인 혹은 지속적인 장애가 발생한다.
에를 들면 api를 단순히 호출하는 경우, batch에서 step을 실행하던 경우, db에 접근하던 도중.. 등등 많은 경우 장애가 발생할 수 있다.
그럼 사용자에게 서비스 장애를 지속시키지 않고 빠르고 안정적으로 서비스를 다시 제공하기 위한 안전장치는 어떤 것들이 있을까?
fault-tolerance(장애허용)을 위해 사용하는 기술로 retry 와 circuitBreaker 을 알아볼 수 있다.

(* 여기서는 자세한 동작,사용 방법보다 큰 틀의 개념만 바라보고자 작성하였습니다. )



Retry

" 하나의 요청에 대해, 요청이 실패했을 경우 해당 요청을 다시 시도한다. 계속 실패할 경우 다른 응답을 반환한다. "

말 그대로, 요청이 실패하면 정의된 재시도 정책에 따라 지정된 횟수만큼 요청을 재시도를 시도하는 것이다.
일정 시간 간격 또는 백오프 전략을 사용하여 재시도 간격을 조절할 수 있다.

적합한 상황

  • 일시적인 오류나 지연에 대응하여 재시도를 통해 성공할 가능성을 높이고자하는 상황에서 적합하다.
  • 예를 들어, 네트워크 문제, DB 문제, 일시적인 서비스 다운 등의 장애에 대응하기 위해 사용된다.
  • 단, 일시적인 장애가 아닌 경우 잦은 재시도를 통해 오히려 반복적인 장애가 발생할 수 있다. (서비스의 신뢰도 하락)

장점

  • 일시적인 오류의 경우 재시도를 통해 성공할 가능성이 높아진다.
  • 어노테이션 하나만으로도 구현이 단순하며 재시도 횟수, 재시도 간격 등을 조절하여 세밀한 제어가 가능하다.

단점

  • 일시적인 오류가 계속 발생할 경우 재시도에 대한 의미가 무의미하고 해당 부분에 대한 본질적인 대응이 필요하다.
  • 서버에 다시 요청을 보내는 행위이기 때문에, 잦은 재시도 요청은 서버의 과부하를 고려해봐야한다.





CircuitBreaker

" 요청들의 추이를 지켜보다가 지속적으로 실패하는 경우, 잠시 요청을 차단해서 장애를 전파하지 않도록 한다. "

retry 와 다르게 오히려 요청을 다시 요청하는 것이 아니라, 서비스의 장애가 지속되면 해당 서비스에 대한 요청을 차단함으로써 전체 시스템의 과부하를 방지하는 것이다.

때문에, 서비스의 상태를 모니터링하여 상태가 회복될 때까지 차단 상태로 전환시키다가 이후 일정 시간 동안 서비스 호출을 차단한 후에 다시 시도를 할 수 있다.

circuit breaker 는 크게 3가지 상태를 기반으로 동작하는데 플로우는 다음과 같이
closeopenfallbackhalf openclose or open 와 같은 순서로 진행된다.

  • CLOSED : 서킷 브레이커로 감싼 내부 프로세스가 요청과 응답을 정상적으로 주고 받을 수 있는 상태 = 정상 상태
  • OPEN : 서킷 브레이커로 감싼 내부 프로세스가 요청과 응답을 정상적으로 주고 받을 수 없는 상태 = 장애 발생 가능
  • HALF_OPEN : fall back 응답을 수행하고 있지만 실패율을 측정해서 CLOSE 또는 OPEN 으로 변경될 수 있는 상태

적합한 상황

  • 일시적인 장애가 아닌, 지속적으로 실패하는 서비스에 대한 호출을 방지하여 시스템 전체의 안정성을 유지할 수 있다.
  • 데이터의 정확도보다 서비스의 안정성이나 응답속도가 더 중요한 경우에 사용된다.
  • 때문에, 의존 서비스의 장애가 현재 서비스에 영향을 주는 경우 적합하다.

장점

  • 빠르게 실패를 감지하고 차단함으로써 전체 시스템이 안정성을 유지할 수 있다.

단점

  • retry 보다 보다 복잡한 구현이 필요할 수 있다.





간단한 예제

아래와 같은 프로젝트의 요구 사항의 경우, retry, 서킷 브레이커를 동시에 적용할 수 있다.
이를 통해 Retry를 통해 일시적인 문제에 빠르게 대응하고, Circuit Breaker를 통해 지속적인 문제에 대응하여 서비스의 안정성을 유지할 수 있다.

프로젝트 요구 사항
☑︎ 해당 프로젝트는 모두 외부 api를 호출한 것들을 통해서만 구성되는 서비스이다.
☑︎ 데이터는 오로지 외부 API를 통해 가져오며, 이 API는 일시적인 네트워크 문제로 가끔 실패할 수 있습니다.
☑︎ 데이터 요청은 상대적으로 자주 발생하며, 실패 시 빠르게 대응하여 서비스를 유지해야 합니다.
☑︎ 그러나 만약 외부 서비스가 지속적으로 응답하지 않는 경우, 이로 인한 서비스 전체의 성능 저하를 방지하기 위한 대책이 필요합니다.

전체 코드

Retry 는 spring-retry 라이브러리를 사용하였고

implementation 'org.springframework.retry:spring-retry'
implementation 'org.springframework:spring-aspects'

CircuitBreaker 는 spring-cloud-starter-circuitbreaker-resilience4j 라이브러리를 사용한 예제이다.

implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
implementation 'org.springframework.boot:spring-boot-starter-aop'
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class DataService {

    // 재시도를 시도한다. 이때 최대 3번까지 재시도하고, 각 재시도 간에는 2초의 딜레이가 있다.
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000))
    // 지정한 실패율이 달성되면 요청이 중단되고 fallbackMethod 로 응답을 처리한다.
    @CircuitBreaker(name = "externalService", fallbackMethod = "fallback")
    public String fetchData(String url) {
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate.getForObject(url, String.class);
    }

    // Circuit Breaker가 열렸을 때 (OPEN 상태일 때) 호출되는 fallback 메서드
    public String fallback(String url, Exception e) {
        return "Fallback data";
    }

    // Retry가 모든 재시도에 실패한 경우 호출되는 메서드 (최종 대체 응답 반환)
    // 파라미터 첫 번째 인자는 @Retryable 에서 발생한 Exception 이 전달
    // 이때, @Retryable 어노테이션의 메서드와 동일한 파라미터, 반환타입을 가져야 한다.
    @Recover
    public String recover(Exception e) {
        return "Recovery data";
    }
}



정리

  • Retry 는 일시적인 오류에 대응하여 요청 재시도를 통해 성공할 가능성을 높이기 위해 사용될 수 있고, (ex. 재시도를 하면 성공할 가능성이 높을 경우)
  • Circuit Breaker 는 지속적인 서비스 장애에 대응하여 요청 방지를 통해 과부하를 방지하기 위해 사용될 수 있다.

때문에, 두 패턴 중 무엇 하나만 가장 적합하다기보다는,
서로 보완적으로 사용하면 전체 시스템의 안정성과 견고성을 향상시킬 수 있으므로 두 패턴을 조합하여 사용하는 것이 서비스의 안정성에 효과적일 수 있다.








[참고]
https://hyeon9mak.github.io/spring-retry/
https://hyeon9mak.github.io/spring-circuit-breaker/
https://yangbongsoo.tistory.com/99
https://findmypiece.tistory.com/328
https://velog.io/@mu1616/Webclient에-적용된-CircuitBreaker와-Retry-테스트하기 // 전체

https://velog.io/@hgs-study/CircuitBreaker // 서킷

profile
- 👩🏻‍💻

0개의 댓글