homelab89 Docs Logs Legacy Files ☰ TOC 🌓
guideistio 2026-07-02istioegresstcptuning

Egress TCP 문제별 처방전 — 어떤 문제에, 어떤 설정을, 어디에, 어떻게

ABSTRACT

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. 준비 — 문제 지도와 측정 입력

문제가 어디서 터지고, 설정이 어디에 붙는가

flowchart LR
  APP["app pod"] --> SC["sidecar<br/>P1': DR subset (layer 2)"]
  SC --> GW["egress gw pod<br/>P1: Envoy 1024 -> DR (layer 1)<br/>P2: port 28k -> Helm+pod sysctl (layer 3)<br/>P5: drain 5s -> Helm (layer 3)"]
  GW --> NODE["gw node<br/>P3: conntrack -> node sysctl (layer 4)"]
  NODE --> FW["firewall<br/>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)

측정 — 값은 감이 아니라 세 입력에서 나온다

# ① 채널별 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.

flowchart LR
  I1["peak 1,500"] -- "x 2~3 headroom" --> O1["maxConnections: 4096"]
  I2["250 conn/s"] -- "/ 470 per pod, + HA" --> O2["replicaCount: 3<br/>+ 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만 고치면 병목이 안쪽으로 한 칸 이동할 뿐.

# (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 idleTimeoutkeepalive를 넣어도 안 막힌다

어떤 설정: 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. 채널 온보딩 시트 — 반복의 표준화

flowchart LR
  N["new channel"] --> M["measure<br/>peak conn, conn/s"] --> D["add layer 1+2<br/>(DR 1 set)"] --> G["recompute global<br/>sum conn/s -> replica, conntrack"] --> A["watch alerts<br/>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 런북이 본체.

참조

아카이브 내부

외부

Files