--- title: mTLS Passthrough TCP 운영 종합 — connectionPool·port reuse·런북 date: 2026-07-06 type: guide domain: istio tags: [egress, mtls, connection-pool, tcp, helm, runbook] --- > [!abstract] > egress **mTLS Passthrough**(HTTPS over mTLS, [용어 정본](/docs/istio/egress/https-pattern-map/)) 채널의 > TCP 운영 설정을 한 문서에 모았다. 구성은 두괄식이다 — 왜 문제가 생기나·문제 4가지(2장) → 답이 되는 > 전체 설정 한 벌(3장) → 문제별 설정 상세(4~7장) 순서다. > 각 주제의 정본은 문서 끝 참조 절에 있다. **대상 환경:** Istio **1.30.0**, sidecar mesh (homelab k8s 1.30.6 기준 컨벤션) **검증:** 본문 주장 대부분은 소스 정본들의 클러스터 실측(T16·T52·T53·T55·T57·T91·T93 — 각 정본의 검증 기록 참조)을 인용한다. 이 문서 자체 실측은 2건 — gateway Helm chart의 securityContext 렌더링(**T94**)과 주입 webhook의 securityContext 덮어쓰기(**T95**)다. (문서 끝 "검증 기록", 번들 `files/verify/T94/`·`files/verify/T95/`) **읽는 법:** 바로 적용할 사람은 0장 요약과 3장(전체 설정)·8장(검증). 문제별 배경은 4~7장. 장애 대응은 9장 런북. 값을 조정하고 싶으면 부록 A. --- ## 0. 요약 egress gateway를 두면, 외부로 나가는 연결이 gateway 몇 대로 모인다. 그러면 전에는 없던 문제 4가지가 생긴다. 전부 "연결의 개수"와 "연결의 수명" 문제다. | # | 문제 | 왜 생기나 | 해결 | 설정 위치 | |---|---|---|---|---| | P1 (4장) | 연결이 일정 개수에서 거부된다 | Envoy의 기본 상한이 목적지당 1,024개 | `maxConnections`를 직접 지정 | DestinationRule | | P2 (5장) | 새 연결을 못 만든다 (포트 부족) | 발신 포트가 pod당 약 28,000개뿐이고, 닫은 포트도 60초간 재사용 불가 | ① 앱 keep-alive ② gateway 증설 ③ 커널 설정(`tcp_tw_reuse`) | istiod Helm values(커널 설정)·gateway chart(증설) | | P3 (6장) | 한동안 안 쓰던 연결이 죽어 있다 | 방화벽이 유휴 연결을 조용히 끊는다. Envoy도 1시간이면 스스로 끊는다 | `tcpKeepalive`와 `idleTimeout`을 **둘 다** 설정 | DestinationRule | | P4 (7장) | 배포할 때마다 연결이 끊긴다 | 종료 전 대기(drain)가 기본 5초뿐 | drain 300초 + PDB | gateway Helm chart | 장애가 났을 때 넷 중 뭔지 구분하는 방법은 9장 런북에 있다. (다섯 번째 후보였던 conntrack은 위 한도들보다 훨씬 넓어서 일반 규모에선 도달하지 않는다 — 부록 B) 기억할 것 3개: 1. **DestinationRule은 2개 필요하다.** 연결 설정은 "연결을 여는 쪽"에 붙는다. 앱→gateway 연결은 앱의 sidecar가 열고, gateway→외부 연결은 gateway가 연다. 그래서 구간마다 하나씩이다. (4장) 2. **1,024는 "아무 설정도 안 했을 때" 기본값이다.** 함정이 하나 있다: connectionPool을 일부만 설정하면, 나머지 상한은 1,024가 아니라 **무제한**으로 풀린다. 그래서 `maxConnections`는 항상 직접 적는다. (4장) 3. **시작값으로 시작한다.** 이 문서의 4096, 300 같은 값은 일반 REST API 채널 기준의 시작값이다. 트래픽이 큰 채널만 부록 A의 계산식으로 다시 잡으면 된다. ### 0.1 용어 최소 사전 — 처음 읽는 사람용 | 용어 | 뜻 | |---|---| | sidecar | 앱 pod마다 함께 뜨는 프록시(Envoy). 앱의 모든 트래픽이 이걸 거쳐 나감 | | egress gateway | 외부(파트너망·인터넷)로 나가는 트래픽을 한곳에 모아 내보내는 전용 프록시 pod | | DestinationRule (DR) | "이 목적지로 갈 때는 이렇게 연결하라"를 정하는 Istio 리소스 — connectionPool이 여기 들어감 | | connectionPool | 목적지로 여는 연결의 개수 상한·수명·타임아웃 규칙 묶음 | | cluster (Envoy) | Envoy 내부에서 "목적지 하나"를 부르는 단위. DR 설정이 최종적으로 여기에 반영됨 | | SNI | TLS 접속 첫 인사에 "이 도메인 찾아왔다"고 밝히는 필드. gateway가 채널을 구분하는 열쇠 | | mTLS / SPIFFE 신원 | 메시 내부 상호 TLS — 워크로드마다 발급된 인증서로 "누가 보냈나"를 증명 | | response flag | Envoy access log의 실패 원인 코드. 이 문서에선 `UO`(풀 초과)·`UF`(연결 실패) 두 개만 알면 됨 | | TIME_WAIT | TCP 연결을 닫은 뒤 커널이 그 포트를 60초간 잠가두는 상태 — 포트 고갈의 주범 | --- ## 1. 패턴 개요 — 두 홉, 두 봉투 mTLS Passthrough(HTTPS over mTLS)가 푸는 문제는 하나다. 종단간 암호화는 건드리지 않으면서, 누가 나가는지만 egress에서 확인하고 싶다. 동작은 3단계다: 1. 앱은 평소처럼 `https://`를 직접 호출한다. 이 TLS(**inner**)는 앱과 외부 서버 사이에서만 열린다. 중간의 누구도 풀지 않는다. 2. sidecar가 그 위에 메시 mTLS(**outer**)를 한 겹 더 입혀 gateway로 보낸다. (홉 1) 3. gateway는 outer만 벗겨서 호출자의 신원(SPIFFE — 메시가 워크로드마다 발급하는 인증서 ID)을 확인한다. 안에 든 inner TLS는 풀지 않고 그대로 외부로 흘린다. (홉 2) ```mermaid flowchart LR APP["app
(inner TLS 생성)"] --> SC["sidecar
(outer mTLS 래핑)"] SC -->|"홉 1: outer mTLS"| GW["egress gateway
(outer만 벗겨 신원 확인)"] GW -->|"홉 2: TCP 그대로 전달"| EXT["외부 서버
(inner TLS 종단)"] ``` 와이어 어디에도 평문 구간은 없다. "홉 2가 일반 TCP"라는 말은 gateway가 TCP로 그대로 전달한다는 뜻이지, 암호화가 없다는 뜻이 아니다. --- ## 2. 왜 문제가 생기나 — 두 가지 사실과 문제 4가지 egress gateway를 도입하면 외부로 나가는 연결이 gateway 몇 대로 몰린다. 이 집중을 고려하지 않고 운영하면 구조적으로 결국 아래 문제 4가지로 이어진다. 더 큰 문제는 장애가 난 뒤다. 넷 다 클라이언트 증상이 비슷해서, 배경지식이 없으면 원인 찾기가 어렵다. 그래서 이 장은 원인이 되는 사실 2개를 먼저 깔고, 그 위에서 문제 4가지를 정의한다. 해결 설정은 3장에 한 벌로 제시한다. ### 2.1 사실 1 — gateway는 연결을 재사용하지 못한다 gateway는 연결을 "통과"시키는 장치가 아니다. 양쪽에 연결을 하나씩 잡고, 가운데서 바이트를 복사하는 중계기다. ```mermaid flowchart LR APP["app"] -->|"연결 A"| GW["gateway
소켓 2개: A 받기 + B 열기"] GW -->|"연결 B (새로 엶)"| EXT["외부 서버"] ``` 앱이 연결 1개를 만들면 gateway에는 소켓이 2개 잡힌다. A를 받는 소켓 하나, 외부로 새로 여는 B의 소켓 하나. 그리고 B를 열 때마다 발신 포트를 1개씩 쓴다. L7 proxy라면 B 하나를 여러 앱의 요청이 나눠 탈 수 있다. (요청의 경계를 아니까) 근데 이 패턴의 gateway가 보는 건 암호화된 바이트 덩어리다. 내용을 모르니 섞을 수 없다. 그래서 A 1개당 B 1개, 1:1이 강제다. (PASSTHROUGH든 ISTIO_MUTUAL이든 같다) 이 1:1은 닫을 때도 그대로다. 앱이 A를 닫으면 gateway도 B를 닫는다. 앱이 연결을 아끼면 gateway 포트도 아껴지고, 앱이 낭비하면 gateway가 대신 아껴줄 방법이 없다. ### 2.2 사실 2 — 발신 포트는 유한하다 TCP 연결은 (출발 IP, 출발 포트, 목적지 IP, 목적지 포트) 4개로 구분된다. 외부 API 호출에서 목적지는 고정이고, 출발 IP도 gateway pod IP로 고정이다. 그러면 변수는 출발 포트 하나뿐이다. - **동시 연결 한계**: 커널이 발신 연결에 빌려주는 임시 포트(ephemeral port)는 기본 약 **28,000개**다 (범위 32768–60999). pod 1개, 목적지 1개당 이게 전부다. - **초당 연결 한계**: 닫은 포트는 바로 반납되지 않는다. **TIME_WAIT 상태로 60초간 잠긴다.** (커널 고정값 — 방금 닫은 연결의 지연 패킷이 같은 포트의 새 연결에 끼어드는 걸 막는 격리 시간) 그래서 초당 r개씩 새로 열면, 어느 순간이든 잠겨 있는 포트가 r × 60개다. 이게 28,000을 넘으면 빌려줄 포트가 없다: ``` r × 60초 ≤ 28,000 → r ≤ 약 470개/초 ``` (비유: 컵 28,000개짜리 대여점인데 반납된 컵은 소독에 60초 걸린다. 초당 470명까지만 지속 가능하다) - 예: pod 500개가 keep-alive 없이 초당 1번씩 호출하면 gateway에 초당 500개가 모인다. gateway 1대면 몇십 초 안에 포트가 마른다. ### 2.3 문제 4가지 — 정의 두 사실 위에 서면 문제 4가지가 그대로 나온다: | # | 증상 | gateway 쪽 기록 | 원인 한 줄 | |---|---|---|---| | P1 | 특정 채널 호출이 일정 개수에서 거부된다 | access log flag `UO` | Envoy 기본 상한 1,024에 전사 트래픽이 부딪힘 | | P2 | 외부로 새 연결 자체가 안 열린다 | flag `UF` + 로그 `EADDRNOTAVAIL` | 발신 포트 고갈 — 사실 2의 산술 한계 | | P3 | 유휴 후 첫 요청만 실패 / 정확히 1시간마다 절단 | `upstream_cx_idle_timeout` 증가 (Envoy 절단일 때만 — 방화벽 절단은 기록 없음) | 방화벽 유휴 절단 + Envoy `idleTimeout` | | P4 | 배포·스케일 인마다 일괄 reset | `upstream_cx_destroy` 급증 | 종료 전 대기(drain)가 기본 5초 | 표의 gateway 쪽 기록 중 `UO`(P1)와 idleTimeout 정각 절단(P3)은 직접 재현해서 확인했다 ([T52 실측 번들](/docs/istio/egress/circuit-breaking-mechanisms/files/verify/T52/) · [T16 실측 번들](/docs/istio/egress/tcp-keepalive-fields/files/verify/T16/)). `UF`+`EADDRNOTAVAIL`(P2)과 `upstream_cx_destroy` 급증(P4)은 Envoy 공식 문서 기준이고(부록 D), 직접 재현하지는 않았다. ### 2.4 어느 문제부터 만나나 — 앱의 연결 사용 방식이 정한다 앱이 연결을 쓰는 방식은 둘 중 하나다. 1. **요청마다 열고 닫는다 (단발성).** 열린 연결은 안 쌓인다. 대신 닫은 포트(TIME_WAIT)가 쌓인다. → **P2부터 만난다.** 2. **연결을 계속 들고 쓴다 (keep-alive).** 포트는 안 마른다. 대신 열린 연결이 쌓인다. → **P1부터 만난다.** 근데 P2의 근본 처방이 "앱을 keep-alive로 바꾸는 것"이다(5장). 처방을 따르는 순간 앱은 2번 방식이 되고, 이번엔 P1 쪽 상한이 일하기 시작한다. P3·P4는 그렇게 수명이 길어진 연결의 후속 문제다. 그래서 골라 잡는 게 아니라 넷을 세트로 잡는다. --- ## 3. 답 — 전체 YAML 한 벌 (복사해서 시작) 2장의 문제 4가지를 전부 막는 설정이다. 예시 채널: `api.partner-a.example.com:443`. 값은 전부 시작값이다. (조정은 부록 A) 각 값이 왜 필요한지는 주석의 P1~P4 표시를 따라 4~7장에서 찾는다. 채널마다 넣는 것(레이어 1 — CRD 5종)과, 전역으로 한 번 넣는 것(레이어 2 — gateway·istiod Helm values)으로 나뉜다. ### 레이어 1 — 배선 + DR 2개 (채널당 1세트) ```yaml # 1) ServiceEntry — 외부 host 등록 (protocol TLS: gateway가 L7 파싱 안 함) apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: { name: partner-a, namespace: istio-system } spec: hosts: [api.partner-a.example.com] ports: - { number: 443, name: tls, protocol: TLS } resolution: DNS location: MESH_EXTERNAL --- # 2) Gateway — outer ISTIO_MUTUAL 종단 (protocol TLS — HTTPS 아님, inner는 opaque) apiVersion: networking.istio.io/v1 kind: Gateway metadata: { name: egress-partner-a, namespace: istio-system } spec: selector: { istio: egressgateway } servers: - port: { number: 8443, name: tls-partner-a, protocol: TLS } hosts: [api.partner-a.example.com] tls: { mode: ISTIO_MUTUAL } # client cert 강제 + SPIFFE 검증 + outer만 종단 --- # 3) DR-hop1 — outer mTLS 래핑 + 홉 1 풀 (호출자 sidecar cluster에 컴파일) apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: { name: egressgateway-partner-a, namespace: istio-system } spec: # exportTo 없음(기본 *) — 소비자 = 모든 클라이언트 ns sidecar host: istio-egressgateway.istio-system.svc.cluster.local # 레지스트리 FQDN과 일치 필수 subsets: - name: partner-a trafficPolicy: portLevelSettings: - port: { number: 443 } # Service 포트 기준 tls: mode: ISTIO_MUTUAL # sidecar가 SPIFFE cert 제시 sni: api.partner-a.example.com # gateway filter chain 매칭 키 connectionPool: # tls와 같은 포트 엔트리에 — 통째 교체 규칙(4장) tcp: maxConnections: 4096 # "클라이언트 pod당" 상한 — 여유 있게 connectTimeout: 3s # 클러스터 내부 홉이므로 짧게 tcpKeepalive: { time: 300s, interval: 30s, probes: 3 } --- # 4) DR-hop2 — tls 블록 없음(raw TCP), 홉 2 풀 전용 (gateway cluster에 컴파일) apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: { name: partner-a-tcp-pool, namespace: istio-system } spec: host: api.partner-a.example.com # ServiceEntry hosts와 정확 일치 exportTo: ["."] # 타 ns sidecar 오적용 차단 trafficPolicy: connectionPool: tcp: # http.* 는 TCP 라우트라 무의미 — tcp만 maxConnections: 4096 # P1: 전사 연결이 모이는 곳 connectTimeout: 3s # 외부 장애 시 빠른 실패 (기본 10s) idleTimeout: 1800s # P3: 채널 최장 유휴보다 길게 (keepalive로 못 막음) maxConnectionDuration: 3600s # P3/P4: 수명 상한 — 재분배·drain 자연 소멸 # (재연결 민감 채널은 제외하거나 길게) tcpKeepalive: { time: 300s, interval: 30s, probes: 3 } # P3: 방화벽 유휴 제한의 1/3 --- # 5) VirtualService — leg-1 tls(SNI 생존) / leg-2 tcp(SNI 소비 후 raw relay) apiVersion: networking.istio.io/v1 kind: VirtualService metadata: { name: partner-a-via-egress, namespace: istio-system } spec: hosts: [api.partner-a.example.com] gateways: [mesh, istio-system/egress-partner-a] tls: - match: - gateways: [mesh] port: 443 sniHosts: [api.partner-a.example.com] # 앱 ClientHello의 SNI로 매치 route: - destination: host: istio-egressgateway.istio-system.svc.cluster.local subset: partner-a # -> DR-hop1 port: { number: 443 } tcp: - match: - gateways: [istio-system/egress-partner-a] port: 8443 route: - destination: host: api.partner-a.example.com # -> DR-hop2 (tls 없음 = raw forward) port: { number: 443 } ``` **문자열 정렬 체크리스트** — 한 글자 어긋나면 에러 없이 조용히 깨진다: - 외부 host: `SE.hosts` = `GW.servers.hosts` = `VS.hosts` = VS leg-1 `sniHosts` = DR-hop1 `sni` = DR-hop2 `host` - gateway 대상: DR-hop1 `host`(레지스트리 FQDN) = VS leg-1 `destination.host` - subset: DR-hop1 `subsets[].name` = VS leg-1 `destination.subset` - 포트 체인: Service 443 → targetPort 8443 = `GW.port` = VS leg-2 match port ### 레이어 2 — Helm values (전역 1벌: gateway 차트 + istiod 차트) ```yaml # (a) gateway 차트 values replicaCount: 3 # 포트 공간 = 28k × replica (P2) autoscaling: enabled: true minReplicas: 3 maxReplicas: 6 targetCPUUtilizationPercentage: 70 # 연결 중심 부하는 CPU와 상관이 약함 — # upstream_cx_active 기반 custom metric 권장 affinity: podAntiAffinity: # SNAT 환경에선 required가 필수 (5장) requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: { matchLabels: { istio: egressgateway } } topologyKey: kubernetes.io/hostname nodeSelector: { node-role/egress: "true" } # 전용 노드풀 = 방화벽 등록 IP 안정화 tolerations: - { key: node-role/egress, operator: Exists } podAnnotations: proxy.istio.io/config: | terminationDrainDuration: 300s # P4 proxyMetadata: EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true" service: type: ClusterIP # egress는 외부 노출 불필요 ports: - { name: status-port, port: 15021, targetPort: 15021 } - { name: tls-egress, port: 443, targetPort: 8443 } ``` ```yaml # (b) istiod 차트 values — P2 커널 파라미터는 여기 # gateway 차트의 securityContext에 넣으면 주입 webhook이 조용히 덮어쓴다 (5장, 실측 T95) gateways: securityContext: # 주입되는 모든 gateway(ingress 포함)에 전역 적용 sysctls: - { name: net.ipv4.ip_unprivileged_port_start, value: "0" } # 자동 기본 재기재 (5장 함정) - { name: net.ipv4.ip_local_port_range, value: "10240 60999" } - { name: net.ipv4.tcp_tw_reuse, value: "1" } # kubelet·PSA 관문 선행 (5장) ``` (+ P4의 PDB는 7장, `tcp_tw_reuse` 관문 2개는 5장 참조. istiod values 변경 후엔 gateway rollout restart 필요) --- ## 4. P1 — 연결이 일정 개수에서 거부된다 ### 문제 특정 채널 호출이 갑자기 거부된다. gateway access log에 flag `UO`가 찍히고 `envoy_cluster_upstream_cx_overflow` 지표가 오른다. 원인: connectionPool을 안 걸면 **Envoy 내장 기본 상한이 목적지당 동시 1,024개**다. sidecar 시절엔 pod 하나가 한 목적지로 1,024개를 열 일이 없어서 한 번도 안 보이던 벽인데, gateway에선 전사 트래픽이 한곳에 모이니 바로 부딪힌다. ### 해결 `maxConnections`를 직접 지정한다. 시작값 4096. (근거와 재계산은 부록 A) 근데 한 군데가 아니라 **두 군데**에 적어야 한다. 이유는 아래. ### 상세 — DR이 2개인 이유 istiod는 DR을 "그 목적지를 향한 cluster 설정"으로 컴파일해서 프록시에 내려주는데, cluster는 **프록시마다 따로** 있다. 그래서 설정이 실제로 먹는 곳은 **그 목적지로 연결을 여는 프록시**다. ```mermaid flowchart LR DR1["DR-hop1
(host: gateway)"] -.->|컴파일| SC["호출자 sidecar의 cluster
→ pod당 상한"] DR2["DR-hop2
(host: 외부 host)"] -.->|컴파일| GW["gateway의 cluster
→ 전사 합류 상한"] ``` | | DR-hop1 (sidecar→gw) | DR-hop2 (gw→외부) | |---|---|---| | `host` | gateway Service의 레지스트리 FQDN (= `istioctl proxy-config cluster` 출력에 보이는 전체 이름) | ServiceEntry의 외부 host | | `tls` | `ISTIO_MUTUAL` + `sni` (outer 봉투를 만든다) | **블록 자체가 없다** (TCP 그대로 전달) | | connectionPool 적용 위치 | **호출자 sidecar** — 값은 "클라이언트 pod당" 상한 | **gateway** — 전사 연결이 모이는 곳, 튜닝의 핵심 지점 | | `exportTo` | 안 적는다 (mesh 전체 공개 — 모든 호출 ns의 sidecar가 써야 함) | `["."]` (쓰는 건 gateway뿐) | 정말 그런가? 확인하는 방법이 있다. 두 DR의 `maxConnections`에 서로 다른 식별값을 넣고 양쪽 프록시의 cluster를 덤프해 보면, 각 값이 자기 프록시에만 나타난다. (8장, 실측 T91) 규칙 3개: 1. **`tls.mode: tcp`는 없다.** enum은 `DISABLE/SIMPLE/MUTUAL/ISTIO_MUTUAL` 4개뿐이고, apply하면 CRD가 거부한다. 홉 2의 "일반 TCP"는 mode를 고르는 게 아니라 **tls 블록을 빼는 것**이다. (DR을 안 만들거나 / tls 없이 풀만 있는 DR / `DISABLE` 명시 — 셋 다 같다) 2. **`http.*` 풀 설정은 조용히 무시된다.** 두 홉 다 TCP 라우트라 HTTP 풀 개념이 없다. 연결 관리는 `tcp.idleTimeout`, `tcp.maxConnectionDuration`, `tcp.tcpKeepalive`로만 한다. 3. **DR의 `host`가 Istio가 아는 이름과 다르면 에러 없이 통째로 무시된다.** 증상은 "설정했는데 기본값"이다. `istioctl proxy-config cluster`에 보이는 이름 기준으로 맞춘다. ### 상세 — 기본값 함정 2개 (실측으로 확정) - **기본값이 둘이다.** "기본 1,024"는 connectionPool을 **아예 안 걸었을 때** 얘기다. connectionPool에 아무 필드든 하나라도 걸면, 생략한 상한은 1,024가 아니라 **무제한**으로 바뀐다: | 상태 | maxConnections 실효값 | |---|---| | DR/connectionPool 자체가 없음 | **1,024** — Envoy 내장 기본 | | connectionPool을 걸고 maxConnections 생략 | **4294967295 ≈ 무제한** — Istio가 명시 주입 | | 명시 | 명시한 값 | `idleTimeout` 하나 넣자고 connectionPool을 만들면 1,024 안전망이 조용히 사라진다. 그래서 connectionPool을 일부만 도입할 때도 `maxConnections`는 같이 적는다. - **subset과 portLevelSettings는 병합이 아니라 통째 교체다.** subset에 설정을 쓰면 상위 설정을 필드 단위로 물려받지 않는다. `connectionPool`, `tls` 같은 덩어리 단위로 교체된다. 포트 엔트리에 `tls`만 적고 connectionPool을 빼먹으면 그 포트의 풀은 기본값으로 돌아간다. 트래픽이 타는 포트 엔트리에 전부 다시 적는다. ### 상세 — 이 상한은 평소에 걸리라고 두는 값이 아니다 `maxConnections`가 세는 건 "지금 열려 있는 연결"뿐이다. 닫고 버린 포트(TIME_WAIT)는 안 센다. 그래서 요청마다 열고 닫는 단발성 채널에선 이 상한이 평소에 놀고 있는 게 정상이다. (그 채널의 진짜 위험은 포트 고갈 — 5장. 그건 이 상한이 못 막는다) 그럼 왜 두나? 외부 서버가 느려지는 날 때문이다. 1. 열려 있는 연결 수는 대략 "초당 새 연결 × 연결 수명"이다. 2. 평소: 초당 500개 × 0.05초 = 동시 25개. 상한 근처에도 안 간다. 3. 외부 응답이 5초로 늘어진 날: 500 × 5 = 2,500개. 순식간에 쌓인다. 응답이 안 오면 연결을 못 닫는다. 단발성 앱도 이 순간엔 "오래 들고 있는" 앱이 된다. 이때 gateway가 연결을 무한정 쌓아 같이 죽는 걸 이 상한이 끊는다. 재시도 폭풍(실패 → 앱 일제 재시도 → 연결 시도 급증)도 같은 방식으로 막는다. (퓨즈와 같다 — 평소에 안 끊어진다고 빼지 않는다. `connectTimeout: 3s`도 같은 목적의 세트다) keep-alive 채널은 다르다. 평시에도 열린 연결이 쌓이니, 여기선 이 상한이 장애 대비가 아니라 평시 용량 설계값이다. (부록 A: 최대 동시 연결 × 2~3) 핵심 설정만 뽑으면 이렇다 (전체 YAML은 3장): ```yaml # DR-hop2 (gateway→외부) — 전사 연결이 모이는 곳, 핵심 connectionPool: tcp: maxConnections: 4096 # 시작값 — 큰 채널은 부록 A로 재계산 connectTimeout: 3s # 외부 장애 시 빠른 실패 (기본 10s) ``` ```yaml # DR-hop1 (sidecar→gateway)의 subset에도 — pod당 상한이라 여유 있지만, 숨은 1,024는 동일 connectionPool: tcp: { maxConnections: 4096 } ``` **확인**: `istioctl proxy-config cluster`로 양쪽 값 확인(8장). 알람은 `upstream_cx_overflow`(9장). --- ## 5. P2 — 새 연결을 못 만든다 (포트 고갈) ### 문제 gateway에서 외부로 connect가 실패한다. access log에 flag `UF`, gateway 로그에 `EADDRNOTAVAIL`. 원인은 2장의 두 사실이다. 포트는 pod당·목적지당 동시 약 28,000개, 초당 약 470개가 산수로 정해진 한계고(2.2절), gateway는 1:1 중계라 앱 대신 연결을 아껴줄 수도 없다(2.1절). ### 해결 (우선순위 순) 1. **앱 keep-alive** — 유일한 근본 처방. keep-alive는 응답이 끝나도 연결을 닫지 않는 약속이다. 다음 요청이 그 연결을 그대로 탄다. 새 연결이 없으면 새 포트도 없고, TIME_WAIT도 안 쌓인다. 2.2절의 산술 자체가 무의미해진다. TIME_WAIT 알람이 울리면 인프라 튜닝 전에 호출 라이브러리의 connection reuse 설정부터 본다. (여기서 keep-alive는 HTTP 연결 재사용이다. 6장의 `tcpKeepalive` — 유휴 연결에 보내는 생존 신호 — 와는 이름만 비슷한 다른 것이다) 2. **gateway 증설** — 포트 공간이 replica 수만큼 늘어난다. 단 SNAT 환경이면 노드를 갈라야 효과가 있다 (아래). 3. **커널 파라미터** — 포트 범위 확장(`ip_local_port_range`) + TIME_WAIT 포트 재사용(`tcp_tw_reuse`). 처방 1을 따르면 앱은 "연결을 들고 쓰는" 쪽이 된다. 그때부터는 4장의 `maxConnections`가 평시에도 일하는 세상이다. P1·P2가 세트인 이유다(2장). ### 상세 1 — SNAT부터 확인한다 포트 계산은 "출발 IP = gateway pod IP"가 전제다. CNI가 노드를 나갈 때 SNAT하면(예: Calico `natOutgoing: true`) 출발 IP가 노드 IP로 바뀐다. 그러면 둘이 달라진다: 1. 같은 노드의 gateway pod들이 노드 IP의 포트를 나눠 쓴다. replica를 늘려도 같은 노드면 효과가 없다. 그래서 `podAntiAffinity`가 필수가 된다. 2. 방화벽에 등록하는 단위가 바뀐다 (pod 대역 ↔ 노드 IP). 용량 계산과 보안 신청을 같은 설정이 동시에 결정하니, 도입 전에 가장 먼저 확인한다. ### 상세 2 — 커널 파라미터는 istiod Helm values로 넣는다 (gateway 차트가 아니다) 커널 파라미터는 노드가 아니라 **pod 네트워크 네임스페이스(netns) 값**이다. 새 netns는 호스트 값이 아니라 커널 기본값으로 시작한다. 그래서 노드 `/etc/sysctl.d`에 넣어도 gateway pod에는 안 먹는다. 그럼 gateway 차트의 `securityContext`에 넣으면 되나? **그것도 안 된다. 이 절의 핵심 함정이다** (T95 실측): 1. gateway 차트 values의 sysctls는 Deployment까지는 정상 렌더링된다 (T94). 2. 근데 gateway pod는 생성 시점에 sidecar injector(MutatingWebhook)를 거친다. 주입 템플릿이 pod-level `securityContext`를 **무조건** 다시 쓰고, `sysctls`는 리스트라 병합이 아니라 **통째 교체**된다. 3. 결과: 최종 pod에는 istiod 쪽 값(기본 `ip_unprivileged_port_start=0` 하나)만 남는다. 차트에 적은 `ip_local_port_range`는 pod spec에서도 `/proc`에서도 사라진다. **에러가 없어서 Deployment만 보면 적용된 걸로 착각한다.** 실효 주입 지점은 **istiod 차트 values의 `gateways.securityContext`**다. 주입 템플릿이 pod를 만들 때 이 값을 읽는다: ```yaml # istiod Helm values (gateway 차트 아님!) gateways: securityContext: # 주입되는 모든 gateway(ingress 포함)에 전역 적용 sysctls: - { name: net.ipv4.ip_unprivileged_port_start, value: "0" } # 재기재 필수 (아래 함정) - { name: net.ipv4.ip_local_port_range, value: "10240 60999" } # safe: 포트 풀 28k -> 50k - { name: net.ipv4.tcp_tw_reuse, value: "1" } # unsafe: 관문 2개 선행 (아래) ``` > **덮어쓰기 함정 2겹 (실측 T94·T95)**: 기본 상태에선 차트와 주입 템플릿이 저권한 포트 바인딩용 > `sysctls: [net.ipv4.ip_unprivileged_port_start=0]`을 자동으로 넣어준다. 근데 securityContext를 > 직접 지정하는 순간 — gateway 차트에서든(T94) istiod values에서든(T95) — 이 자동 기본이 > **통째로 사라진다**. 그래서 위처럼 `ip_unprivileged_port_start`를 같이 다시 적는다. > (빼먹으면 runAsNonRoot인 gateway가 1024 미만 포트를 못 연다) 적용·확인 주의 2개: 1. webhook은 pod **생성 시점**에만 개입한다. istiod values를 바꾼 뒤 gateway를 rollout restart 해야 반영된다. 2. 확인은 Deployment가 아니라 **pod**에서 한다. Deployment에는 차트 값이 그대로 남아 있어서 속는다. `tcp_tw_reuse=1`은 Helm values에 적는 것만으로 안 먹는다. 관문 2개를 먼저 열어야 하고, 빼먹으면 pod가 `SysctlForbidden`으로 뜬다: ```yaml # (1) kubelet: 해당 노드 그룹에 allowedUnsafeSysctls 허용 (safe 목록에 없는 sysctl이라서) kubelet_config_extra_args: allowedUnsafeSysctls: ["net.ipv4.tcp_tw_reuse"] # (2) 네임스페이스 PSA(Pod Security Admission) 정책이 unsafe sysctl을 허용하는지 확인 # 의미: 나가는 연결에 TIME_WAIT 포트 재사용 허용 (TCP timestamps 켜져 있어야 함, 기본 on) # 참고: ip_local_port_range·ip_unprivileged_port_start는 safe sysctl — 관문 없이 선언만으로 적용 ``` **확인**: ```bash # 1) 최종 pod spec에 sysctls가 남아 있나 — Deployment 말고 pod를 본다 (T95 함정) kubectl -n istio-system get pod -l istio=egressgateway \ -o jsonpath='{.items[0].spec.securityContext.sysctls}' # 2) 커널에 실제로 박혔나 kubectl -n istio-system exec deploy/istio-egressgateway -- \ cat /proc/sys/net/ipv4/tcp_tw_reuse /proc/sys/net/ipv4/ip_local_port_range # 기대: 1 / 10240 60999 ``` 알람은 TIME_WAIT 개수(9장). 2만을 넘으면 커널 튜닝이 아니라 앱 keep-alive 캠페인 신호다. --- ## 6. P3 — 한동안 안 쓰던 연결이 죽어 있다 ### 문제 증상이 두 갈래다. 원인이 다르니 구분해야 한다: 1. **유휴 후 첫 요청만 실패한다** — 방화벽이 오래 조용한 연결을 세션 테이블에서 조용히 지운 것. 연결 양쪽은 살아 있다고 믿는데 중간이 끊겨 있다. 트래픽 적은 채널에서 먼저 터진다. (바쁜 채널은 유휴해질 틈이 없다) 2. **정확히 N초(기본 1시간)에 끊긴다** — Envoy 자신의 `idleTimeout`이다. keep-alive로 연결을 오래 쓰기 시작하면(5장) 반드시 따라오는 문제다. 연결이 오래 살수록 유휴 구간도 생기니까. ### 해결 원인이 둘이라 설정도 둘 다 필요하다: - 방화벽 몫: `tcpKeepalive` — 주기적으로 안부 패킷(probe)을 보내 방화벽 세션 타이머를 갱신한다. (5장의 앱 keep-alive와는 다른 물건이다 — 그건 연결 재사용, 이건 살아 있는 연결의 생존 신호) - Envoy 몫: `idleTimeout` — keepalive probe는 데이터가 아니라서 **Envoy의 idle 타이머를 못 돌려놓는다** (실측 확인). "keepalive 넣었는데 정확히 1시간마다 끊긴다"가 바로 이 경우다. 채널의 최장 유휴 시간보다 길게 직접 정한다. ### 상세 — keepalive 세 필드 | DR 필드 | 소켓 옵션 | 역할 | 시작값 | |---|---|---|---| | `time` | TCP_KEEPIDLE | **방화벽 세션 유지** — 유휴 time초 후 probe(빈 ACK)를 보내고, 그 왕복이 방화벽 타이머를 갱신 | 300 (방화벽 유휴 제한의 1/3 이하 — 부록 A) | | `interval` | TCP_KEEPINTVL | **죽은 상대 감지** — 무응답이면 이 간격으로 재시도 | 30 | | `probes` | TCP_KEEPCNT | 연속 무응답이 이 횟수면 죽었다고 보고 폐기 | 3 (30×3=90초 안에 죽은 연결 정리) | Envoy는 연결을 열 때 소켓 옵션(setsockopt)만 걸고, probe는 커널이 보낸다. 커널 sysctl 값은 안 바뀐다. 그리고 이미 열려 있는 연결에는 소급되지 않는다. (기존 연결은 idleTimeout이나 maxConnectionDuration으로 자연 교체되게 한다) ```yaml # DR-hop2 (gateway→외부)에 추가 (전체는 3장) connectionPool: tcp: idleTimeout: 1800s # Envoy 몫 — 채널 최장 유휴보다 길게 maxConnectionDuration: 3600s # 수명 상한 — 재분배·drain 자연 소멸 (재연결 민감 채널은 제외) tcpKeepalive: { time: 300s, interval: 30s, probes: 3 } # 방화벽 몫 ``` ### (선택) 채널별 DR 없이 전역으로 mesh 전체에 기본 keepalive를 깔 수도 있다. (DR이 있으면 DR이 이긴다) ```yaml meshConfig: tcpKeepalive: { time: 300s, interval: 30s, probes: 3 } # time은 "모든 채널의 방화벽 유휴 제한 중 최솟값의 1/3 이하"로 — 전역이라 채널별 조정 여지가 없다 ``` 주의: pod sysctl(`net.ipv4.tcp_keepalive_*`)만으로는 Envoy 연결에 keepalive가 아예 안 켜진다. sysctl은 값만 정하고, 켜는 스위치(`SO_KEEPALIVE`)는 소켓을 만든 쪽이 걸어야 한다. Envoy는 설정 없이는 안 건다. **확인**: 소켓의 timer 필드를 직접 본다 (8장의 ② — sysctl로 확인하면 보는 곳이 틀린 거다). --- ## 7. P4 — 배포할 때마다 연결이 끊긴다 ### 문제 재배포나 스케일 인 시점마다 클라이언트가 일괄 reset을 받는다. 원인: gateway 종료 시 살아 있는 연결을 기다려주는 시간(`terminationDrainDuration`)이 기본 **5초**뿐이다. 수명이 긴 연결에겐 너무 짧다. ### 해결 drain을 늘리고, 동시에 죽는 pod 수를 제한하고(PDB), 연결 수명에 상한을 둔다. (P3의 `maxConnectionDuration`이 여기서도 일한다 — 수명 상한이 있으면 drain 시간 안에 자연 소멸된다) ### 상세 ```yaml # gateway Helm values 중 P4 관련 부분 (전체는 3장) podAnnotations: proxy.istio.io/config: | terminationDrainDuration: 300s # 기본 5s -> 300s proxyMetadata: EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true" # drain 중 연결 0이면 조기 종료 ``` ```yaml # PDB (gateway 차트가 안 만들어줌 — 별도 적용) apiVersion: policy/v1 kind: PodDisruptionBudget metadata: { name: egressgateway, namespace: istio-system } spec: minAvailable: 2 selector: { matchLabels: { istio: egressgateway } } ``` **확인**: 카나리 배포 중 `envoy_cluster_upstream_cx_destroy` 급증 여부와 클라이언트 에러율. --- ## 8. 적용 검증 — 확인할 pod를 헷갈리지 말 것 홉 1은 **클라이언트 sidecar**에서, 홉 2는 **gateway pod**에서 확인한다. 반대로 잡으면 영원히 "미적용"으로 보인다. 설정은 이 순서로 흐른다. 순서대로 확인하면 어디서 끊겼는지 바로 나온다: ```mermaid flowchart LR DR["DR"] --> XDS["istiod
(xDS)"] --> CL["Envoy
cluster"] --> SOCK["커널
소켓"] --> WIRE["와이어
probe"] ``` ### ① xDS — Envoy cluster에 도달했나 ```bash # 홉 1 — 클라이언트 sidecar: 풀 + ISTIO_MUTUAL + sni가 subset cluster에 붙었나 istioctl proxy-config cluster deploy/ -n \ --fqdn istio-egressgateway.istio-system.svc.cluster.local -o json | \ jq '.[] | select(.name | contains("|partner-a|")) | {name, dr: .metadata.filterMetadata.istio.config, max: .circuitBreakers.thresholds[0].maxConnections, ka: .upstreamConnectionOptions.tcpKeepalive, sni: .transportSocket.typedConfig.sni}' # 홉 2 — gateway pod: 풀은 붙고 transportSocket은 "없어야" 정상 (raw TCP의 표식) istioctl proxy-config cluster deploy/istio-egressgateway -n istio-system \ --fqdn api.partner-a.example.com -o json | \ jq '.[] | {name, max: .circuitBreakers.thresholds[0].maxConnections, ka: .upstreamConnectionOptions.tcpKeepalive, transportSocket: (.transportSocket // "ABSENT (raw TCP)")}' ``` 읽는 법: - `dr: null`이면 값 이전의 문제다. DR이 cluster에 아예 안 붙은 것이다. (host 불일치, exportTo, 중복 DR, pod를 잘못 잡음) - `max: 4294967295`면 connectionPool이 그 cluster에 안 닿은 것이다. (미설정 상한에 주입되는 값) - subset 줄만 `ka: null`이면 통째 교체 함정이다. subset에 tcpKeepalive를 다시 안 적은 것이다. ### ② 소켓 — 커널에 실제로 박혔나 DR의 keepalive는 sysctl이 아니라 소켓 옵션으로 들어간다. 그래서 `sysctl`로 확인하면 보는 곳이 틀린 거다. `ss -o`의 timer 필드를 본다: ```bash kubectl exec deploy/istio-egressgateway -n istio-system -- \ ss -tno state established 'dst <외부IP>' # 기대: timer:(keepalive,4min32sec,0) ← 300초에서 카운트다운 = time:300 적용 확정 # 미적용: timer가 없거나(SO_KEEPALIVE off), 2시간 근처(커널 기본 7200s) ``` ### ③ 경유 증명 — 200이 아니라 이게 완료 조건이다 VS 매칭이 어긋나면 sidecar가 gateway를 건너뛰고 직접 나간다 (`outboundTrafficPolicy: ALLOW_ANY` 기준). 호출은 200이라 멀쩡해 보이는데, 신원 확인은 한 겹도 안 된 상태다. 그래서 gateway access log로 경유를 증명해야 끝난 거다: ```bash kubectl -n istio-system logs deploy/istio-egressgateway --tail=5 | \ grep 'outbound|443||api.partner-a.example.com' ``` ### ④ (선택) 와이어 — probe가 실제로 나가나 ```bash # gw 노드에서: 유휴 300초 후 length 0 ACK → 상대의 즉시 ACK, 이후 300초 주기 반복 tcpdump -ni any host <외부IP> and port 443 ``` --- ## 9. 운영 — 장애 런북과 알람 ### 9.1 원인 구분 — 클라이언트 증상은 다 똑같다 풀 초과, 포트 고갈, 인가 거부, 설정 오류가 클라이언트에선 전부 "SSL_ERROR_SYSCALL / Connection reset"류로 보인다. 구분은 gateway 쪽 기록으로만 가능하다. 이 표가 없으면 정상 통제 동작을 장애로 오인한다: | gateway 쪽 기록 | 원인 | 대응 | |---|---|---| | access log flag `UO` | Envoy 연결 풀 초과 (P1) | maxConnections 재계산 | | access log flag `UF` | gw→외부 connect 실패 — 포트 고갈(P2) 또는 외부 장애 | `ss -tan state time-wait \| wc -l` 확인 | | rbac 로그 `denied` | AuthorizationPolicy 거부 — **정상 통제 동작** | 정책 확인 | | gw 로그 미도달 + sidecar에 `PassthroughCluster` | mesh 쪽 VS 미매칭 (sniHosts 오타, sidecar 미주입) | 정렬 체크리스트(3장) 대조 | | handshake 즉시 실패 | DR `sni` 누락/오타, 비-mesh 클라이언트 | 정렬 체크리스트 대조 | | 무응답 timeout (reset조차 없음) | conntrack 유실 — 대규모에서만 (부록 B) | 노드 dmesg + `conntrack -C` | | 유휴 후 첫 요청만 실패 | 방화벽이 유휴 연결을 이미 끊어둠 (P3) | keepalive.time 재점검 | | 정확히 N초(기본 1h)에 절단 | Envoy `idleTimeout` — keepalive로는 안 막힘 (P3) | idleTimeout 직접 설정 | 진단 순서: ① gw access log(거쳐갔나) → ② rbac 로그(왜 거부됐나) → ③ `istioctl proxy-config listeners`·`istioctl analyze`(설정 정합) → ④ `ss`·`conntrack`(커널). tcpdump는 최후 수단이다. ### 9.2 알람 (Prometheus) — 문제별 1:1 | 알람 | 조건 | 의미 | |---|---|---| | 연결 거부 | `rate(envoy_cluster_upstream_cx_overflow[5m]) > 0` | P1 발동 — 재계산 필요 | | 풀 사용률 | `upstream_cx_active / maxConnections > 0.8` | P1 선제 증설 신호 | | connect 실패 | `rate(envoy_cluster_upstream_cx_connect_fail[5m]) > 0` | P2 또는 외부 장애 | | TIME_WAIT | egress 노드 `node_sockstat_TCP_tw > 20000` | 연결 생성·소멸 과다 → **앱 keep-alive 캠페인** | | conntrack | `node_nf_conntrack_entries / limit > 0.7` | 노드 전역 자원 경계 신호 (부록 B) — 울리기 전엔 조치 불필요 | 주의 하나: `upstream_cx_overflow` 카운터는 mesh의 stats matcher 설정에 따라 노출되지 않을 수 있다. 실제로 T52 실측 환경에서 카운터가 안 보여서 access log의 `UO`로만 확인 가능했다. 알람을 걸기 전에 카운터가 노출되는지부터 확인한다. ### 9.3 그 밖의 운영 주의 4개 1. **maxConnections를 무한정 키우면 격벽이 사라진다.** 외부 기관 한 곳이 느려지면 그 cluster에 연결이 쌓이고, gateway 메모리가 마르면 다른 목적지까지 번진다. (메모리 부담 = 동시 연결 × 소켓 2개 × 버퍼 약 1MiB) 2. **재연결에 민감한 채널(전문망, long-lived 스트림)에는 시작값을 그대로 쓰면 안 된다.** `maxConnectionDuration`을 빼거나 늘리고, drain도 더 늘린다. 시작값은 일반 REST API 파트너 기준이다. 3. **도입 전에 기준선을 잰다.** 직접 나갈 때 대비 p99 연결 수립 시간, 초당 연결, TIME_WAIT 추이, gateway CPU. "gateway 때문에 느려졌다" 논쟁을 데이터로 끝내는 장치다. 4. **상대 기관 입장도 있다.** 파트너 서버가 보는 출발 IP가 몇 개로 줄어든다. 상대쪽 IP 기반 rate limit이나 이상탐지에 걸릴 수 있으니 미리 알려준다. 참고 둘: outlierDetection은 단일 IP 목적지에선 기본값(ejection 상한 10%)에 막혀 사실상 무력하다 — DNS가 IP를 여러 개 줄 때만 검토한다. connectTimeout은 HTTP 경로에선 Istio 기본 재시도 2회와 곱해져 최악 지연이 3배가 된다 — 이 패턴의 두 홉(TCP)에는 해당 없지만, 같은 값을 HTTP 경로에 재사용할 때는 같이 계산한다. --- ## 부록 A — 값을 조정하고 싶을 때 (측정과 계산) 시작값이 안 맞는 큰 채널은 3개를 측정해서 다시 계산한다. 측정은 gateway 도입 **전에** sidecar 메트릭에서: ``` ① 채널별 최대 동시 연결 + 초당 새 연결 istio_tcp_connections_opened_total{destination_service="<외부host>"} → 최대치, rate()로 conn/s ② 방화벽/중간장비의 유휴 제한 시간 — 네트워크팀 확인 (보통 30~60분) ③ SNAT 여부 — CNI 설정 확인 (5장) ``` ```mermaid flowchart LR I1["① 최대 동시 연결
예: 1,500"] -->|"× 2~3 여유"| O1["maxConnections
4096"] I2["① 초당 새 연결
예: 250"] -->|"÷ 470 + 이중화"| O2["replicaCount
3"] I3["② 방화벽 유휴 제한
예: 1800s"] -->|"× 1/3 이하"| O3["keepalive.time
300"] ``` `idleTimeout`은 채널의 최장 유휴 시간보다 길게 잡는다. (시작값 1800s) 채널 온보딩 시트 — 시트에는 값이 아니라 계산식을 적는다. 다음 사람이 숫자를 복사하는 사고를 막는 장치다: | 채널 | 최대 동시 연결 (측정) | conn/s (측정) | maxConnections (=최대×2~3) | 방화벽 유휴 제한 → keepalive.time (=1/3) | idleTimeout (최장 유휴+여유) | |---|---|---|---|---|---| | partner-a | 1,500 | 250 | 4096 | 1800s → 300 | 1800s | | partner-b | … | … | … | … | … | 온보딩 = "DR 1세트 추가 + 전역값 재검토". 전역(레이어 2)은 모든 채널의 conn/s 합으로 다시 계산한다. ## 부록 B — conntrack (노드 sysctl): 다채널 합산 규모에서만 일반 규모에선 손댈 필요 없다. 기본 한도 약 26만은 Envoy 1,024나 포트 28,000보다 훨씬 넓어서, P1·P2를 잡았다면 여기까지 안 온다. 다만 알람 하나(9.2절)는 걸어둔다. 이게 터지면 reset도 없이 조용히 패킷이 사라져서, 알람 없이는 진단이 제일 어렵다. - 계산식: 전 채널 합산 conn/s × 잔류 120초 = 상시 점유 엔트리 (예: 초당 250개 → 3만 ≈ 기본 한도의 12%) - 알람이 70%에 접근하면 그때 조치한다 (전용 egress 노드풀의 **노드** `/etc/sysctl.d` — conntrack은 패킷이 호스트 netns의 iptables/CNI를 지날 때 생기므로 pod sysctl이 아니다): ```bash # /etc/sysctl.d/90-egress-gw.conf net.netfilter.nf_conntrack_max = 1048576 # 기본 ~26만 net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30 # 기본 120s 단축 ``` ## 부록 C — 검증 상태: 무엇이 실측이고, 무엇을 적용 환경에서 재확인해야 하나 **실측 완료** (Istio 1.30.0 / k8s 1.30.6 — 원자료는 각 정본 문서의 검증 번들, "검증 기록" 절 참조): | 주장 | 방법 | |---|---| | connectionPool이 홉별로 다른 프록시에 적용 (홉1=sidecar, 홉2=gateway) | 식별값 111/222 주입 후 양쪽 cluster 덤프 | | 홉 2 cluster에 transportSocket 부재 = raw TCP, inner TLS end-to-end 보존 | E2E 호출 + config dump + 서버측 SNI/cert 검증 | | `tls.mode: tcp` 미존재 (CRD 거부) | apply 실측 | | keepalive probe가 Envoy idleTimeout을 리셋하지 못함 | 유휴 절단 실측 | | DR keepalive는 sysctl이 아니라 소켓 옵션 (`ss -o` timer로 검증) | 소켓 실측 | | Envoy cluster 기본 1024 / connectionPool 부분 설정 시 무제한 주입 | config dump 실측 | | subset/portLevelSettings 통째 교체 (deep-merge 아님) | config dump 실측 | | TIME_WAIT 60s (커널 하드코딩) → pod당·목적지당 ~470 conn/s | 커널 소스 + 실측 | **적용 환경에서 재확인 필요** — 이 표를 채우기 전에는 시작값을 그대로 운영값으로 확정하지 말 것: | 항목 | 확인 방법 | 값에 미치는 영향 | |---|---|---| | Istio 버전·설치 방식 | — | API·기본값 차이 (본 문서는 1.30 기준) | | 방화벽/중간장비 유휴 제한 시간 | 네트워크팀 | `keepalive.time`·`idleTimeout` 계산 입력 | | SNAT 여부 (CNI 설정) | CNI IPPool 설정 | antiAffinity 필수 여부, 방화벽 등록 단위, 포트 계산 전제 | | 채널별 최대 동시 연결·conn/s | 도입 전 sidecar 메트릭 | `maxConnections`·replica 수 | | kubelet unsafe sysctl 정책 | 클러스터 관리 주체 | `tcp_tw_reuse` 적용 가능 여부 | | keepalive ACK를 세션 갱신으로 인정 안 하는 중간장비 | 유휴 30분 후 첫 요청 실측 | 드물지만 존재 — 채널별 확인 | | P2·P4 로그 시그니처 (`UF`+`EADDRNOTAVAIL` · `upstream_cx_destroy` 급증) | 장애 시 gateway 로그·지표 | 직접 재현 실측 아님 — Envoy 공식 문서 기준 (부록 D) | | istiod `gateways.securityContext` 경유 sysctl의 최종 pod 반영 | istiod values 적용 + gateway 재기동 후 pod spec·`/proc` | T95는 injector configmap 렌더링까지 실측 — 클러스터 최종 반영은 적용 환경에서 확인 | ## 부록 D — 출처 (외부 공식 문서) - Envoy access log response flags (`UO`·`UF`의 정의): envoyproxy.io › configuration › observability › access_log › usage - Envoy circuit breaking 기본값(cluster당 1024): envoyproxy.io › intro › arch_overview › upstream › circuit_breaking - Envoy tcp_proxy idleTimeout 기본 1h: envoyproxy.io › api-v3 › tcp_proxy.proto - Istio DestinationRule 레퍼런스 (ConnectionPoolSettings·ClientTLSSettings): istio.io › docs › reference › config › networking › destination-rule - TCP keepalive 소켓 옵션: man 7 tcp (TCP_KEEPIDLE/KEEPINTVL/KEEPCNT) - 리눅스 ephemeral port 범위: docs.kernel.org › networking › ip-sysctl - k8s sysctl 정책 (safe/unsafe): kubernetes.io › tasks › administer-cluster › sysctl-cluster --- ## 검증 기록 (2026-07-06 · Istio 1.30.0 / k8s 1.30.6) 이 문서는 종합본이라 실측 대부분을 소스 정본에서 인용한다 — 부록 C "실측 완료" 표의 원본: | 인용 주장 | 원본 실측 | |---|---| | connectionPool 홉별 적용(식별값 111/222) · 홉 2 transportSocket 부재 · `tls.mode: tcp` 미존재 | [mTLS Passthrough — 홉별 DR·connectionPool 구성](/docs/istio/egress/mtls-passthrough-connectionpool/) T91 | | 이중 기본값(4294967295 명시 주입) · connectTimeout×retry 곱 | [DR connectionPool 정본](/docs/istio/egress/dr-connection-settings/) T93 | | keepalive ↔ 커널 소켓 옵션 1:1 매핑 · probe가 idleTimeout을 리셋 못함 | [tcpKeepalive 필드 노트](/docs/istio/egress/tcp-keepalive-fields/) T55·T16 | | subset/portLevelSettings 통째 교체(deep-merge 아님) | [DestinationRule 만들기](/docs/istio/egress/destinationrule-fundamentals/) T53 | | Envoy cluster 기본 1024 발동(UO) | [circuit breaking 메커니즘](/docs/istio/egress/circuit-breaking-mechanisms/) T52 | | TIME_WAIT 60s(커널 하드코딩) → ~470 conn/s | [Egress TCP 병목 정본](/docs/istio/egress/tcp-bottlenecks/) T57 | 이 문서 자체 실측 — **T94**: istio/gateway Helm chart 1.30.0의 pod securityContext 동작 (차트 렌더링 검증, 클러스터 불요). 원자료: [run.sh](files/verify/T94/run.sh) · [result.txt](files/verify/T94/result.txt) · [verdict.json](files/verify/T94/verdict.json) | # | 주장 | 실측 결과 | 판정 | |---|---|---|---| | C1 | 차트가 pod-level `securityContext` value 노출 (기본 `{}`) | `helm show values`에 존재 — 주석: "미설정 시 80/443 바인딩 최소 권한 자동 설정" | ✅ 지지 | | C2 | 미설정 시 `sysctls: [net.ipv4.ip_unprivileged_port_start=0]` 자동 주입 | 기본 `helm template` 렌더링에서 해당 블록 확인 | ✅ 지지 | | C3 | values 지정 시 자동 기본이 **통째 대체** (merge 아님) | `tcp_tw_reuse`만 지정해 렌더링 → `ip_unprivileged_port_start` 소멸 | ✅ 지지 | 이 문서 자체 실측 — **T95**: gateway 차트 values의 sysctls가 sidecar injector(gateway 주입 템플릿)에 의해 최종 pod에서 덮어써지는 문제 (클러스터 실측, 격리 ns `istio-vt-t95`). 원자료: [run.sh](files/verify/T95/run.sh) · [result.txt](files/verify/T95/result.txt) · [verdict.json](files/verify/T95/verdict.json) · [gateway values](files/verify/T95/t95-gateway-values.yaml) · [istiod fix values](files/verify/T95/t95-istiod-fix-values.yaml) | # | 주장 | 실측 결과 | 판정 | |---|---|---|---| | C1 | 차트 values의 sysctls는 Deployment까지 렌더링됨 (T94 재확인) | Deployment pod template에 sysctl 2종(`ip_unprivileged_port_start`·`ip_local_port_range`) 존재 | ✅ 지지 | | C2 | 주입 webhook이 pod-level securityContext를 통째 교체 → values의 sysctls 소실 | 최종 pod에 `ip_unprivileged_port_start=0`만 잔존, `/proc`의 ip_local_port_range = 32768 60999(커널 기본값). 에러·이벤트 없음 | ✅ 지지 | | C3 | 실효 주입 지점 = istiod values `gateways.securityContext` | istiod 차트 렌더링에서 injector configmap `data.values` 전달·템플릿 분기 사용 확인. 클러스터 최종 반영은 미실행(컨트롤플레인 변경 승인 필요) | ⚠️ 렌더링 수준 지지 | --- ## 참조 **아카이브 내부 — 각 주제의 정본** - [mTLS Passthrough — 홉별 DR·connectionPool 구성](/docs/istio/egress/mtls-passthrough-connectionpool/) — 3장 레이어 1·4장의 정본 (T91) - [DR connectionPool 정본 — 필드별 동작·매핑·관측](/docs/istio/egress/dr-connection-settings/) — 필드 12종 상세·이중 기본값·retry 곱 (T93) - [tcpKeepalive 필드 노트 — time·interval·probes](/docs/istio/egress/tcp-keepalive-fields/) — 6장·8장 검증의 정본 (T55·T16) - [Egress TCP 문제별 처방전](/docs/istio/egress/tcp-tuning/) — 4~7장 문제 중심 구조·부록 A 계산식의 정본 - [Egress TCP 병목 정본](/docs/istio/egress/tcp-bottlenecks/) — 5장 포트 계산·SNAT·런북·알람의 정본 (T57) - [Egress mTLS Passthrough 해부](/docs/istio/egress/https-over-mtls/) — 1장 패턴 자체의 정본 (T07) - [Egress HTTPS 패턴 지도](/docs/istio/egress/https-pattern-map/) — 용어 정본 - [Pod 커널 파라미터 정본](/docs/istio/egress/pod-sysctl-netns/) — 5장 sysctl netns 초기화·safe/unsafe 관문의 정본