--- title: DR connectionPool 정본 — 필드별 동작·매핑·관측 date: 2026-07-05 type: ref domain: istio tags: [istio, destination-rule, connection-pool, circuit-breaking, envoy] --- > [!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 경계) ⑥ 따라하기 실습(번들 킷 S1~S7) ⑦ 함정 **검증:** 발동 6종(S1~S6)·config 반영 4종(S7)·기본값 미결점 2건은 **자기 번들 실측 T93**(2026-07-05 — 아래 실습과 동일한 킷 `files/lab/`만으로 cleanup→setup→s1~s7→cleanup 전체 생애주기를 라이브 재현, 원자료 [result.txt](files/verify/T93/result.txt)·[verdict.json](files/verify/T93/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였던 이유가 이것이다. ```mermaid flowchart LR DRR["DestinationRule
connectionPool"] APP["client app
(fortio load)"] SC["client sidecar Envoy
outbound cluster = DR 발효 지점
circuit_breakers · 커넥션 풀"] BE["backend pod
server sidecar — connectionPool 무관"] DRR -.->|"istiod가 컴파일해 xDS로 주입"| SC APP --> SC SC ==>|"upstream 커넥션 풀
(worker thread별)"| BE ``` 이 풀의 성격 두 가지가 필드 전체의 존재 이유를 설명한다. **첫째, 프로토콜이 "어느 필드가 무는가"를 정한다.** HTTP/1.1은 커넥션 1개가 한 번에 요청 1개만 나르므로 동시성이 곧 커넥션 수다 — 그래서 `maxConnections`와 `http1MaxPendingRequests`(커넥션 확보 대기 큐)가 실질 레버다. 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의 **세 반영 위치**로 갈라지고, 그 위치가 곧 계열(거부형/관리형)과 관측 방법을 정한다: ```mermaid flowchart LR DR["DR trafficPolicy
connectionPool"] subgraph CBG["거부형 — 초과를 잘라낸다"] CB["Cluster.circuit_breakers
.thresholds"] end subgraph MG["관리형 — 커넥션을 다듬는다"] CL["Cluster 본체 옵션"] CH["HttpProtocolOptions
common_http_protocol_options"] EH["HttpProtocolOptions
explicit_http_config ·
use_downstream_protocol_config"] end DR -->|"tcp.maxConnections
http.http1MaxPendingRequests
http.http2MaxRequests
http.maxRetries"| CB DR -->|"tcp.connectTimeout
tcp.tcpKeepalive"| CL DR -->|"tcp.idleTimeout · http.idleTimeout
tcp.maxConnectionDuration
http.maxRequestsPerConnection"| CH DR -->|"http.maxConcurrentStreams
http.h2UpgradePolicy
http.useClientProtocol"| EH CB --> R1["발동 = 즉시 거부
503 UO · URX
stat: overflow 카운터"] CL --> R2["발동 = 커넥트 실패 · 커널 프로브
503 UF connect_timeout"] CH --> R3["발동 = 종료·교체 (graceful)
stat: cx_idle_timeout ·
cx_max_duration_reached · cx_max_requests"] EH --> R4["발동 없음 — 프로토콜 스위치
검증은 config_dump 로만"] ``` `istioctl proxy-config cluster --fqdn -o json`으로 뜯을 때의 매핑 치트시트 (T93 S7이 이 위치들을 전부 config_dump로 확인했다): ```text 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 정본](/docs/istio/xds-envoy/envoy-response-flags/)). --- ## 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 필드 노트](/docs/istio/egress/tcp-keepalive-fields/)가 정본이다. ### 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](/docs/istio/egress/tcp-tuning/)를 따른다(§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 매핑 | `UPGRADE` → `explicit_http_config.http2_protocol_options` 부여 | | 기본값 | `DEFAULT` = meshConfig의 `h2UpgradePolicy` 상속 (메시 전역 기본 DO_NOT_UPGRADE) | | 관측 stat | 없음 — config_dump로만 검증 | 발동형이 아니라 업스트림 프로토콜을 정하는 스위치다. T93 S7-full·S7c에서 `UPGRADE` 시 `http2_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에 찍힌 값이다. > [!warning] 부분 설정의 역설 > 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 구성](/docs/istio/egress/mtls-passthrough-connectionpool/)이 정본이다. --- ## 6. 상호작용 — 필드는 혼자 놀지 않는다 ### 6.1 `maxRetries`(CB) ⊥ VirtualService `retries`(정책) 둘은 직교하는 다른 축이다. VS `retries`는 **per-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의 숫자가 이 곱을 그대로 보여준다: ```text 요청 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 필드는 [회로 차단 메커니즘](/docs/istio/egress/circuit-breaking-mechanisms/)이 정본이다. ### 6.4 keepalive·idleTimeout과 중간장비 idle 경로 중간의 FW/NAT가 idle 세션을 조용히 버리면 "유휴 후 첫 요청 실패"가 된다. 처방은 이 문서의 두 필드 조합이다 — `tcpKeepalive.time`을 중간장비 idle보다 짧게 잡아 세션을 유지하거나, `idleTimeout`을 그보다 짧게 잡아 죽을 커넥션을 선점 정리한다. 값 설계와 재현·판별 절차는 [처방전 P4](/docs/istio/egress/tcp-tuning/)와 [tcpKeepalive 필드 노트](/docs/istio/egress/tcp-keepalive-fields/)로. --- ## 따라하기 실습 — 번들 킷으로 발동을 직접 본다 §3~§4가 "필드가 무엇인가"라면, 이 절은 "받아서 발동을 직접 보기"다. 아래 기대 출력은 전부 2026-07-05에 **이 킷만으로** cleanup→setup→s1~s7→cleanup 전체 생애주기를 라이브 재현한 T93 실측 ([result.txt](files/verify/T93/result.txt))에서 발췌했다 — 독자 클러스터에서는 pod 이름·cluster domain·카운터 절대값만 다르고 **델타의 형태는 같아야** 한다. **소요 시간**: setup 1~2분(이미지 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 일치): ```bash istioctl version # client/CP/DP 버전 일치 확인 kubectl -n istio-system get pods # istiod Running 확인 ``` - **어느 클러스터에서든 동작한다** — 스크립트가 현재 kubectl context를 자동 감지하고(다른 컨텍스트는 `CTX=`를 앞에 붙임), S7이 cluster domain을 coredns Corefile에서 자동 발견한다(T93에선 `cluster.local`이 아니라 `homelab.local`이었다). - 랩은 전용 ns `conn-lab`만 만들고 지운다 — 공유 인프라(egress gateway 등)는 건드리지 않는다. ### 실습 1. 랩 킷 받기 킷은 이 문서 번들의 개별 파일이다 (**주의: 디렉토리 링크는 404** — 아래처럼 파일 단위로만 받는다): | 파일 | 역할 | |---|---| | [00-namespace.yaml](files/lab/00-namespace.yaml) | ns `conn-lab` (istio-injection=enabled) — 발동 관측을 오염 없이 격리 | | [10-backend.yaml](files/lab/10-backend.yaml) | backend — fortio server. `?delay=`로 커넥션 점유 제어, Service 포트 name `http`(L7 필수, §5.3) | | [20-client.yaml](files/lab/20-client.yaml) | client — 관측 지점. proxyStatsMatcher 계측 스위치 + DNS capture/auto-allocate(S6 필수) | | [30-blackhole.yaml](files/lab/30-blackhole.yaml) | ServiceEntry — SYN이 무응답으로 사라지는 목적지(TEST-NET-2), S6 전용 | | [setup.sh](files/lab/setup.sh) · [run-scenario.sh](files/lab/run-scenario.sh) · [cleanup.sh](files/lab/cleanup.sh) | 부트스트랩 · 시나리오 하네스(run 내 델타 판정·리포트 생성) · 철거 — 전부 멱등 | | [s1-maxconnections.yaml](files/lab/dr-scenarios/s1-maxconnections.yaml) · [s2-maxpending.yaml](files/lab/dr-scenarios/s2-maxpending.yaml) · [s3-maxrequestsperconn.yaml](files/lab/dr-scenarios/s3-maxrequestsperconn.yaml) · [s4-http-idletimeout.yaml](files/lab/dr-scenarios/s4-http-idletimeout.yaml) · [s5-maxconnduration.yaml](files/lab/dr-scenarios/s5-maxconnduration.yaml) · [s6-connecttimeout.yaml](files/lab/dr-scenarios/s6-connecttimeout.yaml) · [s7-full.yaml](files/lab/dr-scenarios/s7-full.yaml) · [s7b-useclientprotocol.yaml](files/lab/dr-scenarios/s7b-useclientprotocol.yaml) · [s7c-h2-nomcs.yaml](files/lab/dr-scenarios/s7c-h2-nomcs.yaml) | `dr-scenarios/` — 시나리오별 DR(실험 변수). 하네스가 한 번에 하나만 남기고 apply | 한 줄 다운로드: ```bash 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 bash setup.sh # 다른 클러스터면: CTX= bash setup.sh ``` 기대 출력 골격(T93 [1] — pod 이름·노드는 클러스터마다 다름): ```text == [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 == ``` ```bash 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 bash run-scenario.sh s1 # 리포트: reports/_s1.md ``` 하네스가 하는 일: S1 DR apply → before 스냅샷 → fortio `-c 4 -qps 0 -n 40`(?delay=200ms로 커넥션 점유) → mid-load 게이지 → after 스냅샷 → 델타. 기대 출력 핵심(T93): ```text 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 bash run-scenario.sh s2 ``` 기대 출력 핵심(T93 — 직전 run의 잔재로 절대값이 41에서 시작한다. **델타만 본다**): ```text 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 bash run-scenario.sh s3 ``` 기대 출력 핵심(T93): ```text 델타: 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 bash run-scenario.sh s4 # 요청 1회 → 5초 대기 → 스냅샷 ``` 기대 출력 핵심(T93): ```text 델타: upstream_cx_idle_timeout 0 -> 1 = +1 ★ idle 2s 초과로 풀 커넥션 종료 upstream_cx_total 23 -> 24 = +1 ``` ### 실습 7. S5 — `maxConnectionDuration: 5s`, graceful 교체 ```bash bash run-scenario.sh s5 # keepalive 10초 부하 → 5초 시점 수명 도달 ``` 기대 출력 핵심(T93): ```text 델타: 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 bash run-scenario.sh s6 # blackhole.lab.internal (SYN 무응답) 으로 5요청 ``` 기대 출력 핵심(T93 — 관측 대상이 backend가 아니라 blackhole 클러스터로 바뀐다): ```text 델타(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 bash run-scenario.sh s7 # s7-full → s7c → s7b 순서로 apply + istioctl pc cluster 덤프 ``` 기대 출력 핵심(T93): ```text 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 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.;.: value`라 단순 `grep ` 파싱이 어긋날 수 있다 — 하네스는 `'.: '` 패턴으로 값을 추출한다. - **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) | | [회로 차단 메커니즘](/docs/istio/egress/circuit-breaking-mechanisms/) | 왜 회로 차단이 필요한가 / outlierDetection 필드는 무슨 뜻인가 | 개념·outlier 정본 — 이 문서는 connectionPool 필드 차원만 다룸 | | [tcpKeepalive 필드 노트](/docs/istio/egress/tcp-keepalive-fields/) | keepalive 3필드의 커널 매핑·권장값 | §3.3이 위임하는 keepalive 정본 | | [TCP 병목 정본](/docs/istio/egress/tcp-bottlenecks/) · [처방전](/docs/istio/egress/tcp-tuning/) | 이 필드들에 **어떤 값**을 줘야 하나 (OS 병목·운영값) | 값 처방 — 이 문서는 동작 정의 | | [TCP 병목 재현 랩](/docs/istio/egress/tcp-failure-reproduction/) | egress gateway 관점에서 병목이 어떻게 터지나 | 상보 랩 — 이 문서 실습은 sidecar 관점 필드 재현 | | [mTLS Passthrough connectionPool](/docs/istio/egress/mtls-passthrough-connectionpool/) | 2-hop에서 어느 DR 값이 어느 프록시에 발효되나 | §5.4가 위임 | | [DestinationRule 만들기](/docs/istio/egress/destinationrule-fundamentals/) | 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](/docs/istio/egress/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에서 커넥션 분포·재사용 수치가 "한도 직관"과 어긋나 보이는 이유가 대개 이것이다. --- ## 참조 **공식 문서** - [Istio DestinationRule 레퍼런스](https://istio.io/latest/docs/reference/config/networking/destination-rule/) — 전 필드 의미·기본값·portLevelSettings - [Istio circuit breaking task](https://istio.io/latest/docs/tasks/traffic-management/circuit-breaking/) — S2와 동일 패턴의 공식 재현 - [Istio 기본 retry 동작](https://istio.io/latest/docs/concepts/traffic-management/#retries) — §6.2의 기본 2회 - [Envoy circuit breaking arch](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/circuit_breaking) · [circuit_breaker.proto](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/circuit_breaker.proto) — 한도 동작·native 기본값 - [Envoy cluster stats](https://www.envoyproxy.io/docs/envoy/latest/configuration/upstream/cluster_manager/cluster_stats) — stat 이름 원문 - [Envoy connection pooling arch](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/connection_pooling) — per-thread × per-priority 풀 모델 - [Envoy HttpProtocolOptions](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/upstreams/http/v3/http_protocol_options.proto) · [protocol.proto](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/protocol.proto) — 관리형 필드의 반영 위치·기본값 - [Envoy response flags](https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-format-response-flags) — UO/UF/URX/UC **아카이브 내부** - 역할분담 표의 7개 문서 (위 절) · [Envoy response flag 정본](/docs/istio/xds-envoy/envoy-response-flags/) · [Cluster 해부](/docs/istio/xds-envoy/cluster-anatomy/) **번들 산출물** - 랩 킷: [setup.sh](files/lab/setup.sh) · [run-scenario.sh](files/lab/run-scenario.sh) · [cleanup.sh](files/lab/cleanup.sh) + 매니페스트·dr-scenarios — 개별 파일 링크 전체는 "실습 1" 표 - 실측 원자료: [manifest.yaml](files/verify/T93/manifest.yaml) · [run.sh](files/verify/T93/run.sh) · [result.txt](files/verify/T93/result.txt) · [verdict.json](files/verify/T93/verdict.json) --- ## 검증 기록 (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](files/verify/T93/manifest.yaml) · [run.sh](files/verify/T93/run.sh) · [result.txt](files/verify/T93/result.txt) · [verdict.json](files/verify/T93/verdict.json))이다. 판정 구분: **✅ 실측 확인 (자기 번들 T93)** / **✅ 문헌 확인** / **△ 부분 실측** — 부분 실측 행은 무엇이 미검증인지 명시했다. | 주장 | 판정 | 근거 | |---|---|---| | C1. connectionPool은 클라이언트 sidecar의 outbound cluster에 발효된다 (서버 쪽 아님) | ✅ 실측 확인 (자기 번들 T93 — 전 시나리오의 관측 지점이 client istio-proxy) | istio.io/latest/docs/reference/config/networking/destination-rule/ · [result.txt](files/verify/T93/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](files/verify/T93/verdict.json) | | C3. connectionPool 설정 시 생략 CB 필드에 4294967295(2^32-1) 명시 주입 — 필드 하나만 걸어도 4종 전부 | ✅ 실측 확인 (자기 번들 T93 S7-full·S7c) | [result.txt](files/verify/T93/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](files/verify/T93/result.txt) | | C6. http1MaxPendingRequests 발동: 즉시 503/UO, 503 건수 = `upstream_rq_pending_overflow` 델타 | ✅ 실측 확인 (자기 번들 T93 S2 — +12 = 503 12건 정확 일치) | istio.io circuit-breaking task · [result.txt](files/verify/T93/result.txt) | | C7. maxRequestsPerConnection=1: 거부 아닌 커넥션 turnover(`cx_max_requests` ≈ 요청 수, `cx_total` 동반), 다운스트림엔 비가시 | ✅ 실측 확인 (자기 번들 T93 S3 — +20/+20, fortio Sockets 1) | [result.txt](files/verify/T93/result.txt) | | C8. http.idleTimeout 발동: 활성 요청 없는 풀 커넥션이 시한 후 종료(`upstream_cx_idle_timeout`) | ✅ 실측 확인 (자기 번들 T93 S4 — +1) | [result.txt](files/verify/T93/result.txt) | | C9. maxConnectionDuration: graceful close·요청 무손실, HTTP 클러스터에선 HttpProtocolOptions 경로로 반영 | ✅ 실측 확인 (자기 번들 T93 S5 — +2, 200 100%) | [result.txt](files/verify/T93/result.txt) · envoyproxy.io protocol.proto | | C10. connectTimeout 발동: 503 + UF + `{connection_timeout}` + `upstream_cx_connect_timeout` | ✅ 실측 확인 (자기 번들 T93 S6 — +15, 액세스 로그 URX,UF) | [result.txt](files/verify/T93/result.txt) · envoyproxy.io response flags | | C11. connectTimeout × 기본 retry 곱: 기본 정책(2회)이 connect 실패를 재시도해 요청당 시도 3회·체감 ≈ 3×connectTimeout·최종 URX | ✅ 실측 확인 (자기 번들 T93 S6 — 요청 5건에 connect 15회, avg 3033ms) | [result.txt](files/verify/T93/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](files/verify/T93/result.txt) · istio.io DR 레퍼런스 | | C13. maxConcurrentStreams: 설정 시 명시 주입(64) / 미설정 시 미주입(빈 객체) — istio.io "2^31-1"은 config에 나타나는 값이 아님, 실효 기본은 Envoy 1024 | △ 부분 실측 — 주입/미주입은 T93 S7-full·S7c로 확정, "실효 1024"는 문헌, **발동(스트림 한도 도달)은 미검증** | [result.txt](files/verify/T93/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](files/verify/T93/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](files/verify/T93/verdict.json) | | C16. h2UpgradePolicy: UPGRADE → explicit http2 옵션 부여, useClientProtocol=true가 이를 무력화(use_downstream_protocol_config 존재 + explicit ABSENT) | ✅ 실측 확인 (자기 번들 T93 S7b·S7c) | [result.txt](files/verify/T93/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](files/verify/T93/result.txt) · [verdict.json](files/verify/T93/verdict.json) |