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의 죽음 — 가 정확히 이 두 메커니즘에 대응한다.
flowchart LR
DR["DestinationRule<br/>trafficPolicy"]
DR --> CP["connectionPool<br/>(동시성/pending/retry 한도)"]
DR --> OD["outlierDetection<br/>(에러 endpoint 격리)"]
CP -->|컴파일| CB["Envoy cluster<br/>circuit_breakers.thresholds"]
OD -->|컴파일| ODT["Envoy cluster<br/>outlier_detection"]
CB --> OV["한도 초과 → 즉시 503 UO<br/>(빠른 실패 = fail fast)"]
ODT --> EJ["연속 에러 endpoint<br/>→ pool에서 eject"]
여기서 먼저 잡아야 할 구조적 사실: 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종의 의미 — 각 필드가 무슨 질문에 답하는지로 보면 외우지 않아도 된다:
tcp.maxConnections— “host당 TCP connection을 몇 개까지?” destination host당 최대 connection 수. 단, HTTP/2에서는 하나의 connection이 다수 request를 multiplex하므로 사실상 1 connection으로 충분하고, 이때 실질 동시성 상한은maxConnections가 아니라http2MaxRequests다. HTTP/1.1이라야 connection 수 ≈ 동시성이다. (이 함정을 놓치면 “maxConnections를 1로 줄였는데 왜 동시 요청이 안 막히지?“가 된다 — gRPC/HTTP2였던 것.)http.http1MaxPendingRequests— “ready connection이 없을 때 몇 개까지 줄 세울까?” 대기열(pending queue) 상한. 이걸 넘으면 더 기다리지 않고 즉시 거절한다. fail fast의 직접 제어 손잡이.http.maxRequestsPerConnection— “connection 하나를 몇 요청까지 재사용할까?”1이면 keep-alive를 사실상 끄는 것(매 요청마다 새 connection). 한도라기보다 connection 재사용 정책이다.
추가로 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의 자동 회복을 가능하게 한다.
flowchart TD
H["host: 정상<br/>(in pool)"] -->|consecutive5xx 임계 도달| EJ1["1차 eject<br/>baseEjectionTime"]
EJ1 -->|시간 경과| BACK["pool 복귀<br/>(probe 트래픽)"]
BACK -->|또 임계 도달| EJ2["2차 eject<br/>baseEjectionTime x ejection 횟수"]
EJ2 -->|계속 나쁘면| EJN["N차 eject<br/>점점 길어짐"]
EJN -->|정상 회복| H
평가 주기와 핵심 동작 — 역시 무엇을 답하는 손잡이인지로:
interval— “얼마나 자주 평가할까?” 매interval마다 Envoy가 각 host의 에러 누적을 평가한다.consecutive5xxErrors/consecutiveGatewayErrors— “연속 몇 번 실패하면 뺄까?” 둘은 집계 대상이 다르다:consecutiveGatewayErrors는 HTTP 502/503/504 응답만 세고 TCP connect failure는 안 센다. connect 단계 실패로 빼려면consecutiveLocalOriginFailures(+splitExternalLocalOriginErrors: true)가 필요하다. (분류 상세 → Envoy Cluster 해부 §5.4)baseEjectionTime— “최소 얼마나 격리할까?” 재격리 시 배수로 증가한다 — 같은 host가 또 나빠지면 격리 시간이baseEjectionTime × 그 host의 누적 eject 횟수로 늘어난다. 만성적으로 나쁜 host를 점점 오래 빼두는 backoff.maxEjectionPercent— “전체의 몇 %까지 뺄까?” 격리 비율 상한.minHealthPercent— “healthy가 이 밑이면 어떻게?” 이 밑으로 떨어지면 outlier detection을 꺼버린다(panic mode 유사) — 다 빼버리면 갈 곳이 없으므로.
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
UO는 장애가 아니라 정책이 정상 동작한 결과다.503 UO를 보고 app/DB를 디버깅하기 시작하면 시간을 버린다. 먼저upstream_cx_overflow/upstream_rq_pending_overflowstat과 DestinationRuleconnectionPool을 본다. 한도가 너무 빡빡한 것이지 backend가 죽은 게 아닐 수 있다.- HTTP/2·gRPC에서
maxConnections는 동시성 한도가 아니다. multiplexing 때문에 connection 하나가 수천 request를 나르므로,maxConnections를 줄여도 동시성이 안 막힌다. gRPC/HTTP2 cluster의 실질 동시성 손잡이는http2MaxRequests다. 이걸 모르면 한도를 걸어도 효과가 없어 보인다. - outlier detection은 active health check가 아니다. 트래픽이 없으면 판정할 신호도 없다 — 호출이 뜸한 endpoint는 죽어 있어도 eject되지 않고, 트래픽이 들어오는 순간 실패한다. Kubernetes readiness probe(active)와 outlier detection(passive)은 보완 관계지 대체 관계가 아니다.
consecutiveGatewayErrors는 connect failure를 안 센다. backend가 connection refused/timeout으로 실패하는 상황(흔한 장애)에서consecutiveGatewayErrors만 걸어두면 endpoint가 영원히 안 빠진다. connect 단계 격리는consecutiveLocalOriginFailures+splitExternalLocalOriginErrors: true가 필요하다.- endpoint가 적은 서비스에서 outlier detection은 양날의 검이다. Pod 2~3개 서비스에 공격적
maxEjectionPercent를 걸면 일시 에러에 정상 host까지 빠져UH→ 남은 host 과부하 → 추가 eject로 장애를 스스로 키운다.minHealthPercent하한과 보수적maxEjectionPercent가 필수다. - 필드별 Envoy 컴파일 위치는 직관과 다를 수 있다.
connectionPool의 모든 필드가circuit_breakers로 가지 않는다 —connectTimeout·tcpKeepalive·idleTimeout·maxRequestsPerConnection은 cluster의 다른 칸(connect_timeout,common_http_protocol_options등)으로 간다. 한도 4종만circuit_breakers.thresholds다. 전체 매핑 표는 Envoy Cluster 해부에 있다.