1. 서비스 장애 대응(Circuit Breaker)의 필요성
현재 구성한 MSA의 구조는 다음과 같습니다.
Member-Service, Board-Service가 존재하고 Board 로직에서 Member-Service를 호출하는 구조입니다.
게시글 작성 요청 예시
이러한 상황에서, '게시글 작성' 요청 Flow를 살펴봅시다.
- 외부에서 게시글 작성 API 요청(Request#1)
- API Gateway에서 Board-Service로 요청 라우팅
- Board-Service에서 멤버 정보를 가져오기 위해 Member-Service로 OpenFeign 요청
- Member-Service의 Member DB에서 멤버 정보를 Read해서 Board-Service에게 응답
- OpenFeign 결과로 응답받은 멤버 정보를 통해 Board DB에 Board save
- 다음 게시글 요청(Request#2) 수행
이러한 정상적인 요청에서, 만약 Member Application에 장애가 발생하면 어떻게 될까요?
'장애 상황'을 2가지 상황으로 나눠봅시다.
- Member Application으로의 요청이 아예 실패하는 경우
- Member Application으로의 요청은 전달되지만, 응답이 지연되는 경우
이 두 가지 경우의 상황은 둘 다 다른 상황이지만, 서비스에 미치는 악영향은 다음과 같이 비슷합니다.
일정 시간 동안 쓰레드, 메모리 등 서버의 자원을 점유
요청이 아예 실패하는 경우는 요청의 Timeout 시간만큼 요청 스레드를 점유할 것이고,
응답이 지연되는 경우는 응답이 지연되는 Latency 시간만큼 요청 스레드를 점유하게 될 것입니다.
이렇게 장애가 지속된다면, Borad-Service 서버의 스레드풀이 고갈되어 전체 서비스의 장애로 이어질 것입니다.
사용자 입장에서는 게시글 작성이 엄청 지연된 후에 결과적으로 에러 페이지에 도달하게 될 것입니다.
또한, HTTP 요청이나 OpenFeign 요청은 일반적으로 동기 방식으로 동작하기 때문에
다수의 사용자가 요청을 보냈다면 총 지연 시간은 몇 배나 더 증가할 것입니다.
이러한 상황에서 우리는 다음과 같은 Needs를 느끼게 됩니다.
'호출할 서비스가 이미 장애가 발생한 서비스라면 요청을 보내지 않고 싶다.'
'또한, 서비스 호출에서 장애가 발생했을 때는 사용자 입장에서 에러가 아닌 정상적인 응답을 보게하고 싶다.'
'호출할 서비스가 장애가 발생하진 않았지만 지연이 길어져서 일정 자원을 점유한다면,
일정량의 자원은 격리해서 점유하지 못하게 해서 다른 요청은 성공하게 하고 싶다.'
이러한 Needs를 충족시켜주는 것이 바로 Circuit Breaker 패턴입니다.
Circuit Breaker 패턴을 구현하면 장애를 빠르게 탐지하고 요청을 차단할 수 있습니다.
2. Circuit Breaker 구현 라이브러리
이러한 Circuit Breaker를 구현한 라이브러리에는 어떤 것들이 있을까요?
크게 다음과 같은 2가지 라이브러리가 존재합니다.
- Netflix Hystrix
- Resilience4J
이전에는 Netflix의 Hystrix를 사용하여 서비스 장애 대응을 진행하는 경우가 많았습니다.
그러나, Hystrix 공식 Github Readme를 보면 더 이상 개발이 진행되지 않음을 알 수 있습니다.
- 더 이상 개발을 진행하지 않고, maintenance mode(유지보수 모드)이다.
- 기존 Hystrix을 사용하는 Application은 사용해도 좋지만, 새로운 프로젝트에는 Resilience4j를 권장한다.
이렇듯 Hystrix 공식문서에서도 Resilience4j를 권장하고 있기 때문에, Resilience4j를 선택하여 구현하도록 하겠습니다.
3. Resilience4j 원리
Resilience4j는 어떠한 원리로 Circuit Breaker 패턴을 구현할까요?
이를 알기 위해서는 다음과 같은 용어들을 알아야 합니다.
- Circuit Breaker Pattern
- Fallback
- Bulkhead
이러한 3가지 용어들을 알아보도록 하겠습니다.
3-1. Circuit Breaker 패턴 (원리)
Circuit Breaker 패턴은 앞서 간략하게 말했듯이,
장애를 방지하기 위한 패턴으로 장애 발생 지점을 감지하고 실패하는 요청을 계속적으로 보내지 않도록 방지하는 패턴입니다.
앞서 설명한 Needs 중에서 다음과 같은 Needs를 충족시켜 주는 패턴입니다.
'호출할 서비스가 이미 장애가 발생한 서비스라면 요청을 보내지 않고 싶다.'
Circuit Breaker 패턴의 원리는 전기/전자 분야에서 사용하는 회로 차단기를 떠올리면 이해하기 쉽습니다.
애초에 Circuit Breaker라는 용어도 '회로 차단기'라는 용어입니다.
회로를 생각해보면, 위의 스위치가 Closed되면 전류가 흐르고 스위치가 Open되면 전류가 흐르지 못합니다.
이러한 회로 차단기의 원리를 기반으로 Circuit Breaker가 동작하게 됩니다.
이를 기존 상황인 MSA 구조에 대입해보면,
각 서비스들의 상황을 모니터링을 통해 감지하고 하나의 서비스에 장애가 발생하면
요청을 차단(Switch Open)하여 해당 서비스로의 요청(전류)이 빠르게 실패하도록 하는 것입니다.
Circuit Breaker의 3가지 상태
Circuit Breaker는 Closed, Open 외에 Half_Open 상태가 존재하여 총 3가지 상태가 존재합니다.
Circuit Breaker에서는 Circuit Breaker의 상태가 결정되는 기준인 '실패 임계치'가 존재합니다.
실패 임계치를 기준으로 Open, Close 상태가 결정되고, 이후에 Half_Open 상태로 진입하게 됩니다.
- Closed : 요청의 실패율이 설정한 실패 임계치보다 낮은 상태
- Circuit Breaker가 Closed 상태면, 해당 서비스에 정상적으로 모든 요청이 처리된다.
- Open : 요청의 실패율이 설정한 실패 임계치보다 높은 상태
- Circuit Breaker가 Open 상태면, 해당 서비스로 요청을 보내지 않고 즉시 실패처리를 한다.
- Half_Open : Open 상태 이후에 일정 시간이 지난 상태
- Circuit Break가 Half_Open 상태라면 이후 요청의 성공/실패 상태에 따라 Closed/Open 상태로 변경된다.
- 이후 요청이 성공한다면 Closed 상태가 되어 모든 요청이 정상 처리 상태로 돌아간다.
- 이후 요청이 실패한다면 다시 Open 상태가 되어 모든 요청을 즉시 차단한다.
- Circuit Break가 Half_Open 상태라면 이후 요청의 성공/실패 상태에 따라 Closed/Open 상태로 변경된다.
위의 '실패 임계치'값을 정할 때 2가지 기준으로 요청이 실패했다는 것을 정의할 수 있습니다.
- slow call : 설정한 시간보다 오래 걸린 요청(지연된 요청)
- failure call : 말그대로 요청이 실패하거나 에러 응답을 받은 요청(실패한 요청)
Circuit Breaker 시나리오 예시
이러한 slow call, failure call 상황을 기준으로 임계치 시나리오를 다음과 같이 설정할 수 있습니다.
- 요청 시간이 5번 연속 10초를 넘길 경우 요청을 차단한다.
- slow call 기준으로 Closed -> Open 상태로 전환
- 요청이 3번 연속 실패한 경우 요청을 차단한다.
- failure call 기준으로 Closed -> Open 상태로 전환
- 요청이 차단된 상태에서 10초 이후의 3번 연속의 요청이 성공하면 정상적으로 요청을 처리한다.
- Open -> Half_Open 상태로 전환 시간 : 10초로 설정
- Half_Open -> Closed/Open 판단 요청 개수 : 3개
- Open 상태 10초 이후 요청이 3번 연속 성공하면 Closed 상태로 전환
- Open 상태 10초 이후 요청이 3번 연속 성공 전에 실패하면 Open 상태로 전환
※ Resilience4j의 Circuit Breaker 구현 원리
그렇다면, Resilience4j에서는 어떻게 Circuit Breaker Pattern을 구현할까요?
우선 각 호출 결과를 저장하고 집계하는 과정에서는 '슬라이딩 윈도우'를 사용합니다.
Resilience4j의 슬라이딩 윈도우는 시간/개수 기준으로 다음과 같이 나뉩니다.
- Count-based sliding window : 요청 개수 단위로 요청을 저장 및 집계하는 슬라이딩 윈도우
- Time-based sliding window : 요청 시간 단위로 요청을 저장 및 집계하는 슬라이딩 윈도우
이러한 슬라이딩 윈도우를 기반으로 요청을 집계 및 저장하고, 이를 기반으로 Circuit Breaker의 상태를 업데이트합니다.
더 자세한 구현을 알고싶다면 아래 공식문서를 참고하면 될 것 같습니다.
Resilience4j 공식 문서 Circuit Breaker Part
3-2. Fallback
Fallback은 서비스를 차단한 경우 예외를 발생시키는 대신 대비책을 제공하거나 미리 준비된 동작을 실행하는 것을 말합니다.
Circuit Breaker가 Open 상태인 상황에서 사용자 요청을 에러로 응답하지 않고 성공으로 응답하는 것을 의미합니다.
이러한 Fallback을 통해 다음과 같은 Needs를 충족할 수 있습니다.
'또한, 서비스 호출에서 장애가 발생했을 때는 사용자 입장에서 에러가 아닌 정상적인 응답을 보게하고 싶다.'
이러한 Fallback을 적용한 상황 예시를 들어보겠습니다.
예를 들면, 장소와 관련한 서비스일 때 사용자 맞춤 알고리즘을 통해 장소를 추천해주는 기능이 존재한다고 해봅시다.
이때, 추천 서비스에 장애가 발생하여 Circuit Breaker가 Open 상태로 전환되었다고 해봅시다.
일반적으로는 Open 상태이기 때문에 사용자 입장에서 추천 화면이 아닌 에러 화면을 보게 될 것입니다.
사용자 입장에서 이러한 에러 화면은 부정적인 경험을 초래하고 서비스를 사용하지 않을 것입니다.
이런 상황을 막기 위해 Fallback을 사용하여 추천 시스템에 장애가 발생했지만,
사용자에게는 미리 설정한 인기 장소 리스트를 응답하여 장애가 발생하지 않은 것처럼 보이게 할 수 있습니다.
3-3. Bulkhead
Bulkhead는 응답시 지연되는 서비스에 자원을 모두 소진하지 않도록 스레드 풀을 격리하는 것을 의미합니다.
Bulkhead를 통해 다음과 같은 Needs를 충족할 수 있습니다.
'호출할 서비스가 장애가 발생하진 않았지만 지연이 길어져서 일정 자원을 점유한다면,
일정량의 자원은 격리해서 점유하지 못하게 해서 다른 요청은 성공하게 하고 싶다.'
만약 제 개인프로젝트에서 Board, Member 외에 다른 도메인이 생겨서 똑같이 Member-Service를 호출해야 하는 상황이라고 해봅시다.
이때 Board-Service의 트래픽이 급증하여 Member-Service 호출이 잦아져 지연이 발생했을 때
다른 도메인에서 Member-Service를 호출하는 상황을 생각해봅시다.
Member-Service에서 지연이 발생했기 때문에 자연스럽게 해당 도메인에서 Member-Service의 호출도 지연이 발생할 것입니다.
이렇듯 장애가 전파되는 결과를 초래할 수 있습니다.
이러한 장애 전파를 막는 것이 Bulkhead입니다.
각 서비스에서 Membe-Service를 호출 할 때의 스레드 격리 환경을 설정하여 장애 전파를 막을 수 있습니다.
4. Resilience4j 적용하기
이제 원리를 살펴봤으니, 본격적으로 개인 프로젝트에 Resilience4j를 적용해보겠습니다.
4-1. bulid.gradle 의존성 추가
다른 서비스를 호출하는 서비스에 Resilience4j 의존성을 추가해줍시다.
저는 Member-Service를 호출하는 Board-Service 모듈에 추가해줬습니다.
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
4-2. application.yml 설정
설정한 application.yml은 다음과 같습니다.
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
health:
circuitbreakers:
enabled: true
resilience4j:
circuitbreaker:
configs:
default:
failure-rate-threshold: 50
slow-call-rate-threshold: 80
slow-call-duration-threshold: 10s
permitted-number-of-calls-in-half-open-state: 3
max-wait-duration-in-half-open-state: 0
sliding-window-type: COUNT_BASED
sliding-window-size: 10
minimum-number-of-calls: 10
wait-duration-in-open-state: 10s
instances:
shboard-circuit-breaker:
base-config: default
- management.endpoint.health.show-detail & management.health.circuitbreakers.enabled : 서킷 브레이커 Status를 Spring Actuator에서 보기위해서 위와 같이 설정했습니다.
- resilience4j.circuitbreaker.configs : circuitbreaker 설정을 관리합니다. 저는 'default'라는 이름을 지정해서 설정을 생성했습니다.
- failure-rate-threshold : 실패 요청(failure call)의 임계치를 설정합니다. 요청 실패가 실패 임계치 이상이 되면 OPEN 상태로 전환됩니다.
- 저는 50%로 설정해서 요청 실패가 절반이 넘으면 OPEN 상태로 요청이 즉시 차단되게 설정했습니다.
- slow-call-rate-threshold : 지연 요청(slow call)의 임계치를 설정합니다. 지연된 요청이 해당 임계치 이상이 되면 OPEN 상태로 전환됩니다.
- 기본값은 100%인데, 지연 요청이 100%가 되면 서버 스레드가 모두 점유되어 장애가 생길 수 있으므로 100%보다 좀 작은 80으로 설정했습니다.
- slow-call-duration-threshold : 지연 요청(slow call)이라고 판단할 시간을 설정합니다. (ms 단위)
- 저는 요청이 10초가 넘으면 slow call이라고 판단하기로 했습니다.
- permitted-number-of-calls-in-half-open-state : Half_Open 상태에서 Open/Closed 상태 전환을 판단할 요청의 개수입니다.
- 저는 3개로 설정했습니다.
- 3개 요청이 전부 성공 -> Closed 상태로 전환
- 3개 요청 중 1개라도 실패 -> Open 상태로 전환
- 저는 3개로 설정했습니다.
- max-wait-duration-in-half-open-state : Half_Open 상태를 유지할 시간을 지정합니다.
- 기본값은 0으로, 위에서 설정한 permitted calls 개수의 요청이 올 때까지 무한정으로 Half_Open 상태에서 대기합니다.
- 값을 설정하면, 해당 시간만큼 대기했는데 permitted calls 개수까지 도달하지 않았다면 Open 상태로 전환됩니다.
- 저는 값을 설정하지 않고 기본값(0ms)으로 설정해서 설정한 요청 수만큼 왔을 때 Half_Open 상태가 전환되도록 했습니다.
- sliding-window-type : 요청 저장 및 집계에 사용하는 슬라이딩 윈도우의 Type을 지정합니다.
- Type은 앞에서 살펴봤듯이 요청 개수 기반 Count-based와 요청 시간 기반 Time-based가 존재합니다.
- Enum 타입으로 'COUNT_BASED' 또는 'TIME_BASED'를 지정합니다. (기본값은 COUNT_BASED)
- 저는 COUNT_BASED 슬라이딩 윈도우를 선택했습니다.
- sliding-window-type : 슬라이딩 윈도우의 크기를 설정합니다.
- 저는 10으로 설정해서 요청 개수가 10개 단위로 기록될 때 집계되도록(상태 판단) 설정했습니다.
- minimum-number-of-calls : 슬라이딩 윈도우 하나의 단위당 slow-call, failure-call이 계산되는 최소 요청 수를 의미합니다.
- 슬라이딩 윈도우 사이즈가 10이고, 해당 값이 5일 때(해당 값이 슬라이딩 윈도우보다 작을 때)
- 5개 요청이 들어왔을 때 해당 요청들의 slow-call, failure-call을 계산해서 상태 전환을 판단합니다.
- 슬라이딩 윈도우 사이즈가 10이고, 해당 값이 12일 때(해당 값이 슬라이딩 윈도우보다 클 때)
- 해당 값이 슬라이딩 윈도우 크기를 초과하므로, 슬라이딩 윈도우 사이즈인 10개로 계산됩니다.
- 기본값은 슬라이딩 윈도우 크기와 같은 값으로 설정됩니다. 저도 슬라이딩 윈도우 사이즈와 같게 10으로 설정했습니다.
- 슬라이딩 윈도우 사이즈가 10이고, 해당 값이 5일 때(해당 값이 슬라이딩 윈도우보다 작을 때)
- wait-duration-in-open-state : Open -> Half_Open으로 최소 대기 시간을 설정합니다.
- 저는 10초로 지정하여 Open 상태에서 최소 10초가 지나고 요청이 왔을 때 Half_Open 상태가 되도록 설정했습니다.
- failure-rate-threshold : 실패 요청(failure call)의 임계치를 설정합니다. 요청 실패가 실패 임계치 이상이 되면 OPEN 상태로 전환됩니다.
- resilience4j.circuitbreaker.instances : circuitbreaker를 적용할 인스턴스를 지정합니다.
- shboard-circuit-breaker : 임의의 인스턴스 이름을 지정했습니다.
- base-config : 앞에서 설정한 config를 해당 인스턴스에 지정했습니다. (설정 Name : default)
- shboard-circuit-breaker : 임의의 인스턴스 이름을 지정했습니다.
서킷 브레이커 관련 설정이 이외에도 많은데 다른 설정 값들이 궁금하시면 아래의 공식문서를 참고하면 될 것 같습니다.
https://resilience4j.readme.io/docs/circuitbreaker
4-3. OpenFeign CircuitBreaker 설정
OpenFeign 인터페이스에서 @CircuitBreaker을 선언해서 서킷 브레이커를 설정해줬습니다.
@FeignClient(name = "member-service", path = "/api/members")
@CircuitBreaker(name = "circuit")
public interface MemberFeignClient {
@GetMapping
MemberFeignResponse findMemberIdByLoginId(@RequestParam("loginId") final String loginId);
}
이렇게 MemberFeignClient의 메소드가 호출될 때 'circuit'이라는 서킷 브레이커 인스턴스가 실행되도록 했습니다.
4-4. Circuit Breaker 테스트해보기
이제 시나리오를 정하고 Circuit Breaker를 테스트해보겠습니다.
우선, {호출하는 서버 URL}/actuator/circuitbreakers로 요청을 보내면 다음과 같은 응답을 받을 수 있습니다.
이처럼 현재 Circuit Breaker의 상황을 Check할 수 있습니다.
테스트를 위해 호출받는 Memeber 서비스에서 다음과 같이 에러를 발생시키게 해봤습니다.
@GetMapping
public MemberFeignResponse findMemberIdByLoginId(@RequestParam("loginId") final String loginId) {
throw new RuntimeException("Fail ^_^");
// return memberService.findMemberByLoginId(loginId);
}
서버를 실행시키고, Postman으로 요청을 보냈을 때 다음과 같이 NoFallbackAvailableException이 발생하는 것을 알 수 있습니다.
이처럼 기본적으로 호출 실패 시 Fallback이 존재하지 않는다면 NoFallbackAvailableException이 발생하도록 되어있습니다.
저는 ControllerAdvice에서 해당 예외를 잡아서 처리해줬습니다.
Circuit Breaker 시나리오
- Sliding Window type : COUNT_BASED로, 요청 개수 타입으로 설정
- Sliding Window 크기 : 10 -> 10개의 요청 단위로 Circuit Breaker 상태 판단 (기본은 Closed 상태)
- failure call 비율 : 50 & slow call 비율 : 80 -> 10번째 요청 시 저장된 요청 결과 판단해서
- failure call 비율이 50%가 넘거나 slow call 비율이 80%가 넘는다면 Open 상태로 전환
- Half_Open 상태에서 상태 전환 요청 개수 : 3개로 설정
- 3개 요청이 전부 성공 -> Closed 상태로 전환
- 3개 요청 중 1개라도 실패 -> Open 상태로 전환
- Open -> Half_Open 최소 대기 시간 : 10초로 설정
1. 10개 요청 실패 시 Closed -> Open 상태 테스트
첫 번째로, 10개 요청이 실패했을 때 Circuit Breaker가 Closed 상태에서 Open 상태로 전환되는지 테스트해보겠습니다.
호출받는 Member-Service 서버를 실행하지 않고 Board-Service에 10번 요청을 보내도록 하겠습니다.
상태 체크는 위와 마찬가지로 {호출하는 서버 URL}/actuator/circuitbreakers로 체크합니다.
9번 요청을 보냈을 때 failedCalls에 9개가 쌓이고, 아직 CLOSED 상태입니다.
10번째 요청을 하면, 실패함과 동시에 state가 OPEN 상태가 되는 것을 확인할 수 있습니다.
2. Open 상태에서 10초 지난 후 Half_Open 상태 진입 테스트
위의 예시에서 Open 상태로 전환된 10초 이후에 다시 한번 요청을 보내서 Half_Open 상태로 전환되는지 확인해봤습니다.
정상적으로 Half_Open 상태로 전환된 것을 알 수 있습니다.
3. Half_Open 상태에서 Closed/Open 전환 테스트
먼저 Half_Open 상태에서 Member-Service의 서버를 실행시켜서
3번의 요청이 성공하도록 해서 Closed로 전환되는지 확인해봤습니다.
failure call 비율 : 50 & slow call 비율 : 80이므로 3번의 요청 중에서
2번 이상 요청이 실패한다면 OPEN으로 전환되고, 2번 미만 요청이 실패하면 CLOSED로 전환될 것입니다.
3번의 요청 중 1번이 실패하고, 2번이 성공했을 경우 버퍼 요청이 비워지고 CLOSED 상태로 전환되었습니다.
3번의 요청 중 2번이 실패하고, 1번이 성공했을 경우 버퍼에 3개가 쌓인 상태로, OPEN 상태로 전환되었습니다.
이때 failureRate가 66.6%로 failureRateThreshold보다 높기 때문에 OPEN 상태로 전환되는 것을 확인할 수 있습니다.
이렇게 테스트까지 해서 기본 설정은 마무리하도록 하겠습니다.
다음으로는 추가적인 설정 부분을 살펴보겠습니다.
5. Resilience4j 추가 설정(Fallback, Bulkhead)
이번에는 앞서 말했던 Fallback과 Bulkhead를 적용해보도록 합시다.
5-1. OpenFeign Fallback 설정하기
앞서 Fallback 관련해서 설명을 했었는데, 간략하게 요약하면 다음과 같습니다.
Circuit Breaker가 Open 상태인 상황에서
사용자 요청을 에러로 응답하지 않고 성공(미리 설정한 응답)으로 응답하는 것을 의미합니다.
이러한 Fallback을 적용해보도록 하겠습니다.
현재 Fallback과 관련해서 아무런 설정을 하지 않았을 때는 위에서 테스트했던 것처럼
NoFallbackAvailableException을 처리하는 ExceptionHandler에 의해 에러 응답이 반환됩니다.
이러한 상황에서 다음과 같은 비즈니스 요구사항이 도출되었다고 해봅시다.
Member-Service에 장애가 생겼을 때는 게시글 작성자를 '익명'으로 설정하여 작성이 되도록 한다.
물론 게시글 서비스에서 이러한 비즈니스 요구사항은 이상할 수 있겠지만, Fallback 적용을 위해 임의로 설정했습니다.
1. OpenFeign Fallback 클래스 생성
먼저, OpenFeign 요청 실패 시 처리할 Fallback 작업을 정의할 클래스를 생성하고 작업 메소드를 만들어봅시다.
@Component
public class MemberFeignFallback implements MemberFeignClient {
@Override
public MemberFeignResponse findMemberIdByLoginId(final String loginId) {
return new MemberFeignResponse(-1L, "익명");
}
}
저는 'MemberFeignFallback' 클래스를 만들고 MemberFeignClient(OpenFeign 인터페이스)를 구현했습니다.
또한, @Component를 선언해서 스프링 빈으로 등록했습니다.
Fallback의 원리는 요청이 실패하면 기존 응답이 아니라, 여기서 Override한 메소드가 실행되게 됩니다.
따라서 Override 메소드에 요청 실패 시 정상인 것처럼 보이게 하는 응답을 작성하면 됩니다.
저는 '익명'이라는 MemberNickname을 담는 기존 MemberFeignResponse를 생성하여 반환하도록 했습니다.
2. OpenFeignClient에서 Fallback 클래스 지정
OpenFeignClient에서 앞서 만든 Fallback 클래스를 @FeignClient의 옵션으로 지정하면,
OpenFeign 요청 실패시 해당 클래스의 메소드가 실행되게 됩니다.
@FeignClient(name = "member-service", path = "/api/members", fallback = MemberFeignFallback.class)
@CircuitBreaker(name = "circuit")
public interface MemberFeignClient {
@GetMapping
MemberFeignResponse findMemberIdByLoginId(@RequestParam("loginId") final String loginId);
}
Fallback 적용 전 응답
NoFallbackAvailableException을 처리하는 ExceptionHandler에 의한 에러 응답이 반환됐었습니다.
Fallback 적용 후 응답
Member-Service 서버에 장애가 발생했지만, 게시글 작성 시 Fallback 메소드가 실행되어 201 Created로 정상 응답을 반환했습니다.
게시글을 조회해보면, 기존 유저의 닉네임이 아닌 Fallback 메소드에서 설정한 '익명'으로 작성된 것을 확인할 수 있습니다.
5-2. Bulkhead 설정하기
resilience4j의 Bulkhead는 다음과 같이 2개 종류의 구현체로 나뉩니다.
- SemaphoreBulkhead : 동시 호출 요청 수 제한
- FixedThradPoolBulkhead : 스레드풀 설정 제공
기본적으로 세마포어 Bulkhead를 사용하면 간단하게 동시 호출 요청 수를 제한해서 자원을 격리할 수 있습니다.
좀 더 추가적으로 설정하기 위해서는 고정 스레드풀 Bulkhead를 사용하여 스레드를 조절하여 자원을 세세하게 격리할 수 있습니다.
저는 간단한 SemaphoreBulkhead를 통해 동시 요청을 제한해보겠습니다.
1. application.yml 설정
resilience4j:
bulkhead:
configs:
default:
max-concurrent-calls: 5
max-wait-duration: 0
instances:
board-write-member-feign-bulkhead:
base-config: default
- resilience4j.bulkhead.configs : bulkhead 관련 설정 시작점입니다.
- max-concurrent-calls : 최대 동시 요청 수를 의미합니다.
- max-wait-duration : 최대 동시 요청 수를 초과했을 때 대기할 시간입니다.
2. 호출 메소드에서 Bulkhead 적용
@FeignClient(name = "member-service", path = "/api/members", fallback = MemberFeignFallback.class)
@CircuitBreaker(name = "circuit")
public interface MemberFeignClient {
@GetMapping
@Bulkhead(name = "board-write-member-feign-bulkhead")
MemberFeignResponse findMemberIdByLoginId(@RequestParam("loginId") final String loginId);
}
OpenFeign Client에서 위와 같이 @Bulkhead 메소드를 적용하여 Bulkhead를 적용했습니다.
이상으로, 서비스 장애 대응 Circuit Breaker 구현을 마무리하겠습니다.
🎯 Github Repository 링크 (전체 코드)
https://github.com/sh111-coder/sh-board-msa
📘 Monolithic to MSA 전체 목차
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (1) MSA란?
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (2) 멀티 모듈 구성하기
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (3) Service Discovery 패턴 적용하기(feat. Spring Cloud Eureka)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (4) API Gateway 구현(feat. Spring Cloud Gateway)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (5) 서비스 간 통신하기(feat.Spring Cloud OpenFeign)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (6) 각 서비스의 설정 파일 관리하기(feat. Spring Cloud Config)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (7) 서비스 장애 대응 Circuit Breaker 구현(feat. Resilience4J)
[MSA] 개인 프로젝트 Monolithic to MSA 전환기 - (10) MSA 전환 후 비교 및 회고 + 마무리
Reference
https://mangkyu.tistory.com/261
https://hudi.blog/circuit-breaker-pattern/