--- title: Egress TCP 문제별 처방전 — 어떤 문제에, 어떤 설정을, 어디에, 어떻게 date: 2026-07-02 type: guide domain: istio tags: [istio, egress, tcp, tuning] --- > [!abstract] > [TCP 병목 정본](/public/istio-egress/ref__src-tcp-bottlenecks.html)이 "왜 병목이 생기는가"의 정본이라면, 이 문서는 > **egressgateway에서 실제로 발생하는 TCP 문제 5가지를 하나씩 놓고 — 증상 → 어떤 설정을 → 어디에(리소스) → > 어떻게(YAML) → 검증 — 순서로 처방하는 실행 문서**다. 예시 채널 하나 > (peak 동시 연결 1,500 / 신규 250 conn/s / FW idle timeout 30분 / Calico natOutgoing on)의 실측값으로 > 모든 숫자를 도출하고, 마지막에 4개 레이어 **전체 YAML 종합본**을 둔다. > 값을 복사하지 말고 도출식을 복사할 것. **대상 환경**: Istio 1.30 sidecar mode, egress gateway = `tcp_proxy`(PASSTHROUGH 또는 ISTIO_MUTUAL). **선행 문서**: [TCP 병목 정본](/public/istio-egress/ref__src-tcp-bottlenecks.html)(산술·메커니즘), [Pod 커널 파라미터 정본](/public/istio-egress/cfg__guide-pod-sysctl-netns.html)(sysctl 관문), [tcpKeepalive 필드 노트](/docs/istio/tcp-keepalive-fields/)(time/interval/probes). --- ## 00. 준비 — 문제 지도와 측정 입력 ### 문제가 어디서 터지고, 설정이 어디에 붙는가 ```mermaid flowchart LR APP["app pod"] --> SC["sidecar
P1': DR subset (layer 2)"] SC --> GW["egress gw pod
P1: Envoy 1024 -> DR (layer 1)
P2: port 28k -> Helm+pod sysctl (layer 3)
P5: drain 5s -> Helm (layer 3)"] GW --> NODE["gw node
P3: conntrack -> node sysctl (layer 4)"] NODE --> FW["firewall
P4: idle timeout -> DR keepalive (layer 1)"] FW --> EXT["partner-a:443"] ``` | 문제 | 한 줄 증상 | 설정 위치 (레이어) | |---|---|---| | **P1** 연결 거부 | 1,024개에서 즉시 거부, flag `UO` | DR 외부 호스트 (1) + DR subset (2) | | **P2** 포트 고갈 | connect 실패, flag `UF`, `EADDRNOTAVAIL` | Helm replica·antiAffinity·pod sysctl (3) | | **P3** 무응답 timeout | reset조차 없는 silent drop | 노드 `/etc/sysctl.d` (4) | | **P4** 유휴 후 절단 | half-open RST / 정확히 N초 절단 | DR keepalive + idleTimeout (1) | | **P5** 배포 시 절단 | 재배포마다 long-lived 연결 일괄 사망 | Helm drain + PDB (3) | ### 측정 — 값은 감이 아니라 세 입력에서 나온다 ```bash # ① 채널별 peak 동시 연결 + 신규 연결률 — gateway 도입 "전" sidecar 메트릭에서 istio_tcp_connections_opened_total{destination_service="api.partner-a.example.com"} # → peak 동시 연결 수, rate()로 conn/s # ② FW/중간장비 idle timeout — 네트워크팀 확인 (보통 30~60분) # ③ SNAT 여부 — 켜져 있으면 포트 산술의 전제가 바뀐다 (정본 §05) calicoctl get ippool -o yaml | grep natOutgoing ``` **예시 시나리오** (이하 모든 값의 입력): `api.partner-a.example.com:443`, peak **1,500**, 신규 **250 conn/s**, FW idle **1800s**, `natOutgoing: true`. ```mermaid flowchart LR I1["peak 1,500"] -- "x 2~3 headroom" --> O1["maxConnections: 4096"] I2["250 conn/s"] -- "/ 470 per pod, + HA" --> O2["replicaCount: 3
+ required antiAffinity"] I3["FW idle 1800s"] -- "x 1/3 or less" --> O3["tcpKeepalive.time: 300"] I4["natOutgoing: on"] -- "node IP port space shared" --> O2 ``` --- ## P1. 연결이 1,024개에서 거부된다 — Envoy cluster 상한 **증상**: 클라이언트는 reset류(`SSL_ERROR_SYSCALL`), gateway access log에 flag **`UO`**, `envoy_cluster_upstream_cx_overflow` 증가. sidecar 시절엔 절대 안 보이던 벽 — pod 하나가 한 목적지로 동시 1,024개를 열 일이 없다가, gateway에서 **전사 트래픽이 cluster 하나로 합쳐지며** 가장 먼저 부딪힌다. **어떤 설정**: `connectionPool.tcp.maxConnections` (+ `connectTimeout`) **어디에**: ① 외부 호스트 DR — 채널당 1벌. ② **sidecar→gw subset에도 같은 기본값 1,024가 숨어 있다** — 외부 DR만 고치면 병목이 안쪽으로 한 칸 이동할 뿐. ```yaml # (1) 외부 호스트 DR — 핵심 완화 지점 apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: { name: partner-a-external, namespace: istio-egress } spec: host: api.partner-a.example.com trafficPolicy: connectionPool: tcp: maxConnections: 4096 # 측정 peak 1,500 x 2~3. 무한정 키우면 격벽 상실 — connectTimeout: 3s # 메모리 노출 = 동시연결 x 소켓2 x ~1MiB buffer ``` ```yaml # (2) 기존 egressgateway DR의 subset — 이 hop의 cluster에도 명시 subsets: - name: partner-a trafficPolicy: portLevelSettings: - port: { number: 443 } tls: { mode: ISTIO_MUTUAL, sni: api.partner-a.example.com } connectionPool: tcp: { maxConnections: 4096 } # 단, "클라이언트 pod당" 상한이라 보통 여유 ``` **검증**: ```bash rate(envoy_cluster_upstream_cx_overflow[5m]) == 0 # UO 거부 없어야 함 envoy_cluster_upstream_cx_active / 4096 < 0.8 # 0.8 초과 = 선제 증설 신호 ``` --- ## P2. 포트가 고갈된다 — ephemeral port + TIME_WAIT **증상**: gw→외부 connect 실패, flag **`UF`**, gw 로그에 `EADDRNOTAVAIL`. 산술 한계: pod당·목적지당 동시 ~28k, 신규 **~470 conn/s**(= 28,232 ports ÷ TIME_WAIT 60s — [정본 §02](/public/istio-egress/ref__src-tcp-bottlenecks.html)). **어떤 설정** (우선순위 순): ① 앱 keep-alive(분자 축소 — 유일한 근본 처방, YAML 아님) → ② replica + antiAffinity(분모 확대) → ③ pod sysctl(포트 풀 확장 + 60초 규칙 완화) **어디에**: Gateway Helm values (레이어 3). `natOutgoing: true`면 같은 노드의 gw pod들이 **노드 IP 포트 공간을 공유**하므로 antiAffinity가 권장이 아니라 **필수**. ```yaml # Gateway Helm values 발췌 replicaCount: 3 # 250÷470≈0.5 → 산술상 1대지만 HA+여유율로 3 affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: # SNAT 환경: required 필수 - labelSelector: { matchLabels: { istio: egressgateway } } topologyKey: kubernetes.io/hostname securityContext: # pod-level sysctl sysctls: - { name: net.ipv4.ip_local_port_range, value: "10240 60999" } # safe: 28k -> 50k - { name: net.ipv4.tcp_tw_reuse, value: "1" } # unsafe: 아래 관문 선행 필수 ``` `tcp_tw_reuse=1`은 선언만으로 안 먹는다 — **kubelet `allowedUnsafeSysctls`(노드) + PSA 라벨(네임스페이스)** 두 관문이 선행돼야 하고, 빠뜨리면 pod가 `SysctlForbidden`으로 뜬다. netns 초기화 메커니즘·관문 절차·실패 모드 전체는 [Pod 커널 파라미터 정본](/public/istio-egress/cfg__guide-pod-sysctl-netns.html). **검증**: ```bash kubectl -n istio-egress exec deploy/istio-egressgateway -- \ cat /proc/sys/net/ipv4/tcp_tw_reuse /proc/sys/net/ipv4/ip_local_port_range # 1 / 10240 60999 ss -tan state time-wait | wc -l # 20,000 초과 = 앱 keep-alive 캠페인 신호 ``` --- ## P3. 무응답 timeout — conntrack silent drop **증상**: 거부도 reset도 없이 **무응답**. 노드 dmesg에 `nf_conntrack: table full, dropping packet`. 병목 중 유일하게 "조용히" 죽는 놈이라 진단이 가장 어렵다. **어떤 설정**: `nf_conntrack_max` 상향 + TIME_WAIT류 잔류 단축 **어디에**: **노드** `/etc/sysctl.d` (전용 egress 노드풀). pod가 아닌 노드인 이유 — pod netns에는 netfilter 룰이 없고, tracking은 패킷이 호스트 netns의 iptables/Calico를 통과할 때 일어난다 ([스코프 맵](/public/istio-egress/cfg__guide-pod-sysctl-netns.html)). ```bash # /etc/sysctl.d/90-egress-gw.conf (전용 egress 노드풀에만) net.netfilter.nf_conntrack_max = 1048576 net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30 # 기본 120s 단축 # 적용: sudo sysctl --system ``` 산정: 250 conn/s × 잔류 120s ≈ 3만 엔트리 상시 점유 — 기본 26만의 12%라 당장은 여유지만 채널 10개면 위험 구간. 선제 상향 + 70% 알람이 정답. **검증**: ```bash conntrack -C # 현재 엔트리 cat /proc/sys/net/netfilter/nf_conntrack_max # 1048576 반영 확인 # 알람: node_nf_conntrack_entries / limit > 0.7 ``` --- ## P4. 유휴 후 첫 요청이 실패한다 / 정확히 N초에 끊긴다 — FW half-open과 Envoy idleTimeout **증상 두 갈래** — 원인이 다르므로 처방도 다르다: - **유휴 후 첫 송신에서 RST/timeout**: FW가 유휴 세션을 조용히 버려 half-open 상태 (트래픽 적은 채널에서 먼저 발현 — 바쁜 채널은 유휴해질 틈이 없다) - **정확히 N초(기본 1h)에 절단**: Envoy `idleTimeout` — **keepalive를 넣어도 안 막힌다** **어떤 설정**: `tcpKeepalive`(FW 대응) + `idleTimeout`(Envoy 자체) — **역할이 다른 두 값을 세트로**. keepalive probe는 데이터가 아니라서 FW 세션 타이머는 갱신하지만 Envoy idle 타이머는 리셋하지 못한다 ([keepalive 필드 노트](/docs/istio/tcp-keepalive-fields/)의 본체). **어디에**: 외부 호스트 DR (레이어 1). ```yaml # 외부 호스트 DR에 추가 connectionPool: tcp: idleTimeout: 1800s # Envoy 자체 유휴 절단: 채널 최장 유휴보다 길게 직접 설정 maxConnectionDuration: 3600s # 수명 상한 = scale-out 후 재분배 유도 # (재연결 민감 채널은 제외하거나 길게) tcpKeepalive: time: 300 # FW idle(1800s)의 1/3 이하 — probe 유실 1~2회 견디고도 세션 갱신 interval: 30 # 무응답 재시도 간격 probes: 3 # 30x3=90s 안에 죽은 상대 판정 → 유령 연결이 풀 슬롯 점유 방지 ``` **검증**: 유휴 30분 방치 후 첫 요청 실측(half-open 소멸 확인) + "정시 절단" 재발 여부. keepalive ACK를 세션 갱신으로 안 쳐주는 중간장비가 드물게 있으니 FW 타입 미확인 채널은 [재현 랩](/public/istio-egress/cfg__guide-tcp-failure-reproduction.html)에서 실측. --- ## P5. 배포할 때마다 long-lived 연결이 잘린다 — drain 5초 **증상**: 재배포/스케일인 시점마다 클라이언트 일괄 reset. 원인은 `terminationDrainDuration` 기본 **5초** — long-lived 연결에겐 사형 선고 시간. **어떤 설정**: drain 연장 + 조기 종료 조건 + PDB + (P4의 `maxConnectionDuration`이 여기서도 일함 — 수명 상한이 있으면 drain 안에 자연 소멸) **어디에**: Helm `podAnnotations` + 별도 PDB (레이어 3). ```yaml podAnnotations: proxy.istio.io/config: | terminationDrainDuration: 300s # 5s -> 300s proxyMetadata: EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true" # drain 중 연결 0이면 조기 종료. # 부작용 있음(istio#50596) — 정본 참조 ``` ```yaml apiVersion: policy/v1 kind: PodDisruptionBudget metadata: { name: egressgateway, namespace: istio-egress } spec: minAvailable: 2 # replica 3 중 상시 2대 보장 selector: { matchLabels: { istio: egressgateway } } ``` **검증**: 카나리 배포 중 `envoy_cluster_upstream_cx_destroy` 급증 여부, 클라이언트 에러율. 상세 drain 메커니즘은 [운영 정본](/public/istio-egress/ref__src-operations.html). --- ## 09. 전체 YAML 종합본 — P1~P5를 4개 레이어로 합치면 위 처방을 리소스 단위로 재조립한 완성본. **채널당(레이어 1·2) / 전역(레이어 3·4)** 구분이 운영 단위다. ### 레이어 1 — 외부 호스트 DR (채널당 1벌) — P1 + P4 ```yaml apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: { name: partner-a-external, namespace: istio-egress } spec: host: api.partner-a.example.com trafficPolicy: connectionPool: tcp: maxConnections: 4096 # P1 connectTimeout: 3s # P1(부속): 외부 장애 시 빠른 실패 idleTimeout: 1800s # P4 maxConnectionDuration: 3600s # P4/P5 tcpKeepalive: { time: 300, interval: 30, probes: 3 } # P4 ``` ### 레이어 2 — sidecar→gw subset DR (채널당 subset 1개) — P1 ```yaml subsets: - name: partner-a trafficPolicy: portLevelSettings: - port: { number: 443 } tls: { mode: ISTIO_MUTUAL, sni: api.partner-a.example.com } connectionPool: tcp: maxConnections: 4096 tcpKeepalive: { time: 300, interval: 30, probes: 3 } ``` ### 레이어 3 — Gateway Helm values (전역 1벌) — P2 + P5 ```yaml replicaCount: 3 autoscaling: enabled: true minReplicas: 3 maxReplicas: 6 targetCPUUtilizationPercentage: 70 # 연결 중심 부하엔 CPU 상관 약함 — # upstream_cx_active 기반 custom metric 권장 affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: { matchLabels: { istio: egressgateway } } topologyKey: kubernetes.io/hostname nodeSelector: { node-role/egress: "true" } # 전용 노드풀 = FW 등록 src IP 안정화 tolerations: - { key: node-role/egress, operator: Exists } securityContext: sysctls: - { name: net.ipv4.ip_local_port_range, value: "10240 60999" } - { name: net.ipv4.tcp_tw_reuse, value: "1" } podAnnotations: proxy.istio.io/config: | terminationDrainDuration: 300s proxyMetadata: EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true" service: type: ClusterIP ports: - { name: status-port, port: 15021, targetPort: 15021 } - { name: tls-egress, port: 443, targetPort: 8443 } ``` (+ P5의 PDB, P2의 kubelet `allowedUnsafeSysctls`·PSA 라벨은 위 각 절 참조) ### 레이어 4 — 노드 sysctl (전역 1벌) — P3 ```bash # /etc/sysctl.d/90-egress-gw.conf net.netfilter.nf_conntrack_max = 1048576 net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30 ``` --- ## 10. 채널 온보딩 시트 — 반복의 표준화 ```mermaid flowchart LR N["new channel"] --> M["measure
peak conn, conn/s"] --> D["add layer 1+2
(DR 1 set)"] --> G["recompute global
sum conn/s -> replica, conntrack"] --> A["watch alerts
UO=0, pool<0.8"] --> N ``` | 채널 | peak conn (측정) | conn/s (측정) | maxConnections (=peak×2~3) | FW idle → keepalive.time (=1/3) | idleTimeout (최장 유휴+α) | |---|---|---|---|---|---| | partner-a | 1,500 | 250 | 4096 | 1800s → 300 | 1800s | | partner-b | … | … | … | … | … | 전역값(레이어 3·4)은 **모든 채널의 conn/s 합**으로 재계산. 온보딩 = "DR 1벌 추가 + 전역 분수식 재검토" — 시트에는 값이 아니라 **도출식**을 적는다. 다음 사람이 숫자를 복사하는 사고를 막는 장치다. --- ## 핵심 정리 — 문제 → 설정 → 위치 한 장 | 문제 | 시그니처 | 설정 | 위치 | |---|---|---|---| | P1 연결 거부 | `UO`, cx_overflow | `maxConnections: peak×2~3` | DR 외부 호스트 + subset | | P2 포트 고갈 | `UF`, EADDRNOTAVAIL | 앱 keep-alive > replica+antiAffinity > pod sysctl | Helm (+kubelet/PSA 관문) | | P3 silent drop | 무응답, dmesg table full | conntrack_max 1M, tw timeout 30 | 노드 sysctl.d | | P4 유휴 절단 | 유휴 후 RST / 정시 절단 | keepalive(FW용) + idleTimeout(Envoy용) 세트 | DR 외부 호스트 | | P5 배포 절단 | 재배포 시 일괄 reset | drain 300s + PDB + 수명 상한 | Helm + PDB | --- ## What you might be missing - **maxConnections를 키우는 것과 격벽을 유지하는 것은 상충한다.** 외부 기관 1곳 지연 → cluster 적체 → gw 메모리/FD 소진 → 타 목적지 전이. peak×2~3은 이 트레이드오프의 절충점이지 클수록 좋은 값이 아니다. - **가장 싼 처방은 이 문서의 어떤 YAML도 아니고 앱 keep-alive다.** 분수식의 분자를 줄이는 유일한 방법. TIME_WAIT 알람이 울리면 인프라 튜닝 전에 호출 라이브러리의 connection reuse부터. - **도입 전 baseline 고정**: 직접 egress 대비 p99 연결수립시간·conn/s·TIME_WAIT 곡선·gw CPU/conn. "gateway 때문에 느려졌다" 논쟁을 데이터로 끝내는 장치. - **이 값을 그대로 복사하면 안 되는 채널**: 재연결 민감한 전문망·long-lived 스트림은 `maxConnectionDuration` 제외/연장, drain 추가 연장. 예시값은 "일반 REST API 파트너" 프로파일이다. - **reset류 증상은 P1·P2·P4와 AuthorizationPolicy 거부가 클라이언트에서 똑같이 보인다** — 원인 분기는 gateway 쪽 시그니처(`UO`/`UF`/rbac/무응답)로만 가능. [정본 §07 런북](/public/istio-egress/ref__src-tcp-bottlenecks.html)이 본체. --- ## 참조 **아카이브 내부** - [TCP 병목 정본](/public/istio-egress/ref__src-tcp-bottlenecks.html) — 병목 5종의 산술·순서·런북·알람. 이 문서 모든 도출식의 출처 - [Pod 커널 파라미터 정본](/public/istio-egress/cfg__guide-pod-sysctl-netns.html) — P2 sysctl의 netns 메커니즘·3중 관문·실패 모드 - [tcpKeepalive 필드 노트](/docs/istio/tcp-keepalive-fields/) — P4의 time/interval/probes 커널 매핑과 두 역할 - [TCP 장애 재현 랩](/public/istio-egress/cfg__guide-tcp-failure-reproduction.html) — P1~P4를 한계 축소로 kind에서 재현 - [Egress 운영 정본](/public/istio-egress/ref__src-operations.html) — P5 drain 메커니즘·모니터링 일반론 **외부** - [Envoy circuit breaking 기본값](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/circuit_breaking) — cluster당 max_connections 1024의 출처 - [istio/istio#50596](https://github.com/istio/istio/issues/50596) — EXIT_ON_ZERO_ACTIVE_CONNECTIONS abort 사례