resilience4j
MSA 환경에서 서킷브레이커, 유량제어는 매우 중요한 개념으로 보다 더 안전한 서비스 운영을 위해 사용되는 개념이다.
서비스가 확장되면서 같은 팀 내에서 분리된 서비스가 한 쪽으로 의존(트래픽)을 하면서 A의 서버가 B의 API를 비동기로 호출하면서 데이터를 가져오는 상황이 발생한다.
A의 서버를 개발하는 나는 몇 가지 상황을 대비해야한다.
- B의 서버가 죽어있다면?
- B의 서버가 CPU 자원 부족으로 죽기 직전이라면?
- B의 서버가 순간적인 에러를 뱉는다면?
여기서 A서버는 B의 서버가 회복할 수 있도록 기다려줌과 동시에 장애가 전파되지 않도록 막아야한다. 여기서 내가 가져올 수 있는 것은 서킷브레이커이다.
서킷브레이커란?
집에서 사용하는 두꺼비집이라 불리는 누전차단기, 회로차단기가 서킷브레이커이다.
개발자들 세계에서 사용하는 서킷브레이커는 각각의 시스템간 장애가 전파되지 않도록 차단을 목적으로 두는 것을 말한다. 서킷브레이커는 상태에 따라서 서로 다른 동작을 한다.
서킷브레이커의 상태
서킷브레이커의 상태는 보통상태와 특별한 생태가 있다.
보통상태에서 3개로 분류가 된다.
- CLOSED: 정상적으로 호출되고 응답을 주는 상태로 실패율이나 느린 호출 비율이 임계값을 넘지 않은 경우이다.
- OPEN: 문제 발생이 감지된 상태, 일정 기간동안 너무 많은 요청이 실패했거나 너무 느리게 응답하는 경우 서킷 브레이커가 열려 더이상 요청을 막은 상태이다.
- HALF_OPEN: 문제 발생이 감지된 상태, 서킷 브레이커가 일정 시간이 지난 후 다시 닫힐지 열릴지 결정하기 위해 일부 요청을 허용 하는 상태
특별한 상태는 2개로 분류된다.
- DISABLED: 항상 호출을 허용하는 상태
- FORCE_OPEN: 항상 호출을 거부하는 상태
서킷 브레이커는 슬라이딩 윈도우(Sliding Window)를 사용하여 상태의 변화여부를 결정한다. 슬라이딩 윈도우는 횟수 방식(COUNT-BASED)과 시간 방식(TIME_BASED)가 있는데 방식에 따라 슬라이딩 윈도우 안에서 정해진 확률보다 높은 확률로 호출에 실패하게 되면 상태를 OPEN으로 변경한다.
OPEN 상태에서는 연동된 시스템 호출을 시도하지 않으며, 바로 호출 실패 Exception이 발생하여 정해진 Fallback동작을 수행한다.
OPEN 이후 설정한 시간이 지나면 HALF_OPEN 상태로 변경되며 호출이 정상화되었는지 다시한번 실패 확률로 확인한다. 정상화되었다고 판되면 CLOSED 상태로 변경되지만 아직 정상화되지 못했다고 판단되면 다시 OPEN 상태로 되돌아간다.
서킷브레이커 라이브러리
서킷 브레이커를 처리할 수 있게 도와주는 라이브러리는 대표적으로 4개가 있다.
- Resilience4J
- Spring Cloud Circuit Breaker
- Hystrix
- Sentinel
이 중 최근에도 커밋이 찍혀있으며 활발하게 오픈소스로 개발이 이루어지는 것은 Resilience4J이다. 따라서, Resilience4J를 선택하였다.
https://github.com/resilience4j/resilience4j
Resilience4J란?
Resilience는 회복 탄력성이다. 4는 For J는 Java
Resilience For Java 즉, 회복 탄력성을 자바를 위해 만들어진 라이브러리이다. 우리는 여기서 회복 탄력성에 집중해야한다. 시스템은 나날이 복잡해져 가면서 고객들에게 고가용성을 유지해야하고 장애는 전파가 되지 않도록 해야한다. 그렇다면 의존성이 있는 시스템끼리는 서로 장애가 일어나지 않도록 연산작업(트래픽)에 대해서 보낼지 말지를 결정하여 트래픽을 받는 시스템에서 자원이 부족한 상황에서 다운이 되는 상황이 발생하지 않도록 막아야한다. 여기서 다시 회복을 할 수 있도록 하는 것이 회복탄력성이다.
Resilience4J 적용하기
Getting Started를 확인하여 적용하기 (Java 17 이상을 요구한다.)
https://resilience4j.readme.io/docs/getting-started
//Resilience4J
implementation("io.github.resilience4j:resilience4j-spring-boot3:2.2.0")
config는 boot의 application.yml 혹은 application.properties를 통해 스프링 빈에 설정 값을 주입할 수 있다.
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true
slidingWindowType: COUNT_BASED
slidingWindowSize: 10
minimumNumberOfCalls: 5
slowCallRateThreshold: 100
slowCallDurationThreshold: 60000
failureRateThreshold: 60
permittedNumberOfCallsInHalfOpenState: 3
waitDurationInOpenState: 60s
automatic-transition-from-open-to-half-open-enabled: true
instances:
circuit-sample-common: # circuitbreaker name
baseConfig: default # 기본 config 지정
circuit-sample-3000:
baseConfig: default
slowCallDurationThreshold: 3000 # 응답시간이 느린것으로 판단할 기준 시간 [ms]
SlidingWindowType
SlidingWindowType에 COUNT_BASED, TIME_BASED가 있다. 공식문서에 의하면 둘의 시간 복잡도는 O(1)이고 공간 복잡도는 O(WindowSize)이다.
COUNT_BASED: 마지막 슬라이딩 WindowSize 호출이 기록되고 집계된다.
TIME_BASED: 마지막 슬라이딩 WindowSize 초의 호출이 기록되고 집계된다.
slidingWindowSize
서킷의 상태가 CLOSED일 때 요청의 결과를 기록하기 위한 슬라이딩 윈도우의 크기
minimumNumberOfCalls
서킷이 실패율 또는 지연된 응답을 계산하기 전 요구되는 최소 요청의 수, 예시에서는 최소 5개의 요청이 있어야 서킷이 계산을 한다.
slowCallRateThreshold
지연된 응답 Threadhold 퍼센트 값으로 느린 호출의 백분율이 임계값과 같거나 크면 CircuitBreaker가 개방으로 전환되고 단락 호출이 시작된다.
slowCallDurationThreshold
요청이 느린 것으로 간주되는 기간
failureRateThreshold
실패율 Threadhold 퍼센트 값으로 실패율 임계값보다 크거나 같으면 CircuitReaker가 개방으로 전환하고 단락 호출을 시작한다.
permittedNumberOfCallsInHalfOpenState
서킷이 HALF_OPEN상태일 때 허용되는 호출수를 의미한다.
waitDurationInOpenState
서킷의 상태가 OPEN에서 HALF_OPEN으로 변경되기 전에 Circuit Break가 기다리는 시간이다. 예시에서는 20초 이후에 변경된다.
automatic-transition-from-open-to-half-open-enabled
true/false로 Wait Duration In Open State기간이 지난 이후에 Open에서 Half-Open으로 자동으로 상태가 변경된다. 하나의 쓰레드가 CircuitBreaker의 모든 인스턴스들을 모니터링하며 상태를 확인한다.
Resilience4J 상황 시연
@Slf4j
@Service
public class AService {
@CircuitBreaker(name = "circuit-sample-common", fallbackMethod = "fallback2")
public String callOutService() {
throw new RuntimeException("에러발생!");
}
private String fallback2(CallNotPermittedException e) {
log.error("Circuit breaker is open {}", e.getMessage());
return "fallback";
}
}
OUTPUT
2024-08-02T13:56:42.277+09:00 ERROR 84435 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: 에러발생!] with root cause
java.lang.RuntimeException: 에러발생!
2024-08-02T13:56:43.324+09:00 ERROR 84435 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: 에러발생!] with root cause
java.lang.RuntimeException: 에러발생!
2024-08-02T13:56:44.361+09:00 ERROR 84435 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: 에러발생!] with root cause
java.lang.RuntimeException: 에러발생!
2024-08-02T13:56:45.406+09:00 ERROR 84435 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: 에러발생!] with root cause
java.lang.RuntimeException: 에러발생!
2024-08-02T13:56:46.455+09:00 ERROR 84435 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: 에러발생!] with root cause
java.lang.RuntimeException: 에러발생!
2024-08-02T13:56:47.493+09:00 ERROR 84435 --- [nio-8080-exec-6] com.example.resilience.a.AService : Circuit breaker is open CircuitBreaker 'circuit-sample-common' is OPEN and does not permit further calls
설정해둔 임계치인 5회를 넘기자 그대로 fallbackMethod를 실행하고 있다.
현재 설정에서는 최소 5개의 호출이 발생하고 그 중 3개(60%)가 실패할 경우 서킷이 OPEN 상태로 변경된다. 서킷이 열리면 설정된 시간(60초) 동안 새로운 호출을 차단한다. 이후 서킷은 Half-Open 상태로 전환되고 일부 호출을 허용하여 시스템의 상태를 확인한다.
주의 Catch한 에러는 CircuitBreaker에 포함되지 않는다.
CircuitBreaker를 적용한 분들은 짐작을 하였겠지만, CircuitBreaker의 내부는 AOP를 통해 구현되어 있습니다. 따라서, 해당 메서드에서 발생하는 catch가 이루어질 경우, CircuitBreaker는 포함되지 않습니다.
@CircuitBreaker(name = "circuit-sample-common", fallbackMethod = "fallback2")
public String callOutService() {
try {
throw new RuntimeException("에러발생!");
}catch (Exception e) {
log.error("에러발생: {}", e.getMessage());
return "catch error";
}
}
모니터링
https://resilience4j.readme.io/docs/micrometer 매트릭 정보는 해당 사이트에서 확인 가능하다. (본 글은 Spring Actuator를 통해 확인)
https://resilience4j.readme.io/docs/grafana-1 해당 사이트가서 확인을 한다.
grafana JSON 파일 링크 https://resilience4j.readme.io/docs/grafana-1
서킷브레이커가 작동하는 일이 없도록....
Reference.