🏠 목록 Istio 회로 차단은 connection pool 한도로 부하를 빠르게 잘라내고 outlier detection으로 unhealthy 인스턴스를 격리해 연쇄 장애를 끊는다 📄 MD 원본 🌓 테마
istiocircuit-breakingconnection-pooloutlier-detectionresilience

Istio 회로 차단은 connection pool 한도로 부하를 빠르게 잘라내고 outlier detection으로 unhealthy 인스턴스를 격리해 연쇄 장애를 끊는다

ℹ 이 문서가 다루는 것

Istio의 "circuit breaking"은 단일 기능이 아니라 두 개의 독립된 방어선이다 — ① connectionPool 한도(동시성·pending·retry 상한 = Envoy circuit_breakers)로 클라이언트가 upstream을 과부하시키지 못하게 빠르게 실패시키고, ② outlierDetection으로 에러를 뱉는 endpoint를 LB pool에서 일시 격리한다. 둘 다 CircuitBreaker CRD가 아니라 DestinationRule trafficPolicy 에 정의되고 istiod가 Envoy cluster로 컴파일한다. 이 note는 왜 이 메커니즘이 존재하고 어떻게 동작하는가에 집중하고, 필드별 컴파일 매핑·전체 YAML 레퍼런스는 Envoy Cluster 해부로 위임한다.

대상환경: Istio 1.30 / Envoy sidecar · 대상독자: cascading failure를 끊고 싶은 DevOps/SRE · 범위: 멘탈모델·메커니즘·관측 · 선행개념: DestinationRule, Envoy cluster, response flag

01. 배경 — 왜 회로 차단이 필요한가 (cascading failure의 메커니즘)

회로 차단을 이해하려면 먼저 무엇으로부터 보호하는가를 알아야 한다. 답은 연쇄 장애(cascading failure) 다. 분산 시스템에서 한 서비스의 느려짐이 전체로 번지는 가장 흔한 경로가 이것이다.

메커니즘은 이렇다. upstream B가 느려지면(GC pause, DB 락, GPU 큐 적체 등), B를 호출하는 A의 요청들이 응답을 기다리며 A 안에 쌓인다. 요청 하나당 thread·socket·메모리를 점유한 채 blocking되므로, B가 느린 시간 동안 A의 자원이 고갈된다. 이제 A 자신이 느려지고, A를 호출하는 C에서 같은 일이 반복된다. 느림이 호출 체인을 거슬러 올라가며 증폭되어, 결국 B 하나의 문제가 mesh 전체를 마비시킨다.

B 느려짐
  → A의 요청이 응답 못 받고 쌓임 (thread/conn/mem 점유)
    → A 자원 고갈 → A도 느려짐
      → C의 요청이 A를 기다리며 쌓임
        → ... 체인 전체로 전파 (cascading failure)

핵심 통찰은 "느린 실패가 빠른 실패보다 나쁘다" 는 것이다. B가 즉시 거절(fast fail)했다면 A는 자원을 즉시 회수하고 fallback·degraded mode로 전환할 수 있다. 하지만 무한정 기다리면(slow fail) 자원이 묶인 채 장애가 번진다. 회로 차단의 본질은 느린 실패를 빠른 실패로 바꿔 장애 전파의 사슬을 끊는 것이다.

여기에 두 번째 문제가 겹친다. upstream이 여러 host(Pod)로 이뤄질 때, 그중 일부만 죽는 경우다. LB는 죽은 host로도 계속 트래픽을 보내고, 그 요청들은 매번 실패한다 — 정상 host가 멀쩡한데도 서비스 에러율이 올라간다. 죽은 host를 자동으로 솎아내는 장치가 따로 필요하다.

Istio는 이 두 문제를 두 개의 다른 메커니즘으로 푼다. 사람들이 "circuit breaker 켜자"고 뭉뚱그리는 그 한 단어 뒤에는, 성격이 전혀 다른 두 방어선이 있다.

02. 핵심 아키텍처 — 방향이 다른 두 방어선 (멘탈모델 anchor)

★ 한 문장 멘탈모델

머릿속에 잡을 그림은 방향의 차이다. connection pool은 내가 upstream을 얼마나 밀어붙이는가(나가는 트래픽의 을 자름 → fail fast), outlier detection은 upstream의 어느 host가 나쁜가(받는 쪽 host를 솎음 → passive health check). 전자는 traffic shaping, 후자는 런타임 host 판별이다. 이 둘을 한 덩어리로 묶으면 "한도를 걸었는데 왜 endpoint가 안 빠지지?" 같은 혼선이 생긴다.

이 anchor 하나에서 나머지가 다 따라 나온다. 01절의 두 문제 — 과부하로 인한 자원 고갈일부 host의 죽음 — 가 정확히 이 두 메커니즘에 대응한다.

DestinationRuletrafficPolicyconnectionPool동시성/pending/retry 한도outlierDetection에러 endpoint 격리circuit_breakers.thresholdsoutlier_detection컴파일컴파일한도 초과 → 503 UO (fail fast)연속 에러 ep → eject
그림 1. DestinationRule.trafficPolicy의 connectionPool/outlierDetection이 Envoy cluster의 circuit_breakers.thresholds와 outlier_detection으로 컴파일. 한도 초과는 503 UO 즉시 실패.

여기서 먼저 잡아야 할 구조적 사실: CircuitBreaker라는 CRD는 없다. 두 메커니즘 모두 DestinationRule의 trafficPolicy 안에 필드로 들어가고, istiod가 그것을 xDS로 각 sidecar의 Envoy cluster 설정으로 컴파일한다. connectionPool은 cluster의 circuit_breakers.thresholds로, outlierDetection은 cluster의 outlier_detection으로 간다. 즉 "회로 차단을 켠다"는 건 목적지(host)를 가리키는 DestinationRule을 쓰는 일이지 별도 리소스를 만드는 일이 아니다.

두 메커니즘은 같은 cluster 안에 공존하지만 독립적으로 동작한다. connectionPool 한도에 걸려도 outlier detection 판정과 무관하고, 그 반대도 마찬가지다. 보통 둘을 같은 DestinationRule에 같이 둔다 — 하나는 양을, 하나는 host를 막으므로 상호 보완적이다.

connection pool (connectionPool) outlier detection (outlierDetection)
답하는 질문 "이 upstream으로 동시에 얼마나 보낼까?" "이 upstream의 어느 host가 나쁜가?"
보호 대상 upstream 전체 — 과부하로 무너지지 않게 개별 endpoint — 죽은 host로 계속 보내지 않게
트리거 현재 동시성/pending/retry가 한도 초과 특정 host가 연속으로 5xx/connect-fail
효과 새 요청을 즉시 거절(503 UO) 그 host를 LB pool에서 일정 시간 제외
Envoy 매핑 circuit_breakers.thresholds outlier_detection
성격 traffic shaping (fail fast) passive health check
비유 (한계) 입구의 인원 제한 — 단, 사람이 아니라 동시 in-flight 요청 수다 고장난 계산대에 손님 안 보냄 — 단, 능동 점검이 아니라 실패한 손님으로 판정

2-1. connection pool — fail fast가 핵심 가치

connection pool 한도의 목적은 01절에서 본 그대로 느린 실패를 빠른 실패로 바꾸는 것이다. 한도가 없으면 upstream이 느려질 때 요청이 무한정 쌓이고(queue buildup), 클라이언트 쪽 thread·메모리가 고갈되어 연쇄 장애가 된다. 한도를 걸면 "쌓이기 전에 잘라내" 클라이언트가 빠르게 다른 경로(retry budget, fallback, degraded mode)로 전환한다.

핵심 키 3종의 의미 — 각 필드가 무슨 질문에 답하는지로 보면 외우지 않아도 된다:

추가로 http.http2MaxRequests(destination으로의 active request 최대치)와 http.maxRetries(cluster 전체 outstanding retry 상한)도 같은 circuit_breakers.thresholds로 간다. 각 필드가 Envoy cluster의 어느 칸으로 컴파일되는지, 어떤 필드는 circuit_breakers가 아니라 common_http_protocol_options로 가는지의 전체 표는 Envoy Cluster 해부 §5.2에 있다 — 여기서 반복하지 않는다.

2-2. outlier detection — 트래픽 자체가 신호인 passive health check

connection pool은 upstream을 하나의 덩어리로 보고 을 자를 뿐, 그 안의 어느 host가 죽었는지는 모른다. 그 빈자리를 메우는 게 outlier detection이다. 01절의 두 번째 문제 — 일부 host만 죽는 경우 — 가 여기서 해결된다.

결정적 성격: outlier detection은 요청을 실제로 보내본 결과로 host의 건강을 판정하는 passive health check다. 별도 probe를 쏘는 active health check가 아니다 — 흐르는 트래픽 자체가 건강 신호다. 그래서 "트래픽이 없으면 판정도 없다"는 비자명한 함정이 따라온다(05절·What you might be missing).

격리 사이클은 "빼봤다가 → 시간이 지나면 다시 넣어보고 → 또 나쁘면 더 오래 뺀다" 는 backoff 구조다. 이 반복이 host의 자동 회복을 가능하게 한다.

host: 정상in pool1차 ejectbaseEjectionTimepool 복귀probe 트래픽2차 ejectbase x ejection 횟수N차 eject점점 길어짐5xx 임계시간 경과또 임계계속 나쁨정상 회복 → 복귀
그림 2. outlierDetection ejection은 회차마다 baseEjectionTime×횟수로 격리시간이 증가(exponential backoff). 복귀 후 또 실패하면 더 길게 eject, 정상 회복 시 pool 복귀.

평가 주기와 핵심 동작 — 역시 무엇을 답하는 손잡이인지로:

03. 적용·검증 — 한도를 걸고, UO/stat로 발동을 확인한다

3-1. 두 메커니즘을 한 DestinationRule에 — 전체 YAML

회로 차단은 목적지를 가리키는 DestinationRule 하나로 켠다. 아래는 두 방어선을 함께 건 완전한 파일이다(apply 그대로).

apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: llm-server
  namespace: mesh-demo
spec:
  host: llm-server.mesh-demo.svc.cluster.local   # 이 host로 가는 cluster에 정책 부착
  trafficPolicy:
    connectionPool:                  # 방어선 ① 양 자르기 (fail fast)
      tcp:
        maxConnections: 100          # HTTP/1.1 기준 host당 conn; HTTP/2면 사실상 1
        connectTimeout: 300ms        # (참고: circuit_breakers 아님 → cluster.connect_timeout)
      http:
        http1MaxPendingRequests: 1000  # 대기열 상한; 넘으면 즉시 503 UO
        http2MaxRequests: 10000        # HTTP/2 실질 동시성 상한
        maxRetries: 2                  # cluster 전체 동시 retry 상한
    outlierDetection:                # 방어선 ② 나쁜 host 솎기 (passive HC)
      consecutive5xxErrors: 7        # 연속 5xx 7번이면 eject
      consecutiveGatewayErrors: 5    # 502/503/504 연속 5번 (connect fail은 미집계)
      interval: 30s                  # 30초마다 평가
      baseEjectionTime: 5m           # 최초 5분, 재격리마다 배수 증가
      maxEjectionPercent: 50         # 최대 절반까지만 격리
      minHealthPercent: 50           # healthy 50% 미만이면 OD 비활성

정렬 지도 — 같은 magic string이 어디서 어디로 매칭되는지:

DestinationRule spec.host  ==  Envoy cluster 이름의 fqdn 부분
  llm-server.mesh-demo.svc.cluster.local
    └─→ cluster  outbound|80||llm-server.mesh-demo.svc.cluster.local
        (direction|port|subset|fqdn 규칙; subset 비면 빈칸)
connectionPool.*  ==  cluster.circuit_breakers.thresholds (한도 4종) + 일부는 다른 칸
outlierDetection.*  ==  cluster.outlier_detection

3-2. 한도 발동의 관측 — UO와 overflow stat

한도에 걸려 거절된 요청은 장애가 아니라 정책 발동이다. 이걸 식별하는 신호가 두 층위에 있다.

access log response flag 층위에서는 UO(UpstreamOverflow)가 찍힌다. UO의 의미는 "upstream이 죽었다"가 아니라 "Envoy가 정한 동시성/connection/pending/retry 한도를 넘어서 Envoy가 더 보내지 않았다" 이다. 503 UO를 보면 app/DB 상태가 아니라 DestinationRule connectionPool(특히 pending·http2MaxRequests·maxRetries) 부터 의심한다. (flag 전체 의미·UC/UF 등은 Envoy response flag)

Envoy stat 층위에서는 어느 한도에 걸렸는지가 cluster별 counter로 드러난다:

stat 어느 한도 초과인가
cluster.<name>.upstream_cx_overflow maxConnections 초과 (connection 정원)
cluster.<name>.upstream_rq_pending_overflow http1MaxPendingRequests 초과 (대기열 정원)
cluster.<name>.upstream_rq_pending_failure_eject pending 중 endpoint eject로 실패
cluster.<name>.circuit_breakers.<priority>.cx_open (gauge 1) connection circuit breaker가 현재 open 상태
cluster.<name>.circuit_breakers.<priority>.rq_pending_open pending circuit breaker open

확인 명령과 기대 출력:

# sidecar의 cluster별 circuit breaking stat 조회 (admin은 localhost:15000)
istioctl proxy-config bootstrap <pod> -n mesh-demo >/dev/null  # 접근 가능 확인
kubectl exec <pod> -n mesh-demo -c istio-proxy -- \
  pilot-agent request GET 'stats?filter=upstream_(cx|rq_pending)_overflow' \
  | grep llm-server

기대 출력 (한도에 부딪혔다면 0이 아닌 누적값):

cluster.outbound|80||llm-server.mesh-demo.svc.cluster.local.upstream_cx_overflow: 142
cluster.outbound|80||llm-server.mesh-demo.svc.cluster.local.upstream_rq_pending_overflow: 5031

overflow counter가 증가 중이면 한도가 실제로 트래픽을 자르고 있다는 직접 증거다. admin API로 데이터 플레인 실제 stat을 읽는 일반 절차는 Envoy admin API 진단 참조.

04. 두 메커니즘이 만나는 실패 모드 — UO vs UH

운영에서 가장 헷갈리는 지점은 두 메커니즘이 만들어내는 503의 의미가 다르다는 것이다. 02절의 anchor(방향의 차이)가 그대로 두 가지 503으로 나타난다 — 하나는 양을 잘랐을 때, 하나는 host가 다 빠졌을 때.

503 UO (UpstreamOverflow)
  = connectionPool 한도 초과로 Envoy가 거절
  = "보낼 수는 있는데 정원이 찼다"  → connectionPool/retry 폭증을 본다

503 UH (NoHealthyUpstream)
  = LB pool에 healthy endpoint가 하나도 없다
  = outlierDetection이 너무 많이 eject했거나, 실제로 다 죽었거나
  → maxEjectionPercent, 실제 backend 상태를 본다

이 둘이 상호작용해 사고를 키울 수 있다. endpoint가 적은 서비스(예: Pod 2개)에 maxEjectionPercent를 높게 잡으면, 일시적 5xx 한 번에 정상 endpoint까지 빠져 UH가 난다. 그 사이 들어온 요청은 갈 곳이 없어 다시 5xx → 남은 host도 임계 도달 → 더 빠짐, 즉 outlier detection이 스스로 장애를 증폭시킨다. minHealthPercent로 하한 안전장치를 두는 이유다. (maxEjectionPercent ↔ UH 함정의 운영 detail → Envoy Cluster 해부 §5.4)

retry와의 상호작용도 주의해야 한다. upstream이 느릴 때 blanket retry를 켜두면, retry가 connection pool 동시성을 더 밀어붙여 maxRetries/http2MaxRequests 한도에 더 빨리 부딪히고(UO 폭증), upstream이 GPU queue 같은 자원이면 retry가 큐를 더 채워 tail latency를 악화시킨다. 한도와 retry budget은 짝으로 설계해야 한다.

핵심 정리

circuit breaking = connectionPool(과부하 차단) + outlierDetection(나쁜 endpoint 격리)
  → CircuitBreaker CRD는 없다. 둘 다 DestinationRule trafficPolicy → Envoy cluster로 컴파일.
  → 보호 대상: cascading failure(느림→자원고갈→체인 전파)와 일부 host 사망.

방향 anchor:
  connectionPool = 내가 upstream을 얼마나 미는가 (양 자름, fail fast = traffic shaping)
  outlierDetection = upstream의 어느 host가 나쁜가 (host 솎음, passive health check)

connectionPool (fail fast):
  maxConnections          → host당 conn 상한 (HTTP/2면 사실상 1, 실질 상한은 http2MaxRequests)
  http1MaxPendingRequests → 대기열 상한 (넘으면 즉시 503 UO)
  maxRequestsPerConnection→ conn당 request 수 (1이면 keep-alive off)
  maxRetries              → cluster 전체 동시 retry 상한
  초과 신호: access log UO, stat upstream_cx_overflow / upstream_rq_pending_overflow

outlierDetection (passive health check):
  consecutive5xx / consecutiveGatewayErrors(502/503/504만) / consecutiveLocalOriginFailures(connect)
  interval(평가 주기), baseEjectionTime(재격리마다 배수 증가)
  maxEjectionPercent(상한), minHealthPercent(이하면 OD 끔)

503 의미 구분:
  UO = connectionPool 한도 (정책 발동, 장애 아님)
  UH = healthy endpoint 0 (과한 eject 또는 실제 다 죽음)

What you might be missing