---
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) |