마이크로서비스 환경에서 하나의 느린 백엔드가 전체 서비스를 끌어내리는 장애를 겪어 본 팀이라면 서킷브레이커의 필요성을 몸으로 압니다. 응답이 지연되는 업스트림에 요청이 쌓이면 커넥션과 스레드가 고갈되고, 그 압력은 호출 측으로 역류해 연쇄 장애(cascading failure)로 번집니다. Envoy는 이를 두 메커니즘으로 방어합니다. 동시성 상한을 강제하는 서킷브레이커(circuit breaker)와, 오동작 호스트를 골라 격리하는 아웃라이어 감지(outlier detection)입니다.
많은 팀이 이 둘을 뭉뚱그려 부르지만 동작 지점과 목적이 다릅니다. 서킷브레이커는 “클러스터 전체 부하가 임계를 넘으면 즉시 거절한다”는 총량 제어이고, 아웃라이어 감지는 “이 인스턴스가 유독 에러를 많이 뱉으니 풀에서 잠시 빼겠다”는 개별 격리입니다. 이 글에서는 설정값 하나하나가 무엇을 방어하는지와 실전 관측·튜닝 방법을 정리합니다.
서킷브레이커와 아웃라이어 감지는 무엇이 다른가
Envoy의 서킷브레이커는 클러스터 단위로 동작하는 카운터 기반 리소스 상한입니다. 커넥션 수, 대기 요청 수, 활성 요청 수, 재시도 수가 임계값에 도달하면 신규 요청을 즉시 실패시킵니다. 트립(trip)되면 큐잉 없이 503 UpstreamOverflow로 응답하므로 부하가 downstream으로 새어 나가는 대신 빠르게 실패(fail-fast)합니다.
아웃라이어 감지는 반대로 클러스터 안의 개별 호스트를 관찰합니다. 어떤 인스턴스가 연속으로 5xx를 반환하거나 성공률이 다른 호스트 대비 통계적으로 뒤처지면 그 호스트를 일정 시간(ejection time) 동안 로드밸런싱 후보에서 제외합니다. 배포 중 한 파드가 망가진 상황에서 자동 복원력을 제공합니다.
- 서킷브레이커: 클러스터 동시성 총량을 제한 → 연쇄 장애·리소스 고갈 방어
- 아웃라이어 감지: 오동작 개별 호스트를 풀에서 격리 → 부분 장애 영향 최소화
- 둘은 배타적이지 않고 함께 켜야 서로 다른 실패 모드를 막는다.
서킷브레이커 임계값 설정
서킷브레이커는 circuit_breakers 블록에서 우선순위별로 설정합니다. 실무에서 이해해야 할 네 가지 임계값입니다.
# cluster 정의 안의 circuit_breakers
circuit_breakers:
thresholds:
- priority: DEFAULT
max_connections: 1024 # 업스트림으로의 동시 TCP 커넥션 상한 (HTTP/1.1에서 주로 유효)
max_pending_requests: 256 # 커넥션 확보를 기다리는 대기 큐 상한
max_requests: 1024 # 동시 진행 중인 요청 상한 (HTTP/2에서 핵심 지표)
max_retries: 32 # 클러스터 전체 동시 재시도 상한 (재시도 폭풍 방지)
track_remaining: true # 남은 여유분을 stats 게이지로 노출
# priority: HIGH 블록을 따로 두면 x-envoy-priority 고우선 트래픽에 별도 상한 적용 가능
프로토콜에 따라 결정적인 지표가 다릅니다. HTTP/1.1은 커넥션당 요청 하나이므로 max_connections가 실질적 동시성 상한입니다. 반면 HTTP/2·gRPC는 단일 커넥션에서 다수 스트림을 다중화하므로 max_connections는 거의 의미가 없고 max_requests가 실제 동시 요청 수를 결정합니다.
아웃라이어 감지 파라미터 상세
아웃라이어 감지는 outlier_detection 블록에서 설정합니다. 감지 방식은 개별 호스트의 절대 실패 횟수를 보는 연속 에러(consecutive)와 호스트 간 성공률을 통계로 비교하는 성공률(success rate)로 나뉩니다.
# cluster 정의 안의 outlier_detection
outlier_detection:
# (1) 연속 5xx 감지: consecutive_5xx 회 연속 실패 시 격리
consecutive_5xx: 5
# (2) 연속 게이트웨이 실패(502/503/504, 커넥션 실패 포함)
consecutive_gateway_failure: 5
consecutive_local_origin_failure: 5 # 로컬 원인 실패(타임아웃·리셋)
split_external_local_origin_errors: true # 로컬/외부 실패 분리 집계
interval: 10s # 감지 분석 주기
base_ejection_time: 30s # 최초 격리 시간 (반복 시 증가)
max_ejection_percent: 50 # 동시 격리 가능한 호스트 비율 상한
# enforcing_*: 감지가 실제 격리를 유발할 확률(%). 0이면 감지만 함
enforcing_consecutive_5xx: 100
enforcing_success_rate: 100
# (4) 성공률 기반: 호스트 평균/표준편차로 이상치 판별
success_rate_minimum_hosts: 5 # 이 수 이상 있어야 성공률 분석 활성화
success_rate_request_volume: 100 # 호스트당 최소 요청 수
success_rate_stdev_factor: 1900 # (평균 - 1.9*표준편차) 미만이면 격리
max_ejection_percent는 핵심 안전장치입니다. 이 값이 없으면 일시적 네트워크 블립으로 모든 호스트가 동시에 격리되어 클러스터가 빌 수 있으니, 50%로 두면 최소 절반은 항상 남습니다. base_ejection_time은 격리가 반복될수록 곱해져(30초→60초→90초) 플래핑(flapping)을 억제합니다. split_external_local_origin_errors를 켜면 “업스트림이 5xx를 준 것”과 “Envoy가 연결조차 못 한 것”을 구분 집계하므로 원인 분석 정확도가 올라갑니다.
전체 클러스터 설정 예시
두 기능을 함께 적용한 실전 클러스터 정의입니다. xDS로 동적 관리할 때도 필드 구조는 동일합니다.
clusters:
- name: payment_service
type: STRICT_DNS
connect_timeout: 1s
lb_policy: LEAST_REQUEST # 아웃라이어 격리 후 부하 재분배에 유리
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {} # gRPC 업스트림 → HTTP/2
circuit_breakers:
thresholds:
- priority: DEFAULT
max_requests: 800 # HTTP/2 이므로 동시 요청 상한이 핵심
max_pending_requests: 200
max_retries: 24
track_remaining: true
outlier_detection:
consecutive_5xx: 5
consecutive_gateway_failure: 5
interval: 5s
base_ejection_time: 30s
max_ejection_percent: 40
success_rate_minimum_hosts: 5
success_rate_request_volume: 50
health_checks:
- timeout: 1s
interval: 5s
unhealthy_threshold: 3
healthy_threshold: 2
grpc_health_check:
service_name: payment.v1.Health
능동 헬스체크(프로브 기반)와 아웃라이어 감지(실제 트래픽 기반)를 함께 둔 점에 주목하세요. 프로브는 통과하지만 특정 엔드포인트만 장애인 상황은 아웃라이어 감지가 잡아내므로 둘은 상호 보완적입니다.
재시도 정책과의 상호작용
서킷브레이커의 max_retries는 라우트 재시도 정책과 함께 봐야 의미가 있습니다. 재시도 정책이 공격적인데 재시도 서킷브레이커가 느슨하면 업스트림 장애 시 재시도가 원래 부하의 몇 배로 증폭됩니다.
# route 레벨 재시도 정책
retry_policy:
retry_on: "5xx,reset,connect-failure"
num_retries: 2
per_try_timeout: 800ms
retry_back_off: # 지수 백오프로 재시도 간격 분산 (폭풍 방지)
base_interval: 100ms
max_interval: 1s
retry_host_predicate: # 방금 실패한 호스트는 재시도 대상에서 제외
- name: envoy.retry_host_predicates.previous_hosts
previous_hosts predicate가 없으면 재시도가 방금 실패한 그 호스트로 다시 가서, 격리 전까지 같은 인스턴스를 계속 때립니다. 이 predicate가 있으면 재시도가 다른 호스트를 고르므로 성공 확률이 올라갑니다. 백오프 없는 즉시 재시도는 폭풍의 주범이므로 retry_back_off도 필수입니다.
관측: 어떤 통계로 튜닝하는가
설정값은 관측 없이 튜닝할 수 없습니다. Envoy는 admin 인터페이스(/stats)로 상태를 노출하며, 다음 지표를 Prometheus로 스크레이프합니다.
# 서킷브레이커: 오버플로(트립)로 거절된 요청 카운터
curl -s localhost:9901/stats | grep 'payment_service.*overflow'
# upstream_rq_pending_overflow: 0 # 대기 큐 초과로 거절
# upstream_rq_retry_overflow: 12 # 재시도 상한 초과로 거절
# upstream_cx_overflow: 0 # 커넥션 상한 초과
# circuit_breakers.default.remaining_rq: 800 # track_remaining 여유분 게이지
# 아웃라이어 감지: 격리된 호스트 수와 누적 격리 횟수
curl -s localhost:9901/stats | grep 'payment_service.outlier'
# outlier_detection.ejections_active: 1
# outlier_detection.ejections_enforced_total: 7
# outlier_detection.ejections_consecutive_5xx: 5
핵심 신호 해석 기준입니다.
- pending_overflow 증가: 서킷브레이커 트립.
remaining_rq가 자주 0에 닿으면 상한 상향 검토. - retry_overflow 증가: 재시도가 상한에 부딪히는 중. 업스트림 장애 경보다.
- ejections_active 지속: 특정 호스트가 계속 오동작. 배포·노드 이슈를 의심한다.
- ejections 짧은 주기 반복: 플래핑.
base_ejection_time을 늘린다.
운영에서 흔히 겪는 함정
실전에서 반복적으로 관찰되는 오설정 패턴입니다.
- HTTP/2에
max_connections만 설정: gRPC는 실제 동시성을max_requests가 통제하므로, 이걸 놓치면 서킷브레이커가 사실상 꺼진 셈이다. - 기본값 의존: 기본 임계값은 모두 1024다. 명시하지 않으면 트래픽이 근처에 닿는 순간 원인 모를
503이 난다. - enforcing 값:
enforcing_consecutive_5xx가 0이면 감지만 하고 격리는 안 한다. 새 방식은 0으로 통계를 먼저 쌓고 안전이 확인되면 100으로 올린다.
# 특정 호스트의 격리 상태를 실시간으로 확인 (admin 엔드포인트)
curl -s localhost:9901/clusters | grep 'payment_service' | grep -E 'health_flags|success_rate'
# payment_service::10.0.3.14:8443::health_flags::/failed_outlier_check
# → failed_outlier_check 플래그가 붙은 호스트가 현재 격리 대상
마무리
Envoy의 복원력 설정은 “값을 크게 잡으면 안전하다”는 문제가 아닙니다. 서킷브레이커는 클러스터 총량을 fail-fast로 제어해 연쇄 장애를 끊고, 아웃라이어 감지는 오동작 호스트를 자동 격리해 부분 장애를 흡수합니다. 둘의 역할이 다르므로 함께 켜야 하고, 프로토콜에 따라 결정적인 임계값이 달라진다는 점을 반영해야 합니다.
실전 절차는 명확합니다. 임계값을 명시하고, 재시도 정책에는 백오프와 previous_hosts predicate를 붙이며, 새 감지 방식은 enforcing 0으로 관측부터 시작합니다. overflow·ejections_active·remaining_rq를 대시보드에 올리면 장애 표면화 전에 조기 개입할 수 있습니다.
자주 묻는 질문
Q. 서킷브레이커가 트립됐는지 어떻게 즉시 알 수 있나요?
A. upstream_rq_pending_overflow와 upstream_cx_overflow 카운터가 증가하면 트립된 것입니다. 트립된 요청은 access log에 응답 플래그 UO(Upstream Overflow)를 남기므로 %RESPONSE_FLAGS%의 UO 비율을 모니터링하면 실시간 감지가 됩니다. 또 track_remaining으로 remaining_rq가 0에 가까워지는 것을 보면 트립 직전도 예측됩니다.
Q. 아웃라이어 감지와 능동 헬스체크 중 하나만 써도 되나요?
A. 둘 다 쓰는 것을 권장합니다. 능동 헬스체크는 완전히 죽은 호스트를 빠르게 걸러 내고, 아웃라이어 감지는 실제 트래픽 결과로 부분 장애를 포착합니다. 아웃라이어 감지만 쓰면 트래픽이 없는 유휴 시간에 죽은 호스트를 못 잡으므로 두 방식은 서로의 사각지대를 메웁니다.
Q. 소규모 클러스터(호스트 2~3개)에서 아웃라이어 감지가 작동하지 않는 것 같습니다.
A. 성공률 기반 감지는 success_rate_minimum_hosts(기본 5) 이상의 호스트가 있어야 활성화됩니다. 그보다 적으면 성공률 분석이 꺼지므로 소규모 클러스터에서는 consecutive_5xx 같은 연속 에러 방식에 의존해야 합니다. max_ejection_percent도 최소 남길 호스트 수를 고려해 조정하세요.