09.MSA 장애처리
MSA의 문제점
- MSA를 운영 하다 보면 어느 한 서버의 장애가 전체 장애로 확산이 되어 더 큰 문제가 되는 경우가 있다.
- 이를 해결하기 위해 각 마이크로 서비스 간 전파 차단기 역할을 하는 Circuit Breaker가 있다
Circuit Breaker의 역할
- 마이크로 서비스 사이에서 서비스의 상태를 확인하여 정상일 때 API를 전달하고, 정상이 아님을 감지 했을 경우에는 API를 전달하지 않고 쓰레드들이 응답을 기다리지 않도록 다른 메세지로 답을 하여 장애 전파가 되지 않도록 해주는 역할이다.
Circuit Breaker Pattern
- MSA에서는 일반적으로 다른 서비스를 호출하여 데이터를 검색하며 Downstream 서비스가 다운될 가능성이 있다.
- 이는 느린 네트워크 연결, 시간 초과 또는 일시적인 사용 불가능으로 인해 발생할 수 있다.
- 특정 서비스에 심각한 문제가 있는 경우 더 오랜 시간 동안 장애가 지속될 수 있고, 이러한 경우 Client는 특정 서비스가 다운되었다는 사실을 알지 못하기 때문에 요청이 해당 서비스로 계속 전송된다.
- 결과적으로 애플리케이션 전반에 걸친 계단식 실패로 지속적인 장애로 이어질 수 있다.
- 이러한 계단식 실패를 멈추게 해주는 것이 Circuit Breaker Pattern이다.
- Circuit Breaker Pattern은 MSA에서 사용되는 인기있는 디자인 패턴이다.
Closed Status
- 이 상태에서 circuit breaker는 요청을 마이크로 서비스로 라우팅하고 각 기간의 실패 횟수를 계산한다. 작동 중
- 특정 기간 동안의 오류 수가 임계값을 초과하면 회로가 trip되고 개방 상태로 이동한다.
Open Status
- 회로 차단기가 열림 상태로 이동하면 마이크로 서비스의 요청이 즉시 실패하고 예외가 반환된다.
- 시간 초과 후 회로 차단기는 Half-Open 상태로 전환된다.
Half-Open Status
- 이 상태에서 회로 차단기는 마이크로 서비스의 제한된 수의 요청만 통과하도록 허용하고 작업을 호출한다
- 이러한 요청이 성공하면 회로 차단기가 닫힘 상태로 돌아간다
- 그러나 요청이 실패하면 열림 상태로 돌아간다
Circuit Breaker Pattern의 효과
- 1.. Client가 Server의 응답을 기다리지 않고 다른 행동을 할 수 있도록 한다
- 2.. Server는 처리가 불가능한 상태에서 더 많은 API 호출이 몰리지 않도록 하여 부하를 줄인다
- 3.. 다른 마이크로 서비스로 API 호출을 전달하지 않아 2차 장애를 예방한다
circuit breaker basic concept
- circuit breaker는 3가지 상태에 대한 FSM(Final State Machine)을 기반으로 동작한다.
- closed : circuit breaker로 감싼 내부 프로세스가 요청과 응답을 정상적으로 주고 받을 수 있는 상태
- open : circuit breaker로 감싼 내부 프로세스가 요청과 응답을 정상적으로 주고 받을 수 없는 상태
- circuit breaker는 미리 지정해둔 fall back 응답을 수행 할 수 있다.
- 또는 event publisher를 이용하여 이벤트를 발생 시킬 수도 있다
- half-open : fall back 응답을 수행하고 있지만 실패율을 측정해서 close 또는 open으로 변경될 수 있는 상태
- 요청과 실패에 대한 metric을 수집하고, 미리 지정해둔 조건에 따라 상태가 변화한다.
- 그 외에도 metric을 수집하지 않는 2가지 특수상태가 있다
- disabled : circuit breaker를 강제로 closed한 상태. metric을 수집하지 않고 상태 변화도 없다
- forced_open : circuit breaker를 강제로 open한 상태. metric을 수집하지 않고 상태 변화도 없다.
circuit breaker state transit
- circuit breaker는 metric을 수집하고 분석한다. 수집한 결과는 원형 배열 형태의 sliding window에 담긴다
- Count-based sliding window
- n개의 원형 배열로 구현된다
- 각 원소들은 FIFO 방식으로 갱신된다
- Time-based sliding window
- n개의 원형 배열로 구현된다
- 단위는 epoch second를 사용한다
- 10으로 설정할 시, 1초씩 10개의 원소가 생겨난다.
- 각 원소들은 시간의 흐름에 따라 FIFO 방식으로 갱신된다.
- 두 타입 모두 요청이 실패했음을 판단하는 기준이 동일하다. 요청 실패의 기준은 2가지이다
- 1.. exception 발생
- 2.. slow call(정상적으로 수행은 됐지만 지나치게 느린 경우)
- circuit breaker의 open state transit은 exception과 slow call의 관계없이, 지정한 실패율이 달성되면 바로 진행된다.
- sliding window size 10, failure rate 50% 상태에서의 예시 상황을 살펴보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 10개 요청 중 4개 요청에서 exception 발생
- state transit ( X )
2. 10개 요청 중 4개 요청에서 slow call 발생
- state transit ( X )
3. 10개 요청 중 2개 요청에서 exception 발생, 2개 요청에서 slow call 발생
- state transit ( X )
4. 10개 요청 중 5개 요청에서 exception 발생
- closed state transit to open ( O )
5. 10개 요청 중 5개 요청에서 slow call 발생
- closed state transit to open ( O )
- 또한 circuit breaker의 state transit은 sliding window 크기만큼 호출이 기록되지 않으면 상태변화를 일으키지 않는다.
- 예를들어 sliding window size 10, failure rate 50% 에서 9개의 요청 모두 exception이 발생해도 10번이 안되었기 때문에 open으로 변환은 진행되지 않는다.
circuit breaker Test
1
2
3
4
5
6
7
dependencies{
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
// ...
implementation("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j")
implementation("org.springframework.boot:spring-boot-starter-aop")
}
- aop 라이브러리 의존이 추가되지 않으면 정상적으로 동작하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# application.yml
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowType: COUNT_BASED
slidingWindowSize: 10
failureRateThreshold: 50
permittedNumberOfCallsInHalfOpenState: 5
registerHealthIndicator: true
management:
endpoints:
web:
exposure:
include:
- "*" # 테스트를 위해 actuator 전체 노출
health:
circuitbreakers:
enabled: true # circuitbreakers 정보 노출
- 최근 10회 요청 중 50%(5회) 이상 요청 실패 시 open으로 상태 전환
1
2
3
4
5
@RestController
class CircuitBreakerTestController(private val circuitBreakerTestService: CircuitBreakerTestService,){
@GetMapping("/cats/{id}/image")
fun catImage(@PathValue id: Long): String = circuitBreakerTestService.catImage(id = id)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
class CircuitBreakerTestService{
@CircuitBreaker(name = "cat-image-circuit-breaker", fallbackMethod = "fallbackCatImage")
fun catImage(id: Long): String{
if(id < 10L){
return "$id cat's image.png"
}
throw RuntimeException("there is no cat's image for $id")
}
private fun fallbackCatImage(id: Long, t: Throwable): String{
return "fallback cat image.png"
}
}
- 위 설정을 사용하여 의도적으로 RuntimeException이 발생하는 요청을 9번 보낸다
1
2
3
curl -X GET http://localhost:8080/cats/99/image
fallback cat image.png
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
curl -X GET http://localhost:8080/actuator/circuitbreakers | jq
{
"circuitBreakers": {
"cat-image-circuit-breaker": {
"failureRate": "-1.0%",
"slowCallRate": "-1.0%",
"failureRateThreshold": "50.0%",
"slowCallRateThreshold": "100.0%",
"bufferedCalls": 9,
"failedCalls": 9,
"slowCalls": 0,
"slowFailedCalls": 0,
"notPermittedCalls": 0,
"state": "CLOSED"
}
}
}
- 9회차까지 실패하는 요청을 보내고, 마지막 10회차에 정상 요청을 보냈다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
curl -X GET http://localhost:8080/actuator/circuitbreakers | jq
{
"circuitBreakers": {
"cat-image-circuit-breaker": {
"failureRate": "90.0%",
"slowCallRate": "0.0%",
"failureRateThreshold": "50.0%",
"slowCallRateThreshold": "100.0%",
"bufferedCalls": 10,
"failedCalls": 9,
"slowCalls": 0,
"slowFailedCalls": 0,
"notPermittedCalls": 0,
"state": "OPEN"
}
}
}
- 전체 10회 요청 중 마지막 1회를 제외하고 9회의 요청이 실패했기 때문에 circuit breaker가 open 상태로 전환되었다.
- 그 후 정상요청을 다시 보냈다.
1
2
3
curl -X GET http://localhost:8080/cats/1/image
fallback cat image.png
- 정상요청을 보냈음에도 circuit breaker는 open 상태이기 때문에 해당 메서드가 정상적으로 요청을 처리할 수 없다고 판단하여, fallback 메서드로 응답처리 하였다.
circuit breaker 사용
- circuit breaker는 무조건적인 성공을 위해 사용된다
- open이 아닌 상태에도 예외가 발생하면 무조건 fallback 메소드를 호출한다
- 때문에 데이터 정확도가 중요한 서비스에서는 circuit breaker 사용이 적절하지 않다
- 데이터 정확도보다 서비스 안정성이나 응답속도가 더 중요한 경우에 사용한다
- 의존 서비스의 장애가 현재 서비스에 영향을 주는 경우에 사용이 적절
- 의존 서비스의 지연이 현재 서비스의 지연이 되는 경우에 사용이 적절