이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › 아키텍처와 운영 › 서비스 분리
서킷 브레이커
죽은 서비스를 계속 부르면 나까지 죽는다
주문 서비스가 결제 서비스를 1초에 한 번씩 호출합니다. 도중에 결제 서비스가 한동안 응답하지 않습니다. 서킷 브레이커는 실패가 일정 비율을 넘으면 호출 자체를 막아서, 응답 없는 서비스를 기다리느라 스레드가 묶이는 것을 끊어 줍니다.
실행으로 대조 · Resilience4j 2.4.0에 27가지 조합의 호출 순서를 넣어 대조
이 실습에 나오는 개념
- 서킷 브레이커
- 호출 대상 서비스에 장애가 났을 때 계속 호출하며 기다리지 않도록 호출을 차단하는 장치다. 실패가 일정 수준을 넘으면 차단 상태가 되어 이후 호출을 즉시 실패 처리하고, 일정 시간이 지나면 일부 호출을 시험적으로 보내 회복되었는지 확인한다. 한 서비스의 장애가 호출하는 서비스들로 번지는 것을 막는다.
- 모놀리식과 MSA(마이크로서비스)
- 모놀리식은 모든 기능을 하나의 애플리케이션으로 만들어 함께 배포하는 구조다. MSA는 기능별로 작은 서비스로 나눠 각각 독립적으로 개발, 배포, 확장하는 구조다. MSA는 서비스별로 따로 배포하고 확장할 수 있고 장애가 격리되지만, 서비스 간 통신, 데이터 일관성, 운영의 복잡도가 크게 늘어난다.
- 병목(Bottleneck)
- 시스템 전체의 처리 성능을 제한하는 가장 느린 부분이다. 다른 부분을 아무리 개선해도 병목이 그대로면 전체 성능은 나아지지 않는다. 성능 개선은 측정으로 병목을 찾는 것에서 시작한다.
이렇게 바꿔 보세요
- 처음 설정으로 — 장애 15초. 실패가 쌓이면 차단하고, 주기적으로 회복됐는지 떠봅니다.
- 서킷 브레이커가 없다면 — 장애 내내 매 호출이 3초씩 기다리다 실패합니다.
- 임계치를 낮추면 — 30%. 더 빨리 차단합니다.
- 차단 시간을 길게 — 10초. 장애 중에 회복됐는지 떠보는 횟수가 줄어듭니다.
- 짧은 장애 — 장애 5초에 임계치 80%. 실패율이 임계치에 닿지 않아 서킷이 열리지 않습니다.
처음 설정으로 실행하면
- 1초: 결제 서비스 호출 성공.
- 2초: 결제 서비스 호출 성공.
- 3초: 결제 서비스 호출 성공.
- 4초: 결제 서비스 호출 성공.
- 5초: 결제 서비스 호출 성공.
- 6초: 결제 서비스 호출 성공.
- 7초: 결제 서비스 호출 성공.
- 8초: 결제 서비스 호출 성공.
- 9초: 결제 서비스 호출 성공.
- 10초: 결제 서비스 호출 성공. 최근 10번의 실패율 0%.
- 11초: 결제 서비스가 응답하지 않습니다. 3초를 기다리다 타임아웃으로 실패합니다. 그동안 요청 스레드 하나가 묶여 있었습니다. 최근 10번의 실패율 10%.
- 12초: 결제 서비스가 응답하지 않습니다. 3초를 기다리다 타임아웃으로 실패합니다. 그동안 요청 스레드 하나가 묶여 있었습니다. 최근 10번의 실패율 20%.
- 13초: 결제 서비스가 응답하지 않습니다. 3초를 기다리다 타임아웃으로 실패합니다. 그동안 요청 스레드 하나가 묶여 있었습니다. 최근 10번의 실패율 30%.
- 14초: 결제 서비스가 응답하지 않습니다. 3초를 기다리다 타임아웃으로 실패합니다. 그동안 요청 스레드 하나가 묶여 있었습니다. 최근 10번의 실패율 40%.
- 15초: 결제 서비스가 응답하지 않습니다. 3초를 기다리다 타임아웃으로 실패합니다. 그동안 요청 스레드 하나가 묶여 있었습니다. 최근 10번 중 실패율이 50%로 임계치 50% 이상이 됐습니다. 서킷을 엽니다(OPEN). 이제부터 5초 동안 결제 서비스 호출을 막습니다.
- 16초: 서킷이 열려 있습니다(OPEN). 결제 서비스를 호출하지 않고 즉시 대체 응답을 돌려줍니다. 기다리는 시간이 없습니다.
- 17초: 서킷이 열려 있습니다(OPEN). 결제 서비스를 호출하지 않고 즉시 대체 응답을 돌려줍니다. 기다리는 시간이 없습니다.
- 18초: 서킷이 열려 있습니다(OPEN). 결제 서비스를 호출하지 않고 즉시 대체 응답을 돌려줍니다. 기다리는 시간이 없습니다.
- 19초: 서킷이 열려 있습니다(OPEN). 결제 서비스를 호출하지 않고 즉시 대체 응답을 돌려줍니다. 기다리는 시간이 없습니다.
- 20초: 차단한 지 5초가 지났습니다. 회복됐는지 확인하려고 호출을 몇 번만 시험 삼아 통과시킵니다(HALF_OPEN). 결제 서비스가 응답하지 않습니다. 3초를 기다리다 타임아웃으로 실패합니다. 그동안 요청 스레드 하나가 묶여 있었습니다. 시험 호출 1/3.
- 21초: 결제 서비스가 응답하지 않습니다. 3초를 기다리다 타임아웃으로 실패합니다. 그동안 요청 스레드 하나가 묶여 있었습니다. 시험 호출 2/3.
- 22초: 결제 서비스가 응답하지 않습니다. 3초를 기다리다 타임아웃으로 실패합니다. 그동안 요청 스레드 하나가 묶여 있었습니다. 시험 호출 3번의 실패율이 100%로 임계치 이상입니다. 아직 회복되지 않았다고 보고 다시 차단합니다(OPEN).
- 23초: 서킷이 열려 있습니다(OPEN). 결제 서비스를 호출하지 않고 즉시 대체 응답을 돌려줍니다. 기다리는 시간이 없습니다.
- 24초: 서킷이 열려 있습니다(OPEN). 결제 서비스를 호출하지 않고 즉시 대체 응답을 돌려줍니다. 기다리는 시간이 없습니다.
- 25초: 서킷이 열려 있습니다(OPEN). 결제 서비스를 호출하지 않고 즉시 대체 응답을 돌려줍니다. 기다리는 시간이 없습니다.
- 26초: 서킷이 열려 있습니다(OPEN). 결제 서비스를 호출하지 않고 즉시 대체 응답을 돌려줍니다. 기다리는 시간이 없습니다.
- 27초: 차단한 지 5초가 지났습니다. 회복됐는지 확인하려고 호출을 몇 번만 시험 삼아 통과시킵니다(HALF_OPEN). 결제 서비스 호출 성공. 시험 호출 1/3.
- 28초: 결제 서비스 호출 성공. 시험 호출 2/3.
- 29초: 결제 서비스 호출 성공. 시험 호출 3번의 실패율이 0%로 임계치보다 낮습니다. 회복됐다고 보고 정상 상태로 돌아갑니다(CLOSED).
- 30초: 결제 서비스 호출 성공.
- 31초: 결제 서비스 호출 성공.
- 32초: 결제 서비스 호출 성공.
- 33초: 결제 서비스 호출 성공.
- 34초: 결제 서비스 호출 성공.
- 35초: 결제 서비스 호출 성공.
- 36초: 결제 서비스 호출 성공.
- 37초: 결제 서비스 호출 성공.
- 38초: 결제 서비스 호출 성공.
- 39초: 결제 서비스 호출 성공. 최근 10번의 실패율 0%.
- 40초: 결제 서비스 호출 성공. 최근 10번의 실패율 0%.
실행 결과
- 상태가 CLOSED → OPEN → HALF_OPEN → OPEN → HALF_OPEN → CLOSED 순으로 바뀌었습니다. 열려 있는 동안에는 호출하지 않고 바로 대체 응답을 주므로 스레드가 묶이지 않고, 장애 난 서비스에도 부하를 주지 않아 회복할 틈을 줍니다. 일정 시간 뒤에는 소수의 호출만 보내 회복 여부를 확인합니다.
- 차단 시간이 길면 서비스가 회복된 뒤에도 한동안 정상 호출까지 막힙니다. 짧으면 회복되지 않은 서비스를 자주 떠보게 됩니다. 임계치와 차단 시간은 서비스 특성에 맞춰 조정합니다.
코드
# application.yml (Resilience4j)
resilience4j.circuitbreaker.instances.payment:
slidingWindowSize: 10 # 최근 10번의 결과로 판단
minimumNumberOfCalls: 10
failureRateThreshold: 50 # 실패율이 이 값 이상이면 차단
waitDurationInOpenState: 5s # 차단 유지 시간
permittedNumberOfCallsInHalfOpenState: 3
@CircuitBreaker(name = "payment", fallbackMethod = "payLater")
PaymentResult pay(Order order) {
return paymentClient.pay(order); // 응답이 없으면 3초 뒤 타임아웃
}
PaymentResult payLater(Order order, Throwable t) {
return PaymentResult.pending(); // 호출이 실패하거나 차단됐을 때 대신 실행
}
이 모형의 전제
호출 횟수 기준 슬라이딩 윈도(최근 10번)를 쓰는 Resilience4j의 동작을 따랐습니다. 서킷 브레이커를 쓰는 27가지 조합 모두를 실제 Resilience4j 2.4.0 서킷 브레이커에 같은 호출 순서로 넣어(시계를 1초씩 돌리는 방식), 성공·실패·차단 횟수와 상태 전환 순서가 화면과 일치하는 것을 확인했습니다. 한 가지 차이가 있습니다. Resilience4j는 차단 시간이 완전히 지난 뒤의 호출부터 시험 호출로 통과시키므로, 호출이 차단 시간과 정확히 같은 시각에 맞물리면 그 호출까지는 차단됩니다. 화면은 호출이 그 시각보다 아주 조금 뒤에 온다고 본 결과입니다. 실패율이 임계치 이상이면 열리고, 정해진 시간 뒤 시험 호출 3번의 결과로 닫을지 다시 열지 정합니다. 호출은 1초에 한 번, 장애 중인 서비스는 항상 3초 타임아웃으로 실패한다고 단순화했습니다. 실제로는 느린 응답 비율도 기준으로 삼을 수 있고, 시간 기준 윈도도 있습니다.
앞 실습: Kafka 파티션과 컨슈머 · 다음 실습: Saga와 Outbox