- Published on
GCP Cloud NAT 포트 고갈로 egress 실패 진단법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스/VM/GKE 워커가 프라이빗 서브넷에 있고, 외부로 나가는 트래픽(egress)을 Cloud NAT로만 내보내는 구성을 쓰다 보면 어느 순간부터 다음과 같은 증상이 등장합니다.
- 외부 API 호출이 간헐적으로 timeout / connect reset / 5xx로 실패
- 특정 시간대(배치/트래픽 피크)에만 급증
- 애플리케이션 로그에는 “DNS는 되는데 TCP connect가 느리다/실패한다” 수준만 남음
- 재시도하면 대부분 성공하지만 지연이 커짐
이때 가장 흔한 원인 중 하나가 Cloud NAT의 SNAT 포트 고갈(port exhaustion) 입니다. Cloud NAT는 내부(사설 IP:포트) → 외부(공인 IP:포트)로 나갈 때 소스 포트를 할당하는데, 동시 연결/짧은 연결 폭증/연결 재사용 실패가 겹치면 할당 가능한 포트 풀이 바닥나면서 egress가 막힙니다.
이 글은 “Cloud NAT 포트 고갈”을 의심 → 증거 확보 → 원인 분해 → 해결(설정/코드/운영) 순서로 실전 진단하는 체크리스트입니다.
1) Cloud NAT 포트 고갈이 생기는 구조 이해
SNAT 포트가 왜 고갈되나
Cloud NAT는 내부 VM/Pod가 외부로 나갈 때 소스 NAT를 수행합니다.
- 내부 클라이언트가 외부 서버로 TCP 연결을 만들면
- Cloud NAT는 NAT IP(공인) + 임시 소스 포트(ephemeral port) 를 할당해 매핑을 유지합니다.
문제는 다음 상황에서 포트가 빠르게 소모된다는 점입니다.
- 짧은 수명의 연결을 초당 수천~수만 개 만들고 바로 닫음(HTTP keep-alive 미사용, 커넥션 풀 미설정)
- 외부 서버나 중간 장비가 연결을 빨리 끊어 TIME_WAIT/재사용 지연이 커짐
- 애플리케이션이 재시도 폭풍을 일으켜 연결 생성량이 더 증가(실패 → 재시도 → 더 실패)
- NAT IP가 1개인데 노드/Pod 수가 많아 한 공인 IP에 포트 수요가 집중
“포트 고갈”이 다른 장애처럼 보이는 이유
포트가 부족하면 NAT가 새 매핑을 못 만들어서 신규 연결이 실패합니다. 하지만 애플리케이션에서는 대개 다음처럼 관측됩니다.
connect timeout,i/o timeout,ECONNRESET,connection refused(상대/중간 장비 반응에 따라 다양)- 특정 외부 도메인만 실패하는 것처럼 보일 수 있음(실제로는 동시성/재시도 패턴 차이)
이 패턴은 네트워크/프록시 타임아웃 문제와도 유사합니다. 스트리밍/장시간 연결에서의 타임아웃 튜닝 관점은 LLM SSE 스트리밍 499 502 급증과 응답 끊김… 체크리스트도 함께 참고하면 원인 분리가 빨라집니다.
2) “Cloud NAT 포트 고갈”을 의심해야 하는 시그널
다음 조건이 2~3개 이상 겹치면 우선순위를 높이세요.
- 프라이빗 서브넷(외부 IP 없음) + Cloud NAT 사용
- GKE/VM에서 외부 API 호출량이 갑자기 증가(배치, 크롤러, LLM 호출, 결제/알림)
- 짧은 HTTP 요청이 많고(수백 ms~수초), 동시성이 높음
- 재시도 로직이 공격적으로 설정됨(예: 0.1s 간격 무한 재시도)
- 노드 수/Pod 수가 늘었는데 NAT IP는 그대로
3) 증거 확보 1: Cloud NAT 로그로 “포트 부족” 확인
Cloud NAT는 로깅을 켜면 translation/드롭 관련 로그를 남깁니다.
NAT 로깅 활성화(권장)
Cloud Console에서 Cloud NAT 설정에 들어가 Logging을 활성화합니다(필요 시 Errors only로 시작). IaC를 쓰면 Terraform에서도 설정할 수 있습니다.
resource "google_compute_router_nat" "nat" {
name = "nat-prod"
router = google_compute_router.router.name
region = var.region
nat_ip_allocate_option = "MANUAL_ONLY"
nat_ips = [google_compute_address.nat_ip.self_link]
source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
log_config {
enable = true
filter = "ERRORS_ONLY" # 필요 시 "ALL"
}
}
Logs Explorer에서 보는 포인트
Logs Explorer에서 리소스/로그를 NAT로 좁힌 뒤, 다음을 찾습니다.
- 드롭(drop) 이벤트
- 할당 실패/리소스 부족을 암시하는 메시지
환경마다 필드/메시지 형태가 다를 수 있으니, 가장 실전적인 방법은 NAT 관련 로그에서 drop/error만 필터링하고 피크 시간대에 급증하는지 보는 것입니다.
예시 쿼리(개념):
resource.type="nat_gateway"
severity>=ERROR
또는 라우터 NAT 로그 라벨/필드를 기준으로 "drop" 문자열을 찾습니다.
resource.type="gce_router"
("NAT" OR "nat")
("DROP" OR "drop" OR "exhaust" OR "port")
핵심은 “애플리케이션 timeout 급증 시각”과 “NAT drop/error 급증 시각”이 맞물리는지입니다.
4) 증거 확보 2: Cloud Monitoring 메트릭으로 포화 구간 찾기
로그가 정성적 증거라면, 메트릭은 정량적 증거입니다. 다음을 대시보드로 묶어두면 재발 방지가 쉬워집니다.
- NAT 할당/드롭 관련 카운터
- NAT IP별 사용량(가능하면)
- 외부 호출 QPS/동시성(애플리케이션 레벨)
Cloud NAT 메트릭은 라우터/리전/게이트웨이 차원으로 제공됩니다. 콘솔에서 Cloud Monitoring → Metrics Explorer에서 Cloud NAT 또는 Router 관련 메트릭을 검색해 드롭/에러/포트 사용량(또는 번역 수) 계열을 찾아보세요.
운영 팁:
- 피크 시간대에만 문제가 나면 1일/1주 뷰로 패턴을 먼저 잡고
- 그 다음 5분/1분 해상도로 확대해 “고갈 직전의 기울기”를 봅니다.
5) 증거 확보 3: VM/노드에서 TIME_WAIT/연결 폭증 확인
NAT 포트 고갈은 대개 “연결을 너무 많이 만든다”에서 시작합니다. 노드/VM에서 다음을 확인하세요.
TIME_WAIT 소켓이 비정상적으로 많나
# TIME_WAIT 개수
ss -ant state time-wait | wc -l
# 상위 상태 분포
ss -ant | awk '{print $1}' | sort | uniq -c | sort -nr | head
TIME_WAIT가 수만~수십만 단위로 치솟는다면, 짧은 연결/재사용 실패 가능성이 큽니다.
특정 목적지로의 연결이 폭증하나
# 목적지 IP/포트 기준으로 상위 20개
ss -ant | awk 'NR>1 {print $5}' | sed 's/\[//;s/\]//' \
| awk -F: '{print $(NF-1)":"$NF}' | sort | uniq -c | sort -nr | head -20
외부 API 한두 곳으로 연결이 몰려 있으면, 그 클라이언트(서비스)부터 튜닝하는 게 효과가 큽니다.
6) 원인 분해: “NAT 문제” vs “앱/프록시 문제”
포트 고갈은 네트워크 계층이지만, 실제 트리거는 애플리케이션인 경우가 많습니다. 아래 질문으로 분해합니다.
- 실패가 특정 Pod/노드에만 집중되나? (그 워크로드의 연결 패턴 문제)
- 실패가 특정 외부 도메인에서만 두드러지나? (해당 클라이언트 설정/재시도/타임아웃 문제)
- 실패가 전체 외부 트래픽에서 동시다발인가? (NAT IP 부족, 리전/라우터 NAT 한계)
연결/재시도/타임아웃이 문제를 증폭시키는 구조는 OOM의 “메모리 압박→스왑/GC→더 느려짐→더 많이 쌓임”과 비슷합니다. 시스템 자원 고갈의 관점은 리눅스 OOM Killer로 프로세스 죽음 진단·방지처럼 “원인-증상-증폭 루프”로 보면 빠르게 정리됩니다.
7) 해결 1: Cloud NAT 용량 확장(가장 즉각적인 완화)
(1) NAT 공인 IP 수 늘리기
가장 단순하고 강력한 방법은 NAT IP를 추가해 포트 풀을 늘리는 것입니다.
- NAT IP 1개에 포트가 몰리면 고갈이 빨라짐
- IP를 늘리면 선형에 가깝게 여유가 늘어남
Terraform 예시:
resource "google_compute_address" "nat_ips" {
count = 3
name = "nat-ip-${count.index}"
region = var.region
}
resource "google_compute_router_nat" "nat" {
name = "nat-prod"
router = google_compute_router.router.name
region = var.region
nat_ip_allocate_option = "MANUAL_ONLY"
nat_ips = [for ip in google_compute_address.nat_ips : ip.self_link]
source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
log_config {
enable = true
filter = "ERRORS_ONLY"
}
}
운영 팁:
- 외부 서비스가 IP allowlist를 요구한다면 IP 추가는 사전 협의가 필요합니다.
- 그 경우엔 “IP를 늘리되 allowlist도 같이 확장”이 정석이고, 단기적으로는 애플리케이션 연결 재사용을 먼저 잡아야 합니다.
(2) 서브넷/워크로드 분리 + NAT 분리
모든 워크로드를 하나의 NAT로 몰아넣지 말고, 성격이 다른 트래픽(배치/크롤러/스트리밍)을 별도 서브넷 + 별도 NAT로 분리하면 블라스트 레디우스가 줄어듭니다.
8) 해결 2: 애플리케이션에서 연결 재사용/동시성/재시도 튜닝(근본)
NAT IP를 늘리는 건 “통”을 키우는 것이고, 근본은 “물을 덜 새게” 만드는 것입니다.
(1) HTTP keep-alive 및 커넥션 풀 활성화
언어/라이브러리 기본값이 keep-alive가 아니거나, 프록시/로드밸런서에서 끊어버리는 경우가 있습니다.
Python (requests)
requests는 세션을 쓰지 않으면 연결 재사용이 제한적입니다.
import requests
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=200,
pool_maxsize=200,
max_retries=0,
)
session.mount("https://", adapter)
resp = session.get("https://api.example.com/v1/ping", timeout=(3, 10))
print(resp.status_code)
Node.js (undici)
import { Agent, request } from "undici";
const agent = new Agent({
connections: 200,
pipelining: 1,
keepAliveTimeout: 60_000,
keepAliveMaxTimeout: 120_000,
});
const { statusCode } = await request("https://api.example.com/v1/ping", {
dispatcher: agent,
headersTimeout: 10_000,
bodyTimeout: 10_000,
});
console.log(statusCode);
핵심은 매 요청마다 새 TCP/TLS 연결을 만들지 않게 하는 것입니다. TLS 핸드셰이크까지 매번 하면 포트뿐 아니라 CPU도 같이 녹습니다.
(2) 동시성 제한(클라이언트 사이드 rate limit)
배치/크롤러/팬아웃 작업은 “내부 동시성”이 NAT를 먼저 터뜨립니다. 큐/워커에서 동시성을 제한하세요.
Python asyncio 세마포어 예시:
import asyncio
import aiohttp
SEM = asyncio.Semaphore(200) # 동시 요청 상한
async def fetch(session, url):
async with SEM:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as r:
return await r.text()
async def main(urls):
connector = aiohttp.TCPConnector(limit=200, ttl_dns_cache=300)
async with aiohttp.ClientSession(connector=connector) as session:
await asyncio.gather(*(fetch(session, u) for u in urls))
asyncio.run(main(["https://api.example.com"] * 10000))
(3) 재시도 정책을 “지능적으로”
무작정 빠른 재시도는 포트 고갈을 더 악화시킵니다.
- 지수 백오프 + 지터(jitter)
- 4xx는 재시도 금지(특히 429는 서버 정책에 맞게)
- 타임아웃을 짧게 잡되, 재시도 총량을 제한
이 관점은 쿼터/스로틀링 문제와도 유사합니다. 외부 API의 제한과 함께 볼 때는 AWS Bedrock InvokeModel 403·Throttling 해결 - IAM·VPC·쿼터처럼 “제한을 만났을 때 재시도 폭풍을 막는” 접근이 그대로 적용됩니다.
9) 해결 3: 네트워크/플랫폼 레벨의 보완책
(1) Private Google Access / Private Service Connect 고려
대상이 Google API/Google 관리 서비스라면, 굳이 인터넷 egress로 빼지 않고 프라이빗 경로로 우회할 수 있습니다.
- Cloud NAT 포트를 쓰지 않게 되므로 고갈 리스크 감소
- 보안/감사 측면에서도 유리
(2) 프록시(egress proxy)로 커넥션 집약
서비스들이 제각각 외부로 나가지 않고, 내부에 egress proxy(Envoy/Squid)를 두어
- 외부로 나가는 연결 수를 줄이고(커넥션 재사용)
- 관측/정책/캐시를 중앙화
할 수 있습니다. 다만 프록시 자체가 병목/단일 장애점이 될 수 있으니 HA 및 튜닝이 필요합니다.
10) 재현과 검증: “고친 게 맞는지” 확인하는 방법
(1) 부하 테스트로 포트 고갈을 의도적으로 유발
아래는 매우 단순한 예시입니다. 테스트 환경에서만 실행하세요.
# hey로 짧은 요청을 고동시성으로 반복
hey -z 60s -c 1000 https://api.example.com/ping
이후 다음을 동시에 관측합니다.
- NAT 로그의 drop/error 증가 여부
- 애플리케이션의 connect timeout 증가 여부
- 노드의
TIME_WAIT증가 여부
(2) 개선 후 비교
- NAT IP 추가 전/후: drop/error가 사라지는지
- 커넥션 풀 적용 전/후: TIME_WAIT와 신규 연결 생성률이 줄었는지
- 재시도 정책 변경 전/후: 피크에서의 “실패→재시도→더 실패” 루프가 꺼졌는지
11) 운영 체크리스트(요약)
- Cloud NAT 로깅 활성화(최소 errors)
- Monitoring 대시보드: NAT drop/error + 외부 호출 QPS + TIME_WAIT
- 피크 시간대와 실패 시간대 상관관계 확인
- 단기 완화: NAT IP 추가 / NAT 분리
- 근본 해결: keep-alive/커넥션 풀/동시성 제한/재시도 백오프
- Google API라면 프라이빗 경로(Private Google Access/PSC) 검토
12) 마무리: 포트 고갈은 “네트워크 장애”가 아니라 “연결 관리 실패”인 경우가 많다
Cloud NAT 포트 고갈은 겉으로는 네트워크가 불안정해 보이지만, 실제로는 애플리케이션이 만드는 연결 패턴(짧은 연결, 과도한 동시성, 재시도 폭풍)이 NAT의 유한한 자원을 소모하면서 발생하는 경우가 많습니다.
따라서 해결도 두 갈래로 가야 합니다.
- 플랫폼: NAT IP 확장/분리로 즉시 숨통을 틔우고
- 애플리케이션: 커넥션 재사용과 재시도 제어로 장기적으로 안정화
이 두 가지를 함께 적용하면 “피크 때만 egress가 죽는” 문제를 재발 없이 정리할 수 있습니다.