--- type: guide tags: [istio, egress, tcp, reproduction, lab, connection-pool, port-exhaustion, conntrack, how-to] created: 2026-06-10 --- # Egress TCP 병목 재현 랩 — 한계를 줄여서 부딪힌다 > [!abstract] > [TCP 병목 정본](ref__src-tcp-bottlenecks.html)의 한계 수치(Envoy 1024, 포트 28k, > conntrack 26만)는 그대로 재현하려면 연결 수만 개가 필요하다. 이 랩의 기법은 반대다 — > **한계를 5~20으로 줄여서, 같은 메커니즘을 연결 수십 개로 관찰한다.** 재현 4종 각각이 > 운영에서 만날 실패 시그니처(`UO` / `UF` / 무응답 / 정시 절단) 하나씩을 직접 떠올리고, > 그 시그니처 구분이 곧 reset 분기 런북이 된다. **대상 환경**: Istio 1.30.0, [Egress 신원 기반 통제 구성](id__guide-mtls-identity-control.html)의 테스트 클러스터(ns `istio-egress`의 egressgateway + ns `egress-test`의 netshoot-a)가 떠 있는 상태. **대상 독자**: 도입 보고서의 병목 표를 "봤다"에서 "겪었다"로 바꾸고 싶은 사람. **범위**: 병목 1·2·3·4 재현 + 복구. 완화 운영값 자체는 [정본 §06](ref__src-tcp-bottlenecks.html)으로 위임. --- ## 0. 원칙 — 왜 줄여서 부딪히나 > **28,000개 연결을 만들어 한계에 도달하는 게 아니라, 한계를 5~20으로 줄여 같은 메커니즘을 소규모로 관찰한다.** ``` [운영 한계] [랩 재현] maxConnections: 1024(기본) maxConnections: 5 ephemeral ports: 28,232 ip_local_port_range: 20개 nf_conntrack_max: ~260k nf_conntrack_max: 200 idle timeout(FW): 30~60min idleTimeout: 30s | | +--- 같은 메커니즘, 같은 시그니처 ---+ ``` 공유 테스트 클러스터에서 안전하고, 외부 sandbox 엔드포인트에 가는 부하도 연결 수십 개 수준이라 무해하다. (그래도 대상 LB에 이상탐지가 있다면 사전 공유 권장.) 비유 하나: 둑의 높이를 1m로 낮추고 물 한 양동이로 범람을 관찰하는 것. **한계: 비율이 다른 현상은 못 본다** — 예컨대 연결 수만 개 규모에서만 나타나는 Envoy 메모리 압박·FD 고갈은 이 기법으로 재현되지 않는다. --- ## 1. 관찰 도구 준비 ```bash # gw pod에 디버그 컨테이너 부착 (ss, conntrack 등 사용) GW=$(kubectl get pod -n istio-egress -l istio=egressgateway -o jsonpath='{.items[0].metadata.name}') kubectl debug -n istio-egress $GW -it --image=registry.example.com/netshoot:latest \ --target=istio-proxy -- bash # 이 셸에서: ss -tan state time-wait | wc -l 등 실행 # Envoy 통계 조회 (별도 터미널, 반복 사용할 함수) stats() { kubectl exec -n istio-egress deploy/egressgateway -c istio-proxy -- \ pilot-agent request GET stats | grep "api-a" | grep -E "$1" } # 부하 발생용 alias A="kubectl exec -n egress-test deploy/netshoot-a -c netshoot --" ``` `kubectl debug --target=istio-proxy`로 붙이는 이유: ephemeral 컨테이너가 **istio-proxy와 같은 프로세스/네트워크 네임스페이스를 공유**해야 그 pod의 소켓(`ss`)이 보인다. --- ## 2. 재현 1 — Envoy cluster 연결 상한 (운영 기본 1024 → 5) 부딪히는 순서 1번, 가장 먼저 맞을 함정. 상한을 5로 줄이고 10개 연결을 시도한다. ```yaml # dr-api-a-tiny.yaml — 외부 호스트용 DR. gw의 upstream cluster에 적용됨 apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: api-a-external namespace: istio-egress spec: host: api-a.example.com trafficPolicy: connectionPool: tcp: maxConnections: 5 # 재현용. 미설정 시 기본 1024 ``` ```bash kubectl apply -f dr-api-a-tiny.yaml # 동시 연결 10개 보유 (sleep이 stdin을 잡아 5분간 연결 유지) $A bash -c 'for i in $(seq 1 10); do (sleep 300 | openssl s_client -connect api-a.example.com:443 \ -servername api-a.example.com -quiet > /tmp/c$i.log 2>&1) & done; sleep 15; grep -l "BEGIN CERT" /tmp/c*.log | wc -l' ``` | 관찰 | 명령 | 기대 | |---|---|---| | 성공 연결 수 | 위 마지막 출력 | **5** (6번째부터 handshake 실패) | | 거부 카운터 | `stats upstream_cx_overflow` | 5 이상 증가 | | 활성 연결 | `stats upstream_cx_active` | 5에서 고정 | | access log | gw 로그의 response flag | **`UO`** (Upstream Overflow) | **핵심 체감**: 클라이언트 증상이 AuthorizationPolicy 거부와 똑같은 "TLS 실패/reset"이다. flag(`UO` vs rbac 로그)로만 구분 가능 — 런북에 들어갈 내용이 바로 이것. 복구: `kubectl delete dr api-a-external -n istio-egress` (운영에서는 [정본 §06-1](ref__src-tcp-bottlenecks.html)의 운영값으로 재생성). --- ## 3. 재현 2 — Ephemeral 포트 고갈 + TIME_WAIT (28,232개 → 20개) `ip_local_port_range`는 K8s **safe sysctl**이라 pod 단위로 바로 설정 가능하다(kubelet allowlist 불필요). ```yaml # values-egress.yaml에 추가 (gateway 차트의 securityContext = pod-level) securityContext: sysctls: - name: net.ipv4.ip_local_port_range value: "32768 32787" # 재현용: 포트 20개 ``` ```bash helm upgrade egressgateway oci://registry.example.com/charts/istio/gateway \ --version 1.30.0 -n istio-egress -f values-egress.yaml # short-lived 연결 반복 (요청마다 gw->외부 신규 연결 = 포트 1개 + TIME_WAIT 60s) $A bash -c 'ok=0; fail=0; for i in $(seq 1 60); do curl -s -m 3 -o /dev/null https://api-a.example.com/api/ping \ && ok=$((ok+1)) || fail=$((fail+1)); sleep 0.5 done; echo "ok=$ok fail=$fail"' ``` | 관찰 | 명령 (debug 컨테이너) | 기대 | |---|---|---| | TIME_WAIT 적체 | `watch 'ss -tan state time-wait \| wc -l'` | ~20까지 증가 후 정체 | | 포트 소진 시점 | 위 curl 출력 | 약 20번째부터 fail 증가, **60초 지나면 일부 회복** (TIME_WAIT 만료 = 포트 반환의 직접 증거) | | connect 실패 | `stats upstream_cx_connect_fail` | 증가 | | access log | response flag | **`UF`** (Upstream connection Failure) | 이 실험이 보여주는 산술: **포트 20개 ÷ 60s ≈ 0.33 conn/s**가 지속 가능 상한. 운영 기본값(28,232개)에 같은 분수식을 적용한 것이 정본의 470 conn/s다 — 수치는 달라도 식이 같다는 걸 직접 확인하는 게 이 재현의 목적. 복구: `sysctls` 블록 제거 후 `helm upgrade`. --- ## 4. 재현 3 — 유휴 절단 (idle timeout 1h → 30s) 중간장비(FW)가 유휴 세션을 끊는 상황을 Envoy `idleTimeout` 축소로 모사한다. ```yaml apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: api-a-external namespace: istio-egress spec: host: api-a.example.com trafficPolicy: connectionPool: tcp: idleTimeout: 30s # 재현용 (tcp_proxy 기본 1h) ``` ```bash $A bash -c 'time (sleep 120 | openssl s_client \ -connect api-a.example.com:443 \ -servername api-a.example.com -quiet)' # 기대: 120초를 못 채우고 약 30초에 연결 종료. gw access log의 duration ≈ 30000ms ``` **이 재현에 함정 하나가 같이 들어 있다**: 여기에 `tcpKeepalive`를 추가해도 30초 절단은 그대로다. keepalive probe는 데이터가 아니라서 **Envoy의 idle timer를 리셋하지 않는다.** 역할이 다르다 — keepalive는 *방화벽*(패킷만 보면 됨)을 깨어 있게 하고, `idleTimeout`은 Envoy 자신의 기준이라 채널의 최장 유휴 간격보다 길게 직접 설정해야 한다. 직접 keepalive를 넣고 다시 돌려 "그래도 끊긴다"를 확인하면 이 구분이 박힌다. 복구: DR 삭제 또는 운영값(`idleTimeout: 1800s`)으로 교체. --- ## 5. 재현 4 — conntrack 포화 (선택, 전용 노드에서만) > [!warning] > `nf_conntrack_max`는 **노드 전역**이라 그 노드의 모든 pod에 영향을 준다. gw 전용 테스트 노드가 분리되어 있을 때만 수행할 것. ```bash # gw 노드에서 sudo sysctl -w net.netfilter.nf_conntrack_max=200 watch 'conntrack -C; dmesg | tail -3' # 재현 2의 curl 루프 실행 -> dmesg에 "nf_conntrack: table full, dropping packet" sudo sysctl -w net.netfilter.nf_conntrack_max=262144 # 복구 ``` **관찰 포인트**: 증상이 reset이 아니라 **무응답 timeout**이다(silent drop — 커널이 패킷을 버릴 뿐 아무에게도 통보하지 않음). 재현 1·2와 클라이언트 체감은 비슷한데 gateway access log에 단서가 약하다는 것까지가 재현 내용 — 그래서 이 병목만 노드 메트릭(conntrack 사용률)으로 선행 감지해야 한다. --- ## 6. 정리 — 4개의 시그니처가 곧 런북 | 재현 | 축소한 한계 | 실패 시그니처 | 운영에서의 대응 | |---|---|---|---| | 1 | maxConnections 5 | flag `UO`, `upstream_cx_overflow` | 외부 호스트 DR 풀 상향 | | 2 | 포트 20개 | flag `UF`, 60s 후 부분 회복 | keep-alive 캠페인, replica 분산, 커널 튜닝 | | 3 | idleTimeout 30s | 정확히 N초 절단, keepalive 무력 | idleTimeout을 유휴 간격보다 길게 | | 4 | conntrack 200 | 무응답 timeout, dmesg table full | 노드 sysctl + 사용률 알람 | 전체 분기표(rbac denied, PassthroughCluster, handshake 즉시 실패 포함)는 [정본 §07](ref__src-tcp-bottlenecks.html)에. --- ## What you might be missing - **재현 2의 "60초 후 회복"이 이 랩에서 가장 가치 있는 관찰이다.** TIME_WAIT 60초가 추상 지식이 아니라 fail→ok 전환 시각으로 보인다. 운영에서 "간헐적으로 실패하다 저절로 낫는" 패턴을 만나면 이 곡선을 떠올릴 것. - **재현들 사이에 DR 이름이 겹친다** (`api-a-external`). 재현 1과 3을 연달아 할 때 이전 spec이 남아 있으면 두 한계가 동시에 걸려 관찰이 오염된다 — 각 재현 후 복구(삭제)를 건너뛰지 말 것. - **`helm upgrade`로 sysctl을 바꾸면 pod가 재생성된다** — 재현 2 직전까지 쌓인 TIME_WAIT·통계가 리셋된다. 통계 비교는 항상 같은 pod 세대 안에서. - **이 기법의 사각**: 비율이 한계에 비례하지 않는 현상(메모리 압박, FD 고갈, CPU saturation)은 축소 재현이 안 된다. 그쪽은 부하 도구(예: 실제 conn/s를 만드는 tcpkali류)와 전용 환경이 필요한 별개 작업. --- ## 참조 **아카이브 내부** - [Egress TCP 병목 정본](ref__src-tcp-bottlenecks.html) — 이 랩이 재현하는 병목 5종의 메커니즘·산술·완화 운영값 - [Egress 신원 기반 통제 구성](id__guide-mtls-identity-control.html) — 이 랩의 선행조건인 테스트 클러스터 빌드 - [Egress Gateway 도입 가이드 (사내 공유본)](cfg__guide-adoption-passthrough-vs-mtls.html) — 재현 결과가 도입 문서의 병목 표·체크리스트로 압축된 형태 - [Egress 운영 정본](ref__src-operations.html) — L4 모니터링·graceful shutdown 등 운영 전반 - [Envoy response flags](../istio/xds__src-envoy-response-flags.html) — `UO`/`UF` 사전 **외부** - [Envoy circuit breaking](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/upstream/circuit_breaking) — 재현 1의 한계값 출처