homelab89 Docs Logs Legacy Files ☰ TOC 🌓
refistio 2026-07-05istiodestination-ruleconnection-poolcircuit-breakingenvoy

DR connectionPool 정본 — 필드별 동작·매핑·관측

ABSTRACT

connectionPool의 12개 필드는 두 계열이다 — 거부형 4종(maxConnections·http1MaxPendingRequests· http2MaxRequests·maxRetries = Envoy circuit_breakers)은 한도 초과분을 즉시 잘라내고 (503 UO/URX, *_overflow 카운터), 관리형 8종(connect·idle·수명 타임아웃, keepalive, 커넥션당 요청 수, 프로토콜 스위치)은 요청을 거부하지 않고 커넥션을 다듬는다(실패·종료·교체, cx_* 카운터). 계열이 다르면 발동 방식·관측 지표·장애 시그니처가 전부 다르므로, “이 필드를 만졌더니 무엇이 보였다"는 항상 이 두 계열 위에서 읽어야 한다. 이 문서는 필드 하나하나를 의미 → Envoy 매핑 → 기본값 → 발동 동작 → 관측 stat → 함정의 동일한 틀로 답하는 필드 레퍼런스 정본이며, 발동형 6종의 동작과 config 반영 4종, 기본값 미결점 2건을 전부 자기 번들 실측 T93으로 확인했다.

대상 환경: homelab (kubespray bare-metal, k8s v1.30.6, CNI Calico), Istio 1.30.0 대상 독자: DR에 connectionPool을 걸어야 하는데 “이 필드가 실제로 무엇을 일으키는지"를 필드 단위로 확인하고 싶은 사람 다루는 것: ① 이 설정들이 다루는 대상(Envoy upstream 커넥션 풀 모델) ② 필드 → Envoy 반영 위치 지도 ③ tcp.*/http.* 필드별 상세 ④ 스코프·발효 규칙(이중 기본값·portLevelSettings) ⑤ 상호작용(retry 곱·outlier 경계) ⑥ 따라하기 실습(번들 킷 S1S7) ⑦ 함정 검증: 발동 6종(S1S6)·config 반영 4종(S7)·기본값 미결점 2건은 자기 번들 실측 T93(2026-07-05 — 아래 실습과 동일한 킷 files/lab/만으로 cleanup→setup→s1~s7→cleanup 전체 생애주기를 라이브 재현, 원자료 result.txt·verdict.json)으로 확인했다. 필드 의미·기본값의 문헌 주장은 istio.io/Envoy 공식 레퍼런스 대조다 — 문서 끝 “검증 기록"에서 실측/문헌/미검증을 구분해 표기했다.


1. 배경 — 이 설정들은 “누구의 커넥션"을 다루나

connectionPool은 서버를 설정하는 게 아니다. 요청을 보내는 쪽(클라이언트) sidecar Envoy가 그 목적지를 향해 유지하는 upstream 커넥션 풀을 설정한다. 앱이 밖으로 요청을 던지면 그 요청은 자기 pod의 istio-proxy를 지나고, Envoy는 목적지별 outbound cluster에서 커넥션을 꺼내(없으면 새로 열어) 업스트림에 보낸다 — DR의 connectionPool은 istiod가 이 outbound cluster로 컴파일해 넣는다. 서버 쪽 sidecar에는 아무 일도 일어나지 않고, 발동의 모든 관측(stat·액세스 로그)도 클라이언트 프록시에서 한다. T93 실측 전체의 관측 지점이 client istio-proxy였던 이유가 이것이다.

flowchart LR
    DRR["DestinationRule<br/>connectionPool"]
    APP["client app<br/>(fortio load)"]
    SC["client sidecar Envoy<br/>outbound cluster = DR 발효 지점<br/>circuit_breakers · 커넥션 풀"]
    BE["backend pod<br/>server sidecar — connectionPool 무관"]
    DRR -.->|"istiod가 컴파일해 xDS로 주입"| SC
    APP --> SC
    SC ==>|"upstream 커넥션 풀<br/>(worker thread별)"| BE

이 풀의 성격 두 가지가 필드 전체의 존재 이유를 설명한다.

첫째, 프로토콜이 “어느 필드가 무는가"를 정한다. HTTP/1.1은 커넥션 1개가 한 번에 요청 1개만 나르므로 동시성이 곧 커넥션 수다 — 그래서 maxConnectionshttp1MaxPendingRequests(커넥션 확보 대기 큐)가 실질 레버다. HTTP/2는 커넥션 1개에 다수 스트림을 멀티플렉싱하므로 커넥션 수는 요청 수와 무관해지고, http2MaxRequests(동시 요청)와 maxConcurrentStreams(커넥션당 스트림)가 레버가 된다. 같은 필드를 걸어도 업스트림 프로토콜에 따라 발동 여부가 달라지는 이유다.

둘째, 풀과 한도의 스코프가 다르다. Envoy의 물리적 커넥션 풀은 worker thread별 × priority별로 분리돼 있지만, circuit breaker 한도의 집계는 cluster 전역으로 공유된다(thread 수만큼 곱해지지 않는다). 즉 maxConnections: 1은 “스레드당 1개"가 아니라 “이 클러스터 전체에 1개"다. 반대로 커넥션의 재사용·분포 같은 풀 내부 동작은 thread 단위라, 관측할 때 이 스코프 차이를 모르면 수치가 직관과 어긋난다.


2. 설정 지도 — 필드 12개는 Envoy 어디로 가나

connectionPool에는 tcp 5종·http 8종의 필드가 있다. 이름이 같은 idleTimeout이 tcp·http 양쪽에 있어 CRD 경로로는 13개지만, HTTP 클러스터에서 같은 Envoy 필드로 떨어지는 이 쌍을 하나로 세면 개념 필드는 12개다(tcpKeepalive의 하위 3필드는 1종으로 셈). 12개가 Envoy cluster의 세 반영 위치로 갈라지고, 그 위치가 곧 계열(거부형/관리형)과 관측 방법을 정한다:

flowchart LR
    DR["DR trafficPolicy<br/>connectionPool"]
    subgraph CBG["거부형 — 초과를 잘라낸다"]
        CB["Cluster.circuit_breakers<br/>.thresholds"]
    end
    subgraph MG["관리형 — 커넥션을 다듬는다"]
        CL["Cluster 본체 옵션"]
        CH["HttpProtocolOptions<br/>common_http_protocol_options"]
        EH["HttpProtocolOptions<br/>explicit_http_config ·<br/>use_downstream_protocol_config"]
    end
    DR -->|"tcp.maxConnections<br/>http.http1MaxPendingRequests<br/>http.http2MaxRequests<br/>http.maxRetries"| CB
    DR -->|"tcp.connectTimeout<br/>tcp.tcpKeepalive"| CL
    DR -->|"tcp.idleTimeout · http.idleTimeout<br/>tcp.maxConnectionDuration<br/>http.maxRequestsPerConnection"| CH
    DR -->|"http.maxConcurrentStreams<br/>http.h2UpgradePolicy<br/>http.useClientProtocol"| EH
    CB --> R1["발동 = 즉시 거부<br/>503 UO · URX<br/>stat: overflow 카운터"]
    CL --> R2["발동 = 커넥트 실패 · 커널 프로브<br/>503 UF connect_timeout"]
    CH --> R3["발동 = 종료·교체 (graceful)<br/>stat: cx_idle_timeout ·<br/>cx_max_duration_reached · cx_max_requests"]
    EH --> R4["발동 없음 — 프로토콜 스위치<br/>검증은 config_dump 로만"]

istioctl proxy-config cluster <client-pod> --fqdn <svc-fqdn> -o json으로 뜯을 때의 매핑 치트시트 (T93 S7이 이 위치들을 전부 config_dump로 확인했다):

outbound cluster (client sidecar) 안에서:
  circuit_breakers.thresholds[]              <- maxConnections, http1MaxPendingRequests,
                                                http2MaxRequests, maxRetries
  connect_timeout                            <- connectTimeout
  upstream_connection_options.tcp_keepalive  <- tcpKeepalive.{time,interval,probes}
  typed_extension_protocol_options[
    "envoy.extensions.upstreams.http.v3.HttpProtocolOptions"]:
      common_http_protocol_options.idle_timeout                 <- idleTimeout (tcp/http)
      common_http_protocol_options.max_connection_duration      <- maxConnectionDuration
      common_http_protocol_options.max_requests_per_connection  <- maxRequestsPerConnection
      explicit_http_config.http2_protocol_options
        .max_concurrent_streams                                 <- maxConcurrentStreams
      use_downstream_protocol_config                            <- useClientProtocol: true

계열 구분을 한 줄로: 거부형(circuit breaker 4종)은 한도 초과 순간 요청·재시도를 잘라내고 *_overflow 카운터와 circuit_breakers.* 게이지로 보이며, 관리형 8종은 커넥션의 수립·수명·재사용을 다듬고 cx_* 카운터(또는 config_dump)로만 보인다. 장애 시그니처도 갈린다 — 거부형은 클라이언트 프록시가 만드는 503 UO/URX, 관리형 중 유일하게 요청을 죽이는 connectTimeout은 503 UF다 (response flag 정본).


3. tcp.* 필드 상세

tcp 블록(TCPSettings)은 “HTTP·TCP 공통” 설정이다 — L7(HTTP) 클러스터에도, L4(TCP proxy) 클러스터에도 적용된다. 단 maxConnectionDuration·idleTimeout은 클러스터 종류에 따라 Envoy 반영 경로가 달라진다 (각 필드의 함정 참조).

3.1 maxConnections — 커넥션 수 상한 (거부형)

항목 내용
의미 대상 호스트로의 최대 HTTP/1.1·TCP 커넥션 수
Envoy 매핑 Cluster.circuit_breakers.thresholds[].max_connections
기본값 DR 없음 = Envoy 1024 / connectionPool 설정 후 생략 = 4294967295 명시 주입 (§5.1)
관측 stat 카운터 upstream_cx_overflow · 게이지 circuit_breakers.default.cx_open

발동하면 — 한도에 도달한 상태에서의 신규 upstream 커넥션 생성이 거부된다(단 host당 최소 1개는 보장). 중요한 것은 HTTP 경로에서 이 필드 혼자는 요청을 죽이지 않는다는 점이다: 커넥션을 못 연 요청은 pending 큐로 흡수되고, pending 한도가 남아 있는 한 기다렸다가 처리된다. T93 S1이 정확히 이 장면이다 — maxConnections: 1에 fortio 동시 4커넥션·40요청을 걸자 upstream_cx_overflow +41, 게이지 cx_open=1(한도 도달 중), 실제 수립 커넥션은 cx_total +2로 눌렸는데 응답은 40건 전부 200이었다(초과분이 기본 무제한 pending으로 흡수). 503을 보려면 S2처럼 pending 한도까지 함께 조여야 한다.

함정 — ① “maxConnections를 걸면 503이 난다"는 오해(위 실측이 반례). ② H2 업스트림에선 멀티플렉싱 때문에 커넥션 수가 요청 수와 무관해 이 한도가 잘 안 걸린다 — H2의 실질 제어는 http2MaxRequests. ③ 한도 집계는 cluster 전역 공유(+priority별), 물리 풀은 thread별 — “스레드 수 × 한도"로 오해하기 쉽다(§1).

3.2 connectTimeout — 커넥트 타임아웃 (관리형)

항목 내용
의미 TCP 커넥트(handshake 완료까지) 타임아웃, ≥1ms
Envoy 매핑 Cluster.connect_timeout (cluster 본체 옵션 — circuit breaker 아님)
기본값 10s (istio.io)
관측 stat upstream_cx_connect_timeout (동반: upstream_cx_connect_fail)

발동하면 — 시한 내 handshake가 안 끝나면 커넥션 시도가 실패하고, 그 요청은 503에 response flag UF(upstream connection failure)를 받는다. T93 S6: connectTimeout: 1s + SYN이 무응답으로 사라지는 목적지(blackhole SE)에 요청 5건 → upstream_cx_connect_timeout +15(connect_fail 동반 +15), 클라이언트 프록시 액세스 로그에 503 URX,UF upstream_reset_before_response_started{connection_timeout}.

함정 — ① 이 필드는 단독으로 놀지 않는다: Istio 기본 mesh retry(2회)가 connect 실패마다 재시도해 체감 지연 = connectTimeout × 3이 되고 최종 플래그도 URX가 겹친다 — 요청 5건에 connect 시도가 15회였던 이유다(§6.2 상술). ② 재현하려면 “SYN이 실제로 발송되나 응답이 없는” 목적지가 필요하다 — 라우팅이 없는 대역(Class E 240/4)은 즉시 ENETUNREACH로 connect_fail만 오르고 connect_timeout은 안 오른다(함정 모음).

3.3 tcpKeepalive.{time, interval, probes} — 커널 keepalive (관리형 · 정본 위임)

항목 내용
의미 upstream 소켓에 SO_KEEPALIVE + TCP_KEEPIDLE/KEEPINTVL/KEEPCNT 설정
Envoy 매핑 Cluster.upstream_connection_options.tcp_keepalive.{keepalive_time, keepalive_interval, keepalive_probes}
기본값 미설정 시 OS 기본 (Linux: 7200s / 75s / 9)
관측 stat 전용 stat 없음 — config_dump 또는 소켓 ss -o로만 검증

발동형이 아니다 — Envoy는 소켓 옵션을 설정만 하고, 프로브를 보내 죽은 상대를 감지하고 중간장비 세션을 유지하는 주체는 커널이다. 그래서 stat으로는 발동을 재현할 수 없고, T93도 S7 config_dump로 tcp_keepalive: {keepaliveProbes: 3, keepaliveTime: 300, keepaliveInterval: 30} 반영까지만 확인했다. 세 필드 각각의 커널 의미·프로브의 정체·역할 분리(세션 유지 vs 사망 감지)·권장값은 tcpKeepalive 필드 노트가 정본이다.

3.4 maxConnectionDuration — 커넥션 최대 수명 (관리형)

항목 내용
의미 커넥션 최대 수명(수립 시점 기준 경과 시간), ≥1ms
Envoy 매핑 HTTP 클러스터: HttpProtocolOptions.common_http_protocol_options.max_connection_duration
기본값 미설정 = 수명 무제한
관측 stat upstream_cx_max_duration_reached

발동하면 — 수명에 도달한 커넥션을 graceful close한다: 진행 중 요청은 완료시킨 뒤 닫고(H2는 GOAWAY 후 종료) 필요하면 새 커넥션을 수립한다. 거부가 아니라 교체다. T93 S5: 5s 설정 + keepalive 10초 부하 → upstream_cx_max_duration_reached +2(10s 창에서 5s 수명 커넥션 2회 교체), cx_total +2, 요청은 100건 전부 200(무손실). LB 재분배·장수명 커넥션 고착 방지용 필드다.

함정 — 이름은 tcp.*지만 HTTP 클러스터에선 HttpProtocolOptions 경로로 반영된다(T93 S5·S7에서 위치 확인). “TCP 소켓 수명 옵션"이라고 이름만 보고 오해하지 말 것. 순수 TCP(L4) 경로에선 반영 위치가 다르므로 config_dump로 실제 위치를 확인하고 쓰는 게 안전하다.

3.5 idleTimeout (tcp) — 무-트래픽 타임아웃 (관리형)

항목 내용
의미 커넥션에 어느 방향으로든 바이트가 흐르지 않는 시간의 한도
Envoy 매핑 HTTP 클러스터: common_http_protocol_options.idle_timeout / 순수 TCP: tcp_proxy.idle_timeout
기본값 1시간 · 0s = 비활성
관측 stat upstream_cx_idle_timeout

발동하면 — idle 구간이 한도를 넘은 커넥션을 종료한다. 거부가 아니고 놀고 있는 커넥션의 정리다 (발동 stat의 실측 장면은 같은 Envoy 필드로 떨어지는 http.idleTimeout 쪽 T93 S4 참조).

함정http.idleTimeout기준이 다르다: tcp는 “바이트 무이동”, http는 “활성 요청 없음”. HTTP 클러스터에선 둘 다 같은 common_http_protocol_options.idle_timeout으로 떨어지므로, HTTP 트래픽이면 의도가 분명한 http.idleTimeout으로 지정하는 편이 낫고, 이 필드의 고유 영역은 L4(TCP) 클러스터다 — 거기선 http.*가 아예 무효라(§5.3) 이 필드만이 idle 정리 수단이다.


4. http.* 필드 상세

http 블록(HTTPSettings)은 HTTP/1.1·HTTP/2·gRPC 트래픽에만 적용된다. 클러스터가 L4로 다뤄지면(포트 프로토콜이 TCP) 이 블록 전체가 무효다(§5.3).

4.1 http1MaxPendingRequests — 대기 큐 한도 (거부형)

항목 내용
의미 ready 커넥션이 없어 큐에서 대기할 수 있는 최대 요청 수
Envoy 매핑 circuit_breakers.thresholds[].max_pending_requests
기본값 DR 없음 = Envoy 1024 / connectionPool 설정 후 생략 = 4294967295 명시 주입 (§5.1)
관측 stat 카운터 upstream_rq_pending_overflow · 게이지 circuit_breakers.default.rq_pending_open (보조: upstream_rq_pending_total/active)

발동하면 — 대기 큐가 찬 상태의 신규 요청을 즉시 거부한다: 503 + response flag UO(upstream overflow). 기다리게 하지 않는 fail-fast가 이 필드의 본질이다. T93 S2(maxConnections: 1 + http1MaxPendingRequests: 1, fortio -c 3): upstream_rq_pending_overflow +12 = fortio 503 12건과 정확히 일치, 게이지 rq_pending_open=1. 공식 circuit-breaking task가 쓰는 classic 재현 패턴이다.

함정 — 이름의 “http1"에 속지 말 것. pending은 “커넥션 확보 대기"라는 풀 공통 개념이고, H1에서 커넥션당 1요청이라 실질 의미가 클 뿐이다(H2는 멀티플렉싱이라 pending이 순간적). 발동 장면을 보려면 S2처럼 maxConnections와 함께 조여 커넥션 병목을 먼저 만들어야 한다.

4.2 http2MaxRequests — 동시 요청 한도 (거부형)

항목 내용
의미 대상으로의 최대 동시 active 요청 수 — 이름과 달리 H1·H2 모두 적용
Envoy 매핑 circuit_breakers.thresholds[].max_requests
기본값 DR 없음 = Envoy 1024 / connectionPool 설정 후 생략 = 4294967295 명시 주입 (§5.1)
관측 stat 게이지 circuit_breakers.default.rq_open · 카운터는 아래 참조

발동하면 — 동시 active 요청이 한도를 넘는 순간 초과 요청을 거부한다(503/UO). 발동 자체는 T93 범위 밖이다(H2 업스트림 오버플로 시나리오 미수행) — config 주입값 반영만 S7에서 확인했고, 발동 주장은 문헌 기반임을 명시한다. 카운터 집계도 미세하게 열려 있다: Envoy stats 레퍼런스는 upstream_rq_pending_overflow가 “connection pool 또는 requests circuit breaking"을 합산 집계한다고 명시하는데, T93은 upstream_rq_active_overflow 카운터가 실존함(값 0으로 노출)도 관측했다 — http2MaxRequests 발동이 어느 카운터로 집계되는지는 미검증으로 남긴다(검증 기록 C14).

함정 — 이름은 “http2"지만 H1에도 적용된다. H2 시나리오에선 커넥션 수 대신 이 필드가 실질 병렬 상한이다(§1 프로토콜 논의).

4.3 maxRequestsPerConnection — 커넥션당 요청 수 (관리형)

항목 내용
의미 커넥션 1개가 처리할 최대 요청 수. 1 = keepalive 비활성
Envoy 매핑 common_http_protocol_options.max_requests_per_connection (구 Cluster.max_requests_per_connection은 deprecated)
기본값 0 = 무제한 (최대 2^29)
관측 stat upstream_cx_max_requests (한도로 닫힌 커넥션 수)

발동하면 — 한도에 도달한 커넥션을 풀이 drain하고 새 커넥션을 만든다(H2는 N 스트림 후 GOAWAY). 거부가 아니라 재사용 차단 = 커넥션 turnover다. T93 S3(=1, fortio -c 1 -n 20 -keepalive): upstream_cx_max_requests +20, cx_total +20(요청 수만큼 신규 커넥션), 응답 20건 전부 200.

함정 — turnover는 다운스트림(앱)에선 안 보인다. T93 S3에서 fortio는 Sockets used: 1로 커넥션 1개를 재사용했다고 보고했다 — turnover는 sidecar→backend 구간에서만 일어나고 앱↔sidecar 커넥션은 그대로이기 때문이다. overflow 카운터가 아니라 cx_total 증가로 나타나는 것도 같은 이유다. 관측은 반드시 프록시 stat으로.

4.4 maxRetries — 동시 재시도 총량 (거부형)

항목 내용
의미 클러스터 전체에서 동시에 미해결(outstanding) 상태일 수 있는 재시도의 총량
Envoy 매핑 circuit_breakers.thresholds[].max_retries
기본값 DR 없음 = Envoy 3 / connectionPool 설정 후 생략 = 4294967295 명시 주입 (§5.1)
관측 stat 카운터 upstream_rq_retry_overflow · 게이지 circuit_breakers.default.rq_retry_open (보조: upstream_rq_retry)

발동하면 — 한도를 넘는 재시도는 수행되지 않고 원 실패가 그대로 반환된다(response flag URX). 재시도 폭풍(retry storm)이 업스트림을 두 번 죽이는 것을 막는 예산(budget) 성격의 한도다. 발동 실측은 T93 범위 밖(동시 재시도 폭증 유도가 필요해 난이도 높음) — S7에서 thresholds.max_retries: 2 반영만 확인했다.

함정 — ① VirtualService retries와 직교다(§6.1): retry 정책이 켜져 있어야 이 한도가 의미를 가진다. ② T93 S6에서 본 URX는 이 circuit breaker의 overflow가 아니라 retry 정책의 retry-limit-exceeded다 — 같은 플래그, 다른 메커니즘이니 upstream_rq_retry_overflow 카운터로 판별한다. ③ 이중 기본값의 실무 함정이 가장 아픈 필드다: DR이 없을 땐 3으로 절제되던 동시 재시도가 connectionPool을 거는 순간 무제한이 된다(§5.1).

4.5 idleTimeout (http) — 활성 요청 없는 커넥션의 수명 (관리형)

항목 내용
의미 활성 요청(스트림)이 하나도 없는 상태로 풀에 남은 커넥션의 유지 시간 한도
Envoy 매핑 common_http_protocol_options.idle_timeout
기본값 1시간
관측 stat upstream_cx_idle_timeout

발동하면 — idle 한도를 넘은 풀 커넥션을 종료한다. T93 S4: 2s 설정, keepalive 요청 1회 후 5초 대기 → upstream_cx_idle_timeout +1. 관리형답게 어떤 요청도 실패하지 않는다 — 놀던 커넥션이 사라질 뿐이고, 다음 요청은 새 커넥션을 연다.

함정 — 기준이 “활성 요청 없음"이라 롱폴링·스트리밍처럼 요청이 살아 있는 동안엔 안 걸린다(그건 maxConnectionDuration의 영역). 중간장비(FW/NAT) idle drop 대응으로 쓸 때의 값 설계(keepalive로 세션을 유지할지, idleTimeout으로 선점 정리할지)는 처방전 P4를 따른다(§6.4).

4.6 maxConcurrentStreams — H2 커넥션당 스트림 한도 (관리형)

항목 내용
의미 HTTP/2 커넥션 1개당 최대 동시 스트림 수
Envoy 매핑 explicit_http_config.http2_protocol_options.max_concurrent_streams
기본값 istio.io 표기 “2^31-1” — 실측은 미설정 시 아무 값도 주입 안 함 → 실효 기본 = Envoy proto 기본 1024 (T93 S7c, §5.1)
관측 stat 전용 overflow 없음upstream_cx_total 증가로 간접 관측

발동하면 — 커넥션의 스트림이 상한에 닿으면 풀이 새 커넥션을 만들거나 큐잉한다. 거부가 아니라 “커넥션을 더 편다"이므로 overflow 카운터가 없고 cx_total로 간접 관측한다. 발동 실측은 T93 범위 밖 — 설정값(64) 주입은 S7-full에서 확인했다.

함정 — ① H2 전용이다(H1·L4 경로에선 무의미) — h2UpgradePolicy: UPGRADE이거나 업스트림이 H2로 선언돼 있어야 의미가 있다. ② istio.io의 “기본값 2^31-1"은 config_dump에 나타나는 값이 아니다: T93 S7c에서 UPGRADE만 걸고 이 필드를 생략하자 http2_protocol_options = {}(빈 객체)였다 — Istio는 아무 값도 안 쓰고, 런타임 실효값은 Envoy proto 기본 1024가 된다.

4.7 h2UpgradePolicy — H1→H2 승격 스위치 (관리형 · 프로토콜 스위치)

항목 내용
의미 sidecar가 백엔드로의 HTTP/1.1 트래픽을 HTTP/2로 승격할지 — DEFAULT / DO_NOT_UPGRADE / UPGRADE
Envoy 매핑 UPGRADEexplicit_http_config.http2_protocol_options 부여
기본값 DEFAULT = meshConfig의 h2UpgradePolicy 상속 (메시 전역 기본 DO_NOT_UPGRADE)
관측 stat 없음 — config_dump로만 검증

발동형이 아니라 업스트림 프로토콜을 정하는 스위치다. T93 S7-full·S7c에서 UPGRADEhttp2_protocol_options가 클러스터에 나타나는 것을 확인했다.

함정useClientProtocol: true가 이 필드를 무력화한다. T93 S7b: useClientProtocol을 걸자 use_downstream_protocol_config가 생기고 explicit_http_config는 ABSENT — 승격 설정이 사라졌다.

4.8 useClientProtocol — 다운스트림 프로토콜 보존 (관리형 · 프로토콜 스위치)

항목 내용
의미 클라이언트(다운스트림)가 쓴 프로토콜을 업스트림 연결에 그대로 사용
Envoy 매핑 use_downstream_protocol_config
기본값 false
관측 stat 없음 — config_dump로만 검증

역시 스위치다. T93 S7b: use_downstream_protocol_config = {"httpProtocolOptions": {}, "http2ProtocolOptions": {}} 존재 + explicit_http_config ABSENT를 확인했다.

함정h2UpgradePolicy와 조합하면 useClientProtocol이 이긴다(위 S7b가 그 증거). “승격을 걸었는데 config에 안 보인다"면 이 필드부터 의심한다.


5. 스코프·발효 규칙 — 어디에, 언제, 어떤 기본값으로

5.1 이중 기본값 — “DR을 걸면 한도가 풀린다”

circuit breaker 4종의 “기본값"은 하나가 아니다. 어느 상태의 기본값인지가 관건이다:

상태 maxConnections http1MaxPendingRequests http2MaxRequests maxRetries
DR/connectionPool 자체가 없음 1024 1024 1024 3
connectionPool을 걸고 해당 필드 생략 2^32-1 2^32-1 2^32-1 2^32-1

istio.io 레퍼런스의 “default 2^32-1"은 아래 줄(생략 시 Istio가 주입하는 값)의 의미고, DR을 아예 안 걸면 Envoy native 기본(위 줄)이 산다. T93 S7이 이걸 실측으로 확정했다: connectionPool을 걸고 CB 필드를 생략하자 thresholds에 4294967295(=2^32-1)가 명시 주입됐고(S7-full은 3종+maxRetries=2, S7c는 4종 전부), 이는 추정이 아니라 config_dump에 찍힌 값이다.

부분 설정의 역설

connectionPool에 아무 필드든 하나라도 걸면 생략된 CB 4종 전부가 무제한으로 풀린다. T93 S7c가 극단 사례다 — http.h2UpgradePolicy 하나만 걸었는데 4종 전부 4294967295가 주입됐다. 실무 함의: idleTimeout 하나 넣자고 connectionPool을 만드는 순간, 1024 커넥션 안전망과 동시 재시도 3회 절제가 조용히 사라진다. connectionPool을 부분 도입할 때는 CB 4종을 의식적으로 함께 정할 것.

미설정 필드가 config에 나타나는 방식도 필드마다 다르다 — CB 4종은 “무제한을 명시 주입”, maxConcurrentStreams는 “아예 미주입(빈 객체, 실효는 Envoy 기본 1024)"(T93 §5-1 실측). 즉 istio.io의 “default” 표기를 config_dump에서 그대로 찾으려 하면 안 된다.

5.2 portLevelSettings는 대체다 — 병합이 아니다

trafficPolicy.portLevelSettings로 특정 포트를 오버라이드하면 그 포트는 destination 레벨 설정을 상속하지 않는다. istio.io 원문:

“Traffic settings specified at the destination-level will not be inherited when overridden by port-level traffic policies, i.e. default values will be applied to fields omitted in port-level traffic policies.”

포트 하나에 tls만 바꾸려고 portLevelSettings를 걸면, 그 포트의 connectionPool은 destination 레벨 값이 아니라 기본값(= §5.1의 규칙)으로 돌아간다. destination 레벨에서 조인 한도가 포트 오버라이드 순간 풀리는 사고 패턴이다 — 포트 오버라이드에는 필요한 필드를 전부 다시 쓴다.

5.3 L4 경로에선 http.*가 무의미하다

connectionPool의 발효 모양은 클러스터가 L7(HTTP)로 관리되느냐 L4(TCP proxy)로 관리되느냐에 갈리고, 그건 Service/SE 포트의 프로토콜 선택(name/appProtocol)이 정한다. 포트 name이 http면 HTTP 커넥션 풀·http.* 필드·pending/requests circuit breaker까지 전부 발효되지만, tcp(또는 TLS passthrough처럼 L4로 떨어지는 경로)면 http.* 블록 전체가 무효tcp.*만 남는다. 번들 킷의 backend Service 포트 name이 http인 것, egress의 TLS Passthrough 경로에서 http.retries류가 침묵하는 것이 모두 이 규칙이다. tcp.idleTimeout(§3.5)·maxConnectionDuration(§3.4)의 반영 경로가 클러스터 종류에 따라 달라지는 것도 같은 뿌리다.

5.4 2-hop(mTLS Passthrough)에선 홉마다 발효 프록시가 다르다

egress gateway를 경유하는 2-hop 경로에선 “그 홉의 연결을 여는 프록시"에 그 홉의 DR이 발효된다 — 홉 1(sidecar→gateway) 값은 호출자 sidecar에, 홉 2(gateway→외부) 값은 gateway에. 어느 DR을 어디에 두고 어떤 값이 어느 Envoy에 반영되는지는 mTLS Passthrough — 홉별 DR·connectionPool 구성이 정본이다.


6. 상호작용 — 필드는 혼자 놀지 않는다

6.1 maxRetries(CB) ⊥ VirtualService retries(정책)

둘은 직교하는 다른 축이다. VS retriesper-request 재시도 정책이다 — 몇 번, 어떤 조건에서 (retryOn), per-try timeout은 얼마로. maxRetries클러스터 전역 동시 재시도 총량의 circuit breaker다 — 지금 이 순간 미해결 재시도가 몇 개까지 허용되나. 재시도를 “하게 만드는” 건 정책(VS 또는 mesh 기본)이고, maxRetries는 그렇게 발생한 재시도가 폭증할 때 총량을 자르는 안전판이다. 정책이 없으면 한도는 잴 대상이 없고, 정책만 있고 한도가 무제한이면(§5.1의 역설) retry storm을 막을 수 없다.

6.2 connectTimeout × 기본 retry = 체감 지연 3배 (T93 S6 발견)

Istio는 VS를 안 걸어도 **기본 HTTP retry 정책(2회, retryOn에 connect-failure 포함)**을 적용한다. 그래서 connect 실패는 자동으로 2회 더 시도된다. T93 S6의 숫자가 이 곱을 그대로 보여준다:

요청 5건 → upstream_cx_connect_timeout +15   (= 요청당 connect 시도 3회: 원 시도 + 재시도 2)
fortio avg 3033ms                             (≈ 3 × connectTimeout(1s))
최종 response flag: URX,UF                    (재시도 한도 소진 + 커넥션 실패)

실무 함의 두 가지. ① connectTimeout을 줄여도 사용자 체감 최악 지연은 connectTimeout × (1 + 재시도 수)다 — “1초로 줄였으니 1초 안에 실패한다"가 아니다. ② 무응답 endpoint 하나가 SYN 부하를 3배로 증폭시킨다 — blackhole 상황에서 연결 시도 폭이 정책 곱만큼 커진다. connectTimeout 값을 설계할 때는 항상 유효 retry 정책과 함께 계산한다.

6.3 outlierDetection과의 경계

connectionPool은 “내가 업스트림을 얼마나 밀어붙이는가”(나가는 양)를 자르고, outlierDetection은 “업스트림의 어느 host가 나쁜가”(받는 쪽 host)를 솎아낸다. 전자는 traffic shaping, 후자는 passive health check — 한도를 걸었는데 endpoint가 왜 안 빠지는지, eject됐는데 왜 503/UO가 나는지 같은 혼선은 전부 이 경계를 흐릴 때 생긴다. 두 방어선의 개념·outlierDetection 필드는 회로 차단 메커니즘이 정본이다.

6.4 keepalive·idleTimeout과 중간장비 idle

경로 중간의 FW/NAT가 idle 세션을 조용히 버리면 “유휴 후 첫 요청 실패"가 된다. 처방은 이 문서의 두 필드 조합이다 — tcpKeepalive.time을 중간장비 idle보다 짧게 잡아 세션을 유지하거나, idleTimeout을 그보다 짧게 잡아 죽을 커넥션을 선점 정리한다. 값 설계와 재현·판별 절차는 처방전 P4tcpKeepalive 필드 노트로.


따라하기 실습 — 번들 킷으로 발동을 직접 본다

§3~§4가 “필드가 무엇인가"라면, 이 절은 “받아서 발동을 직접 보기"다. 아래 기대 출력은 전부 2026-07-05에 이 킷만으로 cleanup→setup→s1~s7→cleanup 전체 생애주기를 라이브 재현한 T93 실측 (result.txt)에서 발췌했다 — 독자 클러스터에서는 pod 이름·cluster domain·카운터 절대값만 다르고 델타의 형태는 같아야 한다.

소요 시간: setup 12분(이미지 pull 제외) · 시나리오당 30초1분 · cleanup 약 1분 — S1~S7 전부 돌아도 15분 이내.

실습 0. 사전 조건

  • 도구: kubectl(대상 클러스터 컨텍스트 설정 완료), istioctl(S7 config_dump), jq(S7 파싱), bash
  • Istio가 설치돼 있고 sidecar injection이 동작해야 한다. T93 기준 Istio 1.30.0(client/CP/DP 일치):
istioctl version                    # client/CP/DP 버전 일치 확인
kubectl -n istio-system get pods    # istiod Running 확인
  • 어느 클러스터에서든 동작한다 — 스크립트가 현재 kubectl context를 자동 감지하고(다른 컨텍스트는 CTX=<context>를 앞에 붙임), S7이 cluster domain을 coredns Corefile에서 자동 발견한다(T93에선 cluster.local이 아니라 homelab.local이었다).
  • 랩은 전용 ns conn-lab만 만들고 지운다 — 공유 인프라(egress gateway 등)는 건드리지 않는다.

실습 1. 랩 킷 받기

킷은 이 문서 번들의 개별 파일이다 (주의: 디렉토리 링크는 404 — 아래처럼 파일 단위로만 받는다):

파일 역할
00-namespace.yaml ns conn-lab (istio-injection=enabled) — 발동 관측을 오염 없이 격리
10-backend.yaml backend — fortio server. ?delay=로 커넥션 점유 제어, Service 포트 name http(L7 필수, §5.3)
20-client.yaml client — 관측 지점. proxyStatsMatcher 계측 스위치 + DNS capture/auto-allocate(S6 필수)
30-blackhole.yaml ServiceEntry — SYN이 무응답으로 사라지는 목적지(TEST-NET-2), S6 전용
setup.sh · run-scenario.sh · cleanup.sh 부트스트랩 · 시나리오 하네스(run 내 델타 판정·리포트 생성) · 철거 — 전부 멱등
s1-maxconnections.yaml · s2-maxpending.yaml · s3-maxrequestsperconn.yaml · s4-http-idletimeout.yaml · s5-maxconnduration.yaml · s6-connecttimeout.yaml · s7-full.yaml · s7b-useclientprotocol.yaml · s7c-h2-nomcs.yaml dr-scenarios/ — 시나리오별 DR(실험 변수). 하네스가 한 번에 하나만 남기고 apply

한 줄 다운로드:

mkdir -p dr-connpool-lab/dr-scenarios && cd dr-connpool-lab
base=https://blog.homelab89.com/docs/istio/egress/dr-connection-settings/files/lab
for f in 00-namespace.yaml 10-backend.yaml 20-client.yaml 30-blackhole.yaml \
         setup.sh run-scenario.sh cleanup.sh; do curl -fsSLO "$base/$f"; done
for f in s1-maxconnections.yaml s2-maxpending.yaml s3-maxrequestsperconn.yaml \
         s4-http-idletimeout.yaml s5-maxconnduration.yaml s6-connecttimeout.yaml \
         s7-full.yaml s7b-useclientprotocol.yaml s7c-h2-nomcs.yaml; do
  curl -fsSL -o "dr-scenarios/$f" "$base/dr-scenarios/$f"
done
chmod +x setup.sh run-scenario.sh cleanup.sh

실습 2. setup.sh — 랩 부트스트랩

bash setup.sh          # 다른 클러스터면: CTX=<context> bash setup.sh

기대 출력 골격(T93 [1] — pod 이름·노드는 클러스터마다 다름):

== [0] preflight (context=homelab) ==
== [1] namespace ==
namespace/conn-lab created
== [2] backend (fortio server, sidecar 주입) ==
== [3] client (fortio server + proxyStatsMatcher) ==
== [4] blackhole ServiceEntry (S6 connectTimeout 용) ==
== [5] rollout 대기 ==
deployment "backend" successfully rolled out
deployment "client" successfully rolled out
== ready ==
kubectl -n conn-lab get pods
# backend-…   2/2   Running     <- 2/2 = sidecar 주입 확인
# client-…    2/2   Running

읽는 법 — setup은 DR을 하나도 걸지 않는다(시나리오별 DR은 run-scenario.sh의 몫). 핵심은 client pod의 두 annotation이다: ① proxyStatsMatcher inclusionRegexps(.*backend.* / .*blackhole.* / .*circuit_breakers.*) — 기본 stats matcher가 억제하는 발동 카운터·게이지를 노출하는 계측 스위치 (없으면 발동을 “안 함"으로 오판, 함정 모음 참조) ② DNS capture + auto-allocate — S6의 STATIC SE host 해석용.

실습 3. S1 — maxConnections: 1, 커넥션 서킷브레이커

bash run-scenario.sh s1     # 리포트: reports/<timestamp>_s1.md

하네스가 하는 일: S1 DR apply → before 스냅샷 → fortio -c 4 -qps 0 -n 40(?delay=200ms로 커넥션 점유) → mid-load 게이지 → after 스냅샷 → 델타. 기대 출력 핵심(T93):

mid-load 게이지:  circuit_breakers.default.cx_open = 1   (한도 도달 중)
델타:
  upstream_cx_overflow            0 -> 41   = +41   ★ 커넥션 서킷브레이커 발동
  upstream_cx_total               0 -> 2    = +2    (실제 수립 커넥션은 최소치)
  upstream_rq_total               0 -> 40   = +40
fortio: Sockets 4 · Code 200: 40 (100%)

읽는 법 — overflow가 41번 발동했는데 503이 한 건도 없다. 초과분이 (기본 무제한) pending으로 흡수됐기 때문이다(§3.1). cx_open=1 게이지는 부하 중에만 1이다 — 부하가 끝나면 0으로 돌아가므로 mid-load 스냅샷이 필요하다.

실습 4. S2 — + http1MaxPendingRequests: 1, 즉시 거부(503/UO)

bash run-scenario.sh s2

기대 출력 핵심(T93 — 직전 run의 잔재로 절대값이 41에서 시작한다. 델타만 본다):

mid-load 게이지:  circuit_breakers.default.rq_pending_open = 1
델타:
  upstream_rq_pending_overflow    0 -> 12   = +12   ★ pending 서킷브레이커 발동(UO)
  upstream_cx_overflow           41 -> 57   = +16   (maxConnections=1 도 여전히 발동)
  upstream_rq_total              40 -> 58   = +18
fortio: Sockets 14 · Code 200: 18 (60%) · Code 503: 12 (40%)

읽는 법 — 503 개수(12) = rq_pending_overflow 델타(12) 정확 일치. 거부형의 서명이다: 초과 요청은 기다리지 않고 그 자리에서 잘린다. S1과의 대조가 이 랩의 핵심 한 컷이다 — 같은 커넥션 병목인데 pending 한도의 유무가 “전부 200"과 “40% 503"을 가른다.

실습 5. S3 — maxRequestsPerConnection: 1, 거부 없는 turnover

bash run-scenario.sh s3

기대 출력 핵심(T93):

델타:
  upstream_cx_max_requests        0 -> 20   = +20   ★ 한도로 닫힌 커넥션 수
  upstream_cx_total               3 -> 23   = +20   (요청 수만큼 신규 커넥션)
  upstream_rq_total              58 -> 78   = +20
fortio: Sockets used 1 · Code 200: 20 (100%)

읽는 법 — fortio(다운스트림)는 Sockets used: 1로 커넥션 1개를 재사용했다고 보고하지만, upstream에선 요청마다 커넥션이 교체됐다(+20/+20). turnover는 sidecar→backend 구간에서만 일어나 앱에는 안 보인다(§4.3 함정) — 이 어긋남을 눈으로 확인하는 지점이다.

실습 6. S4 — http.idleTimeout: 2s, idle 종료

bash run-scenario.sh s4     # 요청 1회 → 5초 대기 → 스냅샷

기대 출력 핵심(T93):

델타:
  upstream_cx_idle_timeout        0 -> 1    = +1    ★ idle 2s 초과로 풀 커넥션 종료
  upstream_cx_total              23 -> 24   = +1

실습 7. S5 — maxConnectionDuration: 5s, graceful 교체

bash run-scenario.sh s5     # keepalive 10초 부하 → 5초 시점 수명 도달

기대 출력 핵심(T93):

델타:
  upstream_cx_max_duration_reached  0 -> 2  = +2    ★ 5s 수명 도달 → graceful close·재수립
  upstream_cx_total                24 -> 26 = +2
fortio: Sockets 1 · Code 200: 100 (100%)

읽는 법 — 10초 창에서 5초 수명 커넥션이 2회 교체됐는데 요청은 무손실(200 100%)이다. 진행 중 요청을 완료시킨 뒤 닫는 graceful close의 증거다(§3.4).

실습 8. S6 — connectTimeout: 1s, UF와 retry 곱

bash run-scenario.sh s6     # blackhole.lab.internal (SYN 무응답) 으로 5요청

기대 출력 핵심(T93 — 관측 대상이 backend가 아니라 blackhole 클러스터로 바뀐다):

델타(blackhole 클러스터):
  upstream_cx_connect_timeout     0 -> 15   = +15   ★ connect 타임아웃 발동
  upstream_cx_connect_fail        0 -> 15   = +15
  upstream_cx_total               0 -> 15   = +15
fortio: Code 503: 5 (100%) · avg 3033ms
액세스 로그: "GET / HTTP/1.1" 503 URX,UF upstream_reset_before_response_started{connection_timeout}
  … "blackhole.lab.internal" "198.51.100.10:80" outbound|80||blackhole.lab.internal - 240.240.0.12:80 …

읽는 법 — 요청 5건에 connect 시도 15회 = 기본 retry 정책(2회)과의 곱(§6.2), avg 3033ms ≈ 3 × connectTimeout. 로그의 240.240.0.12는 sidecar DNS proxy가 auto-allocate한 VIP고 실제 endpoint는 198.51.100.10(TEST-NET-2)이다 — 이 배관이 왜 필요한지는 함정 모음.

실습 9. S7 — config 반영 검증 (발동 아님)

bash run-scenario.sh s7     # s7-full → s7c → s7b 순서로 apply + istioctl pc cluster 덤프

기대 출력 핵심(T93):

S7-full (tcpKeepalive + maxRetries:2 + h2UpgradePolicy:UPGRADE + maxConcurrentStreams:64):
  ① tcp_keepalive: {"keepaliveProbes":3,"keepaliveTime":300,"keepaliveInterval":30}
  ② thresholds: [{"maxConnections":4294967295,"maxPendingRequests":4294967295,
                  "maxRequests":4294967295,"maxRetries":2}]
  ③④ http2_protocol_options: {"maxConcurrentStreams":64}

S7c (h2UpgradePolicy:UPGRADE 만 — maxConcurrentStreams 미설정):
  http2_protocol_options = {}          <- Istio 가 아무 값도 안 씀 (실효 = Envoy 기본 1024)
  thresholds = 4종 전부 4294967295     <- CB 필드 생략 시 명시 주입 (§5.1)

S7b (useClientProtocol:true):
  use_downstream_protocol_config 존재 · explicit_http_config = ABSENT  <- h2 승격 무력화

읽는 법 — 발동이 아니라 반영 위치·주입값 검증이다. §5.1의 이중 기본값 함정(생략 CB 필드 = 4294967295 명시 주입)과 §4.6(미설정 maxConcurrentStreams = 미주입), §4.8(useClientProtocol 우선)을 config_dump 원문으로 확인하는 단계다.

실습 10. cleanup.sh — 철거

bash cleanup.sh
kubectl get ns conn-lab
# Error from server (NotFound): namespaces "conn-lab" not found

멱등이다 — 이미 없는 상태에서 재실행해도 --ignore-not-found로 에러 없이 통과하고, 랩이 살아 있는 중간에 실행해도 정상 동작한다(T93은 mid-lab 철거로 시작했다).


함정 모음 — T93이 실제로 밟은 것

  • proxyStatsMatcher 없으면 발동 카운터가 아예 안 보인다. Istio 기본 stats matcher는 cardinality 절감을 위해 cluster 단위 upstream_cx_*/rq_*circuit_breakers.* 게이지를 억제한다. 이 상태로 발동을 관측하면 “발동 안 함"으로 오판한다 — 킷의 client pod annotation (proxyStatsMatcher.inclusionRegexps 3종)이 이 계측 스위치다.
  • stats filter는 부분 문자열 매칭이다. stats?filter=backend는 타 ns의 backend.service-a 클러스터까지 매칭해 엉뚱한 값을 읽게 만든다(T93에서 실제 발생) — filter=backend.conn-lab처럼 ns까지 포함해 스코프한다.
  • stat 이름에 세미콜론 구분자가 있다. 노출 형식이 cluster.<name>;.<metric>: value라 단순 grep <metric> 파싱이 어긋날 수 있다 — 하네스는 '.<metric>: ' 패턴으로 값을 추출한다.
  • cx/rq 카운터는 프록시 기동 이래 누적이다. 판정은 반드시 run 내 before→after 델타로 격리한다(실습 4의 절대값 이월이 그 예). 파드 재시작 후엔 미사용 카운터가 lazy하게 사라졌다 재등장한다 — overflow stat이 트래픽 전엔 안 보일 수 있다.
  • STATIC SE는 sidecar DNS capture가 없으면 시작도 못 한다. fortio는 커넥트 전에 DNS 조회를 하는데, blackhole.lab.internal(resolution: STATIC)은 클러스터 DNS에 없어 NXDOMAIN으로 abort된다 — 트래픽이 Envoy에 닿기도 전에 죽으므로 connectTimeout이 발동할 기회가 없다. 킷은 ISTIO_META_DNS_CAPTURE + AUTO_ALLOCATE로 sidecar DNS proxy가 VIP(240.240.0.0/16 대역, T93에선 240.240.0.12)를 돌려주게 해서 이 조회를 통과시킨다.
  • blackhole은 “라우팅이 있는 무응답 대역"이어야 한다. 킷의 endpoint 198.51.100.10(RFC 5737 TEST-NET-2)은 default route로 SYN이 실제 발송되지만 응답이 없다 → connectTimeout이 먼저 만료된다. Class E(240/4)를 쓰면 커널에 route 자체가 없어 즉시 ENETUNREACH → connect_fail만 오르고 connect_timeout은 안 잡힌다.
  • S1/S2는 ?delay=가 없으면 재현이 안 된다. 응답이 즉시 오면 커넥션이 곧바로 반납돼 동시성이 안 쌓이고 한도가 안 걸린다 — delay 200ms로 커넥션을 붙잡아 둬야 overflow 장면이 나온다.
  • backend Service 포트 name이 http여야 한다. tcp로 두면 클러스터가 L4로 떨어져 http.* 필드·pending/requests CB가 전부 무효가 된다(§5.3).

역할분담 — 이 문서가 답하는 질문, 넘기는 질문

문서 정본으로 답하는 질문 이 문서와의 관계
이 문서 connectionPool 필드 하나하나가 무엇이고 실제로 무엇을 일으키나 필드 레퍼런스 정본 + sidecar 관점 발동 재현 랩(T93)
회로 차단 메커니즘 왜 회로 차단이 필요한가 / outlierDetection 필드는 무슨 뜻인가 개념·outlier 정본 — 이 문서는 connectionPool 필드 차원만 다룸
tcpKeepalive 필드 노트 keepalive 3필드의 커널 매핑·권장값 §3.3이 위임하는 keepalive 정본
TCP 병목 정본 · 처방전 이 필드들에 어떤 값을 줘야 하나 (OS 병목·운영값) 값 처방 — 이 문서는 동작 정의
TCP 병목 재현 랩 egress gateway 관점에서 병목이 어떻게 터지나 상보 랩 — 이 문서 실습은 sidecar 관점 필드 재현
mTLS Passthrough connectionPool 2-hop에서 어느 DR 값이 어느 프록시에 발효되나 §5.4가 위임
DestinationRule 만들기 DR 리소스 자체를 어떻게 구성하나 입문 — 이 문서는 그중 connectionPool만 깊게

핵심 정리

  • 필드는 두 계열이다. 거부형 4종(circuit breaker)은 초과를 즉시 잘라 *_overflow·503 UO/URX로 보이고, 관리형 8종은 커넥션을 다듬어 cx_* 카운터(또는 config_dump)로만 보인다. 계열이 관측법과 장애 시그니처를 정한다.
  • 기본값은 이중이다. DR이 없으면 Envoy native(1024/1024/1024/3), connectionPool을 하나라도 걸면 생략된 CB 필드에 2^32-1이 명시 주입된다(T93 확정) — “DR을 걸면 한도가 풀린다"는 부분 설정의 역설을 기억할 것.
  • 필드는 혼자 놀지 않는다. connectTimeout은 기본 retry와 곱해져 체감 지연 3배가 되고(T93 S6), maxRetries는 retry 정책이 있어야 의미가 있으며, maxConnections 단독은 503을 만들지 않는다(pending 흡수).

What you might be missing

  • 이 문서의 실습은 sidecar-direct L7 경로다. egress gateway 경유(2-hop)면 발효 프록시가 홉마다 갈리고(mtls-passthrough-connectionpool), TLS Passthrough처럼 L4로 떨어지는 경로면 http.* 전체가 무효다(§5.3). “필드가 안 듣는다"의 태반은 경로가 이 랩과 다른 경우다.
  • 거부형의 503은 “우리 편"이 만든 503이다. 서버는 그 요청을 본 적이 없다 — 클라이언트 프록시가 한도에서 잘랐기 때문이다. 서버 로그에 없는 503이 클라이언트에 있으면 response flag(UO/URX)와 overflow 카운터부터 본다. 반대로 이 사실은 부하 격리가 잘 동작한다는 신호이기도 하다.
  • 한도 집계와 물리 풀의 스코프가 다르다. circuit breaker 한도는 cluster 전역 공유지만 물리 풀은 worker thread별이다(§1) — concurrency가 큰 게이트웨이 Envoy에서 커넥션 분포·재사용 수치가 “한도 직관"과 어긋나 보이는 이유가 대개 이것이다.

참조

공식 문서

아카이브 내부

번들 산출물


검증 기록 (2026-07-05 · Istio 1.30.0 / k8s 1.30.6)

검증 방법 요약: istio.io/Envoy 공식 레퍼런스 대조 + homelab 클러스터 실측. 실측은 번들 랩 킷 (files/lab/)만으로 cleanup→setup→s1~s7→cleanup 전체 생애주기를 라이브 재현한 T93(자기 번들 — 원자료: manifest.yaml · run.sh · result.txt · verdict.json)이다. 판정 구분: ✅ 실측 확인 (자기 번들 T93) / ✅ 문헌 확인 / △ 부분 실측 — 부분 실측 행은 무엇이 미검증인지 명시했다.

주장 판정 근거
C1. connectionPool은 클라이언트 sidecar의 outbound cluster에 발효된다 (서버 쪽 아님) ✅ 실측 확인 (자기 번들 T93 — 전 시나리오의 관측 지점이 client istio-proxy) istio.io/latest/docs/reference/config/networking/destination-rule/ · result.txt
C2. CB 4종(maxConnections/http1MaxPendingRequests/http2MaxRequests/maxRetries) → circuit_breakers.thresholds[] 매핑 ✅ 실측 확인 (자기 번들 T93 S7 — maxRetries=2 및 생략 필드 주입값을 config_dump로 관측) envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/circuit_breaker.proto · verdict.json
C3. connectionPool 설정 시 생략 CB 필드에 4294967295(2^32-1) 명시 주입 — 필드 하나만 걸어도 4종 전부 ✅ 실측 확인 (자기 번들 T93 S7-full·S7c) result.txt
C4. DR/connectionPool이 전혀 없으면 Envoy native 기본 1024/1024/1024/3 적용 ✅ 문헌 확인 envoyproxy.io circuit_breaker.proto (Thresholds 기본값)
C5. maxConnections 발동: upstream_cx_overflow·게이지 cx_open, 단독으론 거부 없음(초과분 pending 흡수) ✅ 실측 확인 (자기 번들 T93 S1 — +41, cx_open=1, 200 100%) result.txt
C6. http1MaxPendingRequests 발동: 즉시 503/UO, 503 건수 = upstream_rq_pending_overflow 델타 ✅ 실측 확인 (자기 번들 T93 S2 — +12 = 503 12건 정확 일치) istio.io circuit-breaking task · result.txt
C7. maxRequestsPerConnection=1: 거부 아닌 커넥션 turnover(cx_max_requests ≈ 요청 수, cx_total 동반), 다운스트림엔 비가시 ✅ 실측 확인 (자기 번들 T93 S3 — +20/+20, fortio Sockets 1) result.txt
C8. http.idleTimeout 발동: 활성 요청 없는 풀 커넥션이 시한 후 종료(upstream_cx_idle_timeout) ✅ 실측 확인 (자기 번들 T93 S4 — +1) result.txt
C9. maxConnectionDuration: graceful close·요청 무손실, HTTP 클러스터에선 HttpProtocolOptions 경로로 반영 ✅ 실측 확인 (자기 번들 T93 S5 — +2, 200 100%) result.txt · envoyproxy.io protocol.proto
C10. connectTimeout 발동: 503 + UF + {connection_timeout} + upstream_cx_connect_timeout ✅ 실측 확인 (자기 번들 T93 S6 — +15, 액세스 로그 URX,UF) result.txt · envoyproxy.io response flags
C11. connectTimeout × 기본 retry 곱: 기본 정책(2회)이 connect 실패를 재시도해 요청당 시도 3회·체감 ≈ 3×connectTimeout·최종 URX ✅ 실측 확인 (자기 번들 T93 S6 — 요청 5건에 connect 15회, avg 3033ms) result.txt · istio.io/latest/docs/concepts/traffic-management/#retries
C12. tcpKeepalive.{time,interval,probes} → upstream_connection_options.tcp_keepalive 반영 △ 부분 실측 — config 반영은 T93 S7로 확인, 발동(커널 프로브)은 stat 재현 불가(소켓 옵션)라 config로만 검증 result.txt · istio.io DR 레퍼런스
C13. maxConcurrentStreams: 설정 시 명시 주입(64) / 미설정 시 미주입(빈 객체) — istio.io “2^31-1"은 config에 나타나는 값이 아님, 실효 기본은 Envoy 1024 △ 부분 실측 — 주입/미주입은 T93 S7-full·S7c로 확정, “실효 1024"는 문헌, 발동(스트림 한도 도달)은 미검증 result.txt · envoyproxy.io protocol.proto
C14. http2MaxRequests → max_requests 매핑, 초과 시 503/UO △ 부분 실측 — 매핑·주입값은 T93 S7로 확인, 발동은 미검증(H2 오버플로 시나리오 미수행). 카운터 집계도 미확정 — 문헌은 upstream_rq_pending_overflow 합산 집계를 명시하나 T93이 upstream_rq_active_overflow의 실존(값 0)도 관측 envoyproxy.io cluster stats · verdict.json
C15. maxRetries → max_retries 매핑, 초과 시 재시도 억제·URX·upstream_rq_retry_overflow △ 부분 실측 — 매핑(max_retries=2)은 T93 S7로 확인, 발동은 미검증(동시 재시도 폭증 유도 필요). S6의 URX는 별개 메커니즘(retry-limit-exceeded)임을 T93이 명시 envoyproxy.io circuit breaking arch · verdict.json
C16. h2UpgradePolicy: UPGRADE → explicit http2 옵션 부여, useClientProtocol=true가 이를 무력화(use_downstream_protocol_config 존재 + explicit ABSENT) ✅ 실측 확인 (자기 번들 T93 S7b·S7c) result.txt
C17. VS retries(정책) ⊥ maxRetries(전역 동시량 CB) — 직교 ✅ 문헌 확인 istio.io DR 레퍼런스 · envoyproxy.io circuit breaking arch
C18. portLevelSettings는 대체(병합 아님) — 오버라이드된 포트는 destination 레벨을 상속하지 않고 생략 필드는 기본값 ✅ 문헌 확인 (istio.io 원문 인용) istio.io/latest/docs/reference/config/networking/destination-rule/
C19. http.*는 HTTP/1.1·H2·gRPC 트래픽에만 적용 — L4(TCP) 클러스터에선 무효, tcp.*는 공통 ✅ 문헌 확인 istio.io DR 레퍼런스 (TCPSettings/HTTPSettings 정의)
C20. tcp.idleTimeout 기본 1시간·0s=비활성, http.idleTimeout과 기준 상이(바이트 무이동 vs 활성 요청 없음) ✅ 문헌 확인 istio.io DR 레퍼런스
C21. 풀은 worker thread별 × priority별, circuit breaker 한도는 cluster 전역 공유 ✅ 문헌 확인 envoyproxy.io connection pooling arch · circuit breaking arch
C22. 번들 킷(files/lab/)만으로 전체 생애주기(cleanup→setup→s1~s7→cleanup) 재현 가능 — context 자동 감지·cluster domain 자동 발견·cleanup 멱등 포함 ✅ 실측 확인 (자기 번들 T93 KIT) result.txt · verdict.json

Files