1. Circuit Breaker 란?
서킷 브레이커란 마이크로서비스 간에 호출 실패를 감지하고 시스템의 안정성을 관리하는 패턴을 말한다. 이 패턴은 전기 차단 회로기의 개념을 차용하여 서비스 호출 중 장애가 발생할 경우 더이상의 호출을 차단하여 시스템의 리소스를 보호하고 장애 확산을 방지한다. 외부 서비스 호출시 빠른 실패를 감지해서 장애를 격리하고, 시스템의 다른 부분에 영향이 가지 않도록 한다. 특히 Resilience4j 는 Java 애플리케이션에서 CircuitBreaker 패턴을 구현하는 경량화된 라이브러리이다.
https://resilience4j.readme.io/docs/getting-started
2. CircuitBreaker 의 세 가지 주요 상태
CLOSED
정상 상태로, 모든 요청이 통과 된다.
실패율이 설정된 임계값을 초과하면 OPEN 상태로 전환된다.
OPEN
장애가 발생한 상태로, 모든 요청이 차단 된다.
이 상태에서는 지정된 대체 메서드(fallback)를 호출할 수 있다.
HALF_OPEN
일정 시간이 지난 후 OPEN 상태에서 전환되는 상태이다.
일부 요청을 허용하여 시스템이 정상으로 돌아왔는지 체크한다.
성공하면 CLOSED 상태로, 실패하면 OPEN 상태로 다시 전환된다.
3. Sliding Window
Resilience4j의 Circuit Breaker는 서비스 호출의 실패율을 관리하기 위해 Sliding Window를 사용한다. 슬라이딩 윈도우는 서비스 호출의 결과를 일정한 시간 또는 호출 횟수 단위로 저장하고, 이를 기반으로 실패율을 계산하여 Circuit Breaker의 상태를 결정하게 된다. Resilience4j에서는 두 가지 유형의 슬라이딩 윈도우를 지원한다.
3 - 1. Count-based Sliding Window
이 방식은 최근 N개의 호출 결과를 저장하고, 그 중 실패한 호출의 비율을 계산한다.
예를 들어, 크기가 100이라면, 최근 100개의 호출 중 실패한 비율이 설정된 임계치를 초과하면 OPEN 상태로 전환된다.
3 - 2. Time-based Sliding Window
이 방식은 최근 N초 동안의 호출 결과를 저장하여 실패율을 계산한다.
예를 들어, 크기가 60초라면, 최근 60초 동안의 호출 중 실패한 비율이 임계치를 초과하면 OPEN 상태로 전환된다.
4. Fallback 메커니즘
fallback 메커니즘은 외부 서비스 호출이 실패했을 때 대체 로직을 제공하는 것을 말한다.
fallback 메커니즘을 통해 시스템의 안정성을 높이고, 장애가 발생해도 사용자에게 일정한 응답을 제공할 수 있다.
그리고, 장애가 다른 서비스에 전파되는 것을 방지한다.
5. 실습
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.5'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.spring-cloud.resilience4j'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test'){
useJUnitPlatform()
}
aop, actuator, web, prometheus를 의존하고 있는 상태다.
특징적인 것은 resilience4j를 spring starter io 에서 가져오지 않고, io.github에서 가져온 것이다.
spring starter 에서는 인터페이스를 제공하기 때문에 따로 구현체를 만들어줘야 한다.
따라서, io.github에서 가져온다.
application.yml
spring:
application:
name: sample
server:
port: 19090
resilience4j:
circuitbreaker:
configs:
default: # 기본 구성 이름
registerHealthIndicator: true # 애플리케이션의 헬스 체크에 서킷 브레이커 상태를 추가하여 모니터링 가능
# 서킷 브레이커가 동작할 때 사용할 슬라이딩 윈도우의 타입을 설정
# COUNT_BASED: 마지막 N번의 호출 결과를 기반으로 상태를 결정
# TIME_BASED: 마지막 N초 동안의 호출 결과를 기반으로 상태를 결정
slidingWindowType: COUNT_BASED # 슬라이딩 윈도우의 타입을 호출 수 기반(COUNT_BASED)으로 설정
# 슬라이딩 윈도우의 크기를 설정
# COUNT_BASED일 경우: 최근 N번의 호출을 저장
# TIME_BASED일 경우: 최근 N초 동안의 호출을 저장
slidingWindowSize: 5 # 슬라이딩 윈도우의 크기를 5번의 호출로 설정
minimumNumberOfCalls: 5 # 서킷 브레이커가 동작하기 위해 필요한 최소한의 호출 수를 5로 설정
slowCallRateThreshold: 100 # 느린 호출의 비율이 이 임계값(100%)을 초과하면 서킷 브레이커가 동작
slowCallDurationThreshold: 60000 # 느린 호출의 기준 시간(밀리초)으로, 60초 이상 걸리면 느린 호출로 간주
failureRateThreshold: 50 # 실패율이 이 임계값(50%)을 초과하면 서킷 브레이커가 동작
permittedNumberOfCallsInHalfOpenState: 3 # 서킷 브레이커가 Half-open 상태에서 허용하는 최대 호출 수를 3으로 설정
# 서킷 브레이커가 Open 상태에서 Half-open 상태로 전환되기 전에 기다리는 시간
waitDurationInOpenState: 20s # Open 상태에서 Half-open 상태로 전환되기 전에 대기하는 시간을 20초로 설정
management:
endpoints:
web:
exposure:
include: prometheus
prometheus:
metrics:
export:
enabled: true
설정 항목들은 공식 문서를 통해 확인할 수 있다.
ProductService
package com.spring_cloud.resilience4j.sample;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ProductService {
private final Logger log = LoggerFactory.getLogger(getClass());
private final CircuitBreakerRegistry circuitBreakerRegistry;
@PostConstruct
public void registerEventListener() {
circuitBreakerRegistry.circuitBreaker("productService").getEventPublisher()
.onStateTransition(event -> log.info("#######CircuitBreaker State Transition: {}", event)) // 상태 전환 이벤트 리스너
.onFailureRateExceeded(event -> log.info("#######CircuitBreaker Failure Rate Exceeded: {}", event)) // 실패율 초과 이벤트 리스너
.onCallNotPermitted(event -> log.info("#######CircuitBreaker Call Not Permitted: {}", event)) // 호출 차단 이벤트 리스너
.onError(event -> log.info("#######CircuitBreaker Error: {}", event)); // 오류 발생 이벤트 리스너
}
@CircuitBreaker(name = "productService", fallbackMethod = "fallbackGetProductDetails")
public Product getProductDetails(String productId) {
log.info("###Fetching product details for productId: {}", productId);
if ("111".equals(productId)) {
log.warn("###Received empty body for productId: {}", productId);
throw new RuntimeException("Empty response body");
}
return new Product(
productId,
"Sample Product"
);
}
public Product fallbackGetProductDetails(String productId, Throwable t) {
log.error("####Fallback triggered for productId: {} due to: {}", productId, t.getMessage());
return new Product(
productId,
"Fallback Product"
);
}
// 이벤트 설명 표
// +---------------------------+-------------------------------------------------+--------------------------------------------+
// | 이벤트 | 설명 | 로그 출력 |
// +---------------------------+-------------------------------------------------+--------------------------------------------+
// | 상태 전환 (Closed -> Open) | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환되면 발생 | CircuitBreaker State Transition: ... |
// | 실패율 초과 | 설정된 실패율 임계치를 초과하면 발생 | CircuitBreaker Failure Rate Exceeded: ... |
// | 호출 차단 | 서킷 브레이커가 오픈 상태일 때 호출이 차단되면 발생 | CircuitBreaker Call Not Permitted: ... |
// | 오류 발생 | 서킷 브레이커 내부에서 호출이 실패하면 발생 | CircuitBreaker Error: ... |
// +---------------------------+-------------------------------------------------+--------------------------------------------+
// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
// | 이벤트 | 설명 | 로그 출력 |
// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
// | 메서드 호출 | 제품 정보를 얻기 위해 메서드를 호출 | ###Fetching product details for productId: ... |
// | (성공 시) 서킷 브레이커 내부에서 호출 성공 | 메서드 호출이 성공하여 정상적인 응답을 반환 | |
// | (실패 시) 서킷 브레이커 내부에서 호출 실패 | 메서드 호출이 실패하여 예외가 발생 | #######CircuitBreaker Error: ... |
// | (실패 시) 실패 횟수 증가 | 서킷 브레이커가 실패 횟수를 증가시킴 | |
// | (실패율 초과 시) 실패율 초과 | 설정된 실패율 임계치를 초과하면 발생 | #######CircuitBreaker Failure Rate Exceeded: ... |
// | (실패율 초과 시) 상태 전환 (Closed -> Open) | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환됨 | #######CircuitBreaker State Transition: Closed -> Open at ... |
// | (오픈 상태 시) 호출 차단 | 서킷 브레이커가 오픈 상태일 때 호출이 차단됨 | #######CircuitBreaker Call Not Permitted: ... |
// | (오픈 상태 시) 폴백 메서드 호출 | 메서드 호출이 차단될 경우 폴백 메서드 호출 | ####Fallback triggered for productId: ... due to: ... |
// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
}
`@CircuitBreaker` 를 사용하여 해당 메서드에 Circuit Breaker 패턴을 적용한다.
해당 메서드가 실패시 fallbackMethod 로 지정한 메서드가 실행된다.
동작 흐름
1. `getProductDetails` 메서드가 호출되면, 정상적인 경우 제품 정보를 반환한다.
2. 제품 ID가 "111"인 경우, 예외가 발생하여 서킷 브레이커의 실패 카운트가 증가한다.
3. 설정된 임계값을 초과하면 서킷 브레이커가 열리고(OPEN), 이후 호츨은 fallback 메서드로 전달된다.
4. 서킷 브레이커의 상태 변화와 각종 이벤트는 등록된 리스너를 통해 로깅된다.
6. 로그로 동작 확인
제품 ID에 "111"이 들어온 순간 예외가 발생하였다.
실패율이 60.0 에 도달하였다.
상태가 CLOSED -> OPEN
OPEN 상태에서는 무조건 fallback 메서드를 타게 된다.
OPEN 상태에서 시간이 좀 지나자 HALF_OPEN으로 변경되고
이 상태에서 괜찮은 상태인지 체크를 한 뒤, 문제가 없으므로
CLOSED 상태로 전환된다.
7. 상태 모니터링
/actuator/prometheus 를 호출하면
resilience4j의 항목들을 볼 수 있고,
이것으로 모니터링할 수 있다.
References
https://resilience4j.readme.io/docs/circuitbreaker
https://jaehoney.tistory.com/429
'MSA' 카테고리의 다른 글
2024 11 28 TIL - Spring Cloud Config (1) | 2024.11.28 |
---|---|
2024 11 27 TIL - API Gateway, Spring Cloud Gateway (0) | 2024.11.27 |
2024 11 25 TIL - 로드 밸런싱, Netflix Ribbon (0) | 2024.11.25 |
2024 11 22 TIL - Spring Cloud Netfilx Eureka로 알아보는 서비스 디스커버리 (3) | 2024.11.22 |
2024 11 21 TIL - MSA란? (1) | 2024.11.21 |