Egress TCP 문제별 처방전 — 어떤 문제에, 어떤 설정을, 어디에, 어떻게
TCP 병목 정본이 "왜 병목이 생기는가"의 정본이라면, 이 문서는 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 병목 정본(산술·메커니즘), Pod 커널 파라미터 정본(sysctl 관문), tcpKeepalive 필드 노트(time/interval/probes).
00. 준비 — 문제 지도와 측정 입력
문제가 어디서 터지고, 설정이 어디에 붙는가
| 문제 | 한 줄 증상 | 설정 위치 (레이어) |
|---|---|---|
| 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) |
측정 — 값은 감이 아니라 세 입력에서 나온다
# ① 채널별 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.
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만 고치면 병목이 안쪽으로 한 칸 이동할 뿐.
# (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
# (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당" 상한이라 보통 여유
검증:
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).
어떤 설정 (우선순위 순): ① 앱 keep-alive(분자 축소 — 유일한 근본 처방, YAML 아님) → ② replica + antiAffinity(분모 확대) → ③ pod sysctl(포트 풀 확장 + 60초 규칙 완화)
어디에: Gateway Helm values (레이어 3). natOutgoing: true면 같은 노드의 gw pod들이 노드 IP 포트 공간을 공유하므로 antiAffinity가 권장이 아니라 필수.
# 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 커널 파라미터 정본.
검증:
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를 통과할 때 일어난다 (스코프 맵).
# /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% 알람이 정답.
검증:
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 필드 노트의 본체).
어디에: 외부 호스트 DR (레이어 1).
# 외부 호스트 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 타입 미확인 채널은 재현 랩에서 실측.
P5. 배포할 때마다 long-lived 연결이 잘린다 — drain 5초
증상: 재배포/스케일인 시점마다 클라이언트 일괄 reset. 원인은 terminationDrainDuration 기본 5초 — long-lived 연결에겐 사형 선고 시간.
어떤 설정: drain 연장 + 조기 종료 조건 + PDB + (P4의 maxConnectionDuration이 여기서도 일함 — 수명 상한이 있으면 drain 안에 자연 소멸)
어디에: Helm podAnnotations + 별도 PDB (레이어 3).
podAnnotations:
proxy.istio.io/config: |
terminationDrainDuration: 300s # 5s -> 300s
proxyMetadata:
EXIT_ON_ZERO_ACTIVE_CONNECTIONS: "true" # drain 중 연결 0이면 조기 종료.
# 부작용 있음(istio#50596) — 정본 참조
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 메커니즘은 운영 정본.
09. 전체 YAML 종합본 — P1~P5를 4개 레이어로 합치면
위 처방을 리소스 단위로 재조립한 완성본. 채널당(레이어 1·2) / 전역(레이어 3·4) 구분이 운영 단위다.
레이어 1 — 외부 호스트 DR (채널당 1벌) — P1 + P4
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
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
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
# /etc/sysctl.d/90-egress-gw.conf
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
10. 채널 온보딩 시트 — 반복의 표준화
| 채널 | 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 런북이 본체.
참조
아카이브 내부 - TCP 병목 정본 — 병목 5종의 산술·순서·런북·알람. 이 문서 모든 도출식의 출처 - Pod 커널 파라미터 정본 — P2 sysctl의 netns 메커니즘·3중 관문·실패 모드 - tcpKeepalive 필드 노트 — P4의 time/interval/probes 커널 매핑과 두 역할 - TCP 장애 재현 랩 — P1~P4를 한계 축소로 kind에서 재현 - Egress 운영 정본 — P5 drain 메커니즘·모니터링 일반론
외부 - Envoy circuit breaking 기본값 — cluster당 max_connections 1024의 출처 - istio/istio#50596 — EXIT_ON_ZERO_ACTIVE_CONNECTIONS abort 사례