포스트

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에 담긴다
  1. Count-based sliding window
    • n개의 원형 배열로 구현된다
    • 각 원소들은 FIFO 방식으로 갱신된다
  2. 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 사용이 적절하지 않다
  • 데이터 정확도보다 서비스 안정성이나 응답속도가 더 중요한 경우에 사용한다
    • 의존 서비스의 장애가 현재 서비스에 영향을 주는 경우에 사용이 적절
    • 의존 서비스의 지연이 현재 서비스의 지연이 되는 경우에 사용이 적절
참고 사이트

BESPING GLOBAL tech blog
조대협의 블로그
현구막 기술 블로그