---
type: guide
tags: [istio, egress, tcp, connection-pool, destinationrule, helm, sysctl, keepalive, capacity-planning, runbook]
created: 2026-07-02
---
# Egress TCP 문제별 처방전 — 어떤 문제에, 어떤 설정을, 어디에, 어떻게
> [!abstract]
> [TCP 병목 정본](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 병목 정본](ref__src-tcp-bottlenecks.html)(산술·메커니즘), [Pod 커널 파라미터 정본](cfg__guide-pod-sysctl-netns.html)(sysctl 관문), [tcpKeepalive 필드 노트](ref__note-tcp-keepalive-fields.html)(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](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 커널 파라미터 정본](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를 통과할 때 일어난다 ([스코프 맵](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 필드 노트](ref__note-tcp-keepalive-fields.html)의 본체).
**어디에**: 외부 호스트 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 타입 미확인 채널은 [재현 랩](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 메커니즘은 [운영 정본](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 런북](ref__src-tcp-bottlenecks.html)이 본체.
---
## 참조
**아카이브 내부**
- [TCP 병목 정본](ref__src-tcp-bottlenecks.html) — 병목 5종의 산술·순서·런북·알람. 이 문서 모든 도출식의 출처
- [Pod 커널 파라미터 정본](cfg__guide-pod-sysctl-netns.html) — P2 sysctl의 netns 메커니즘·3중 관문·실패 모드
- [tcpKeepalive 필드 노트](ref__note-tcp-keepalive-fields.html) — P4의 time/interval/probes 커널 매핑과 두 역할
- [TCP 장애 재현 랩](cfg__guide-tcp-failure-reproduction.html) — P1~P4를 한계 축소로 kind에서 재현
- [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 사례