실전 프로젝트 23 - Resilence4j
장애 격리
연결되어 있는 서비스들에서 특정 서비스가 장애를 일으키게 되면 장애 전파를 통해 전체 서비스 마비가 발생할 수 있다는 생각을 하게 되었다. 이는 곧 서비스의 장애내성과 회복탄력성이 필요하다는 결론을 내릴 수 있었다.
분리되어 있는 서비스 간의 장애 전파를 막기 위해서는 어떤 방식이 있는지 찾아보는 과정에서 기존 Hystrix라는 기술은 더 이상 update 되지 않아 사실상 deprecated가 되었고 보다 경량화된 Resilience4j를 이용하게 되었다.
Resilience4j
서비스가 실패하거나 느려질 때 시스템의 안정성을 유지하고, 복구하는 기능을 제공하는 라이브러리.
Resilience4j는 주로 마이크로서비스 아키텍처나 클라우드 네이티브 환경에서 사용되며, 주요한 네트워크 및 외부 서비스와 상호작용할 때 발생할 수 있는 문제들에 대한 대비책을 제공한다.
1. Circuit Breaker (서킷 브레이커)
서킷 브레이커 패턴을 구현하여, 외부 서비스가 일시적으로 사용 불가능할 때 요청을 빠르게 실패하도록 만든다. 이렇게 함으로써 해당 서비스에 대한 과도한 부하를 방지하고 시스템의 안정성을 유지한다.
2.Retry (재시도): 임의의 오류나 장애가 발생할 때 요청을 자동으로 다시 시도한다. 이를 통해 일시적인 문제에 대응하고 외부 서비스의 안정성을 높일 수 있다.
3.Bulkhead (벌크헤드): 다양한 유형의 작업을 격리하여 서로 영향을 미치지 않도록 한다. 예를 들어, 네트워크 호출과 데이터베이스 쿼리는 별도의 자원풀에서 관리된다.
4.Rate Limiter : 특정 서비스에 대한 요청 속도를 제한하여, 과도한 요청으로 인한 서비스 과부하를 방지한다.
5.TimeLimiter : 특정 작업의 실행 시간을 제한하여 무한루프나 긴 대기 시간으로 인한 시스템 불안정을 방지한다.
CircuitBreaker
원래 용어는 전기 회로, 외부 서비스와의 통신에서 장애가 발생했을 때 이를 감지하고 서비스 간의 연결을 끊어 해당 서비스에 대한 요청을 즉시 실패로 처리. 이렇게 함으로써 장애가 전파되는 것을 방지하고 시스템의 안정성을 유지.
Closed 상태 (Closed State):
초기 상태로, 외부 서비스 호출이 수행. 만약 외부 서비스에서 오류가 발생하면 오류 횟수가 증가하고, 일정 수준 이상이 되면 서킷 브레이커가 작동.
Open 상태 (Open State):
서킷 브레이커가 작동되면 일정 시간 동안 외부 서비스 호출을 통제. 이는 외부 서비스가 복구될 때까지 대기하는 시간으로 사용.
Half-Open 상태 (Half-Open State):
일정 시간이 경과한 후, 서킷 브레이커는 자동으로 Half-Open 상태로 전환. 이 상태에서는 한 번의 요청만을 허용하여 외부 서비스가 여전히 정상 작동하는지 확인. 이러한 동작은 외부 서비스와의 통신에서 장애가 발생할 경우, 해당 서비스의 과도한 호출을 막고 시스템의 안정성을 유지. 이는 전체 시스템이 외부 서비스의 장애로 인해 마비되는 것을 방지.
설정
failure-rate-threshold
실패율 임계값. 이 값은 호출의 실패율이 이 임계값을 초과하면 Circuit Breaker가 열리도록 설정.
slow-call-rate-threshold:
느린 호출 비율 임계값. 이 값은 느린 호출의 비율이 이 임계값을 초과하면 Circuit Breaker가 열리도록 설정.
slow-call-duration-threshold:
느린 호출의 지속 시간 임계값. 이 값은 호출이 이 임계값을 초과하는 경우 해당 호출이 느린 호출로 간주.
permitted-number-of-calls-in-half-open-state:
half-open 상태에서 허용되는 호출의 최대 수. Circuit Breaker가 half-open로 전환된 후 동시에 허용되는 호출의 최대 수
max-wait-duration-in-half-open-state:
half-open 상태에서의 최대 대기 시간. half-open 상태로 전환된 후에 허용된 호출 수를 초과하는 호출이 있을 경우, 이 값은 이후 호출을 기다리는 최대 시간.
sliding-window-type:
슬라이딩 윈도우의 타입. 이 설정은 호출 횟수를 기반으로 슬라이딩 윈도우를 구성.
sliding-window-size:
슬라이딩 윈도우의 크기.
minimum-number-of-calls:
최소 호출 횟수. Circuit Breaker가 작동하기 전에 고려해야 하는 최소 호출 횟수
wait-duration-in-open-state
열린 상태에서의 대기 시간. Circuit Breaker가 열린 상태로 전환된 후 다음 시도를 기다리는 시간.
register-health-indicator:
actuator health 지표를 등록 여부.
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
ext {
set('springCloudVersion', "2022.0.3")
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
기본 의존성 설정
-다른 블로그나 구글에서 의존성을 찾기 위해서 노력했지만 과거 버전을 사용하거나 Hystrix를 이용하는 경우가 많아 적절한 의존성 버전을 찾는 작업이 꽤 힘들었다. 특히 스프링 부트 3,0으로 넘어오면서부터 전체적인 버전들이 많이 상향되었기 때문에 좀 더 힘든 점이 있었다.
@Configuration
public class Resilience4jConfig {
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
return CircuitBreakerRegistry.ofDefaults();
}
}
yml파일에 실제 circuitBreaker 설정들을 해주고 레지스트리에서 해당 구현체를 꺼내올 수 있는 방식이라 레지스트리를 빈으로 등록하였다. 처음에 이 부분에 대해서 생각을 안 하고 바로 CircuitBreakerRegistry가 구현체로 등록이 되는 줄 알고 꽤 헤멨었다. 이후 빈으로 등록하고 잘 설정한 줄 알았는데 이게 나중에 골머리를 썩히게 되는 것이 될 줄 몰랐다...
cloud:
openfeign:
circuitbreaker:
enabled: true
alphanumeric-ids:
enabled: true
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000
@FeignClient(name = "member-server", fallback = MemberServerClientFallback.class)
public interface MemberServiceClient extends GetMemberPointPort {
@GetMapping("/members/point")
ResponseEntity<ResponsePointDto> getPoint(@RequestHeader("x-authorization-id") Long memberId);
@Override
default void validatePoint(Long bidPoint, Long memberId) {
this.getPoint(memberId).getBody().validatePoint(bidPoint);
}
}
OpenFeign은 좀 다르게 CircuitBreaker가 설정되어야 한다. feign값과 관련된 yml에 CircuitBreaker enabled를 해주면 전체 Feign Class에 Wrapping되어 적용이 된다.
@Component
@Slf4j
public class MemberServerClientFallback implements MemberServiceClient {
@Override
public ResponseEntity<ResponsePointDto> getPoint(Long memberId) {
log.info("포인트 조회 실패 -> Fallback");
throw new IllegalStateException("멤버 서버로부터 정보를 불러올 수 없습니다");
}
@Override
public void validatePoint(Long bidPoint, Long memberId) {
log.info("포인트 검증 실패 -> Fallback");
throw new IllegalArgumentException("유효하지 않은 멤버 포인트 정보입니다");
}
}
장애가 발생하여 정상적인 응답이 실패할 시에 Fallback 메서드를 통해 대안적인 서비스를 제시할 수 있는데 해당 fallback메서드의 구현도 공식 문서를 참고하여 적용하는 것이 제대로 작동했다. 회원 도메인에 장애가 발생하면 CircuitBreaker가 open상태로 변경되면 연쇄적으로 장애가 전파될 수 있는 경매와 주문 도메인에서 각각 희망 낙찰가를 보여주거나,
결제 상품과 연관되어 있는 다른 상품들을 보여주는 방식과 같은 대안적인 서비스를 추가적으로 구상했으나, 시간적인 문제로 현재는 예외를 던지는 방식을 채택하고 있다.
이후 해당 CircuitBreaker를 확인하기 위해 actuator의 health에 정보를 등록하고 조회하는데 yml에서 설정한 값들로 circuitBreaker 기준치들이 설정되지 않는 문제가 발생하였다. 분명 실패 요청 대비 비율과 실패 시간 대비 비율을 각각 40%로 설정하였는데 CircuitBreaekr는 50,100%로 설정된 채 기동되었다. 해당 부분을 해결하기 위해서 yml에 instances 지정도 해보고 Configuration에 커스텀한 CircuitBreaeker를 등록도 해봤으나 번번이 실패하였다.
구글링을 통해서 다른 레퍼런스를 찾아본 결과 모든 자료들이 50,100%로 설정되어 있길래 이러한 설정값들이 기본으로 적용이 되기 때문에 똑같이 나오나 싶다 했지만 아무래도 찜찜했다. 분명 내가 설정한 값들은 40인데 표시되는 수치가 다른 건 문제가 있는 게 아닐까라는 생각이 들었다. 단순히 원래 이런가보다 하고 넘어가기엔 개인적으로 너무 아쉬워서 다시 공식문서를 천천히 읽어봤다.
다른 사람들 모두가 해당 수치로 적용되어 블로그에 올렸다는 건 기본 써킷브레이크 설정값이 50,100이기에 가능한 일인 것 같다느 생각이 들어 Registry 부분을 다시 살펴본 결과 공식문서를 참고하여 수정할 수 있었다.
@Configuration
public class Resilience4jConfig {
@Bean
public CircuitBreakerConfig circuitBreakerConfig() {
return CircuitBreakerConfig.custom()
.failureRateThreshold(40)
.slowCallRateThreshold(40)
.slowCallDurationThreshold(Duration.ofSeconds(6))
.permittedNumberOfCallsInHalfOpenState(5)
.maxWaitDurationInHalfOpenState(Duration.ofSeconds(3))
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(10)
.minimumNumberOfCalls(20)
.waitDurationInOpenState(Duration.ofSeconds(1))
.build();
}
@Bean
public CircuitBreakerRegistry circuitBreakerRegistry() {
return CircuitBreakerRegistry.of(circuitBreakerConfig());
}
}
기존에는 Registry.ofDefaults를 사용했기 때문에 기본 설정된 CircuitBreaker가 적용되었던 것이고 해당 config와 이 config를 참고한 Registry를 등록하여 처음에 내가 원했던 값으로 CircuitBreaker를 설정하여 기동시킬 수 있게 되었다.
서비스와 관련해서 가장 중요한 게 장애를 어떻게 핸들링하는 것인가이다. 안정적인 서비스를 제공하기 위해서는 예기치 않은 상황에서 발생하는 다양한 장애상황들을 산정하고 이를 미리 차단하여 기존의 서비스 흐름을 유지시키는 게 중요하다. 이번 써킷 브레이크 구현 과정은 장애 핸들링에 대한 내 관심이 투영되어 프로젝트의 일부분을 중도에 추가되었지만 생각보다 꽤 길고 어려운 여정이었다. 다만 이를 통해 MSA에서 필요로 하는 여러 기능들을 만들어보고 나아가 어떻게 활용할 수 있을지 생각해볼 수 있는 시간이었다.