콘텐츠로 이동

네트워크 디버깅

네트워크 디버깅: 관측 지점으로 가설을 좁히는 기술

섹션 제목: “네트워크 디버깅: 관측 지점으로 가설을 좁히는 기술”

네트워크 디버깅은 “요청이 실패했다”는 겉 증상을 OSI(Open Systems Interconnection) 계층 모델의 관측 지점으로 나누고, 각 지점에서 반증 가능한 가설을 하나씩 제거하는 과정이다.

OSI 모델은 네트워크를 물리 링크, 데이터 링크, IP 라우팅, TCP/UDP 전송, 세션, 표현, 애플리케이션 계층으로 나눠 보는 학습 모델이다. 실제 시스템은 이 7개 층이 항상 깔끔히 분리되지는 않지만, 장애를 좁힐 때는 “어느 층의 약속이 먼저 깨졌는가”를 묻는 지도 역할을 한다.

MTTR(Mean Time To Recovery 또는 Mean Time To Repair)은 장애가 발생한 뒤 정상 상태로 복구되기까지 걸린 평균 시간이다. 네트워크 디버깅의 목적은 명령어를 많이 외우는 것이 아니라, 잘못된 재시작과 무작정 롤백을 줄여 MTTR을 낮추는 것이다.

이 문서의 중심 질문은 하나다.

어느 관측 지점에서
무슨 증거를 얻었고
그 증거가 어떤 가설을 반증했는가?

2. 왜 계층형 디버깅이 필요한가

섹션 제목: “2. 왜 계층형 디버깅이 필요한가”

“API가 안 된다”는 문장은 보통 L7(Application, 애플리케이션 계층) 증상이다. 그러나 원인은 DNS 이름 해석, TCP 연결 수립, TLS 인증서, 로드밸런서 타겟, Security Group, NACL, Kubernetes Service, 애플리케이션 코드 어디에나 있을 수 있다.

한 계층의 증상은 다른 계층의 실패를 포장한다.

브라우저:
Failed to fetch
가능한 원인:
DNS가 NXDOMAIN을 반환했다.
DNS는 맞지만 TCP SYN에 SYN-ACK가 오지 않는다.
TCP는 열리지만 TLS 인증서의 SAN이 호스트명과 다르다.
TLS는 성공하지만 ALPN 협상 결과가 기대한 HTTP/2가 아니다.
HTTP는 도착했지만 로드밸런서가 타겟과 연결하지 못했다.
앱은 정상인데 CORS(Cross-Origin Resource Sharing) preflight 응답 헤더가 빠졌다.

명령어를 순서대로 실행하는 체크리스트만으로는 부족하다. 같은 curl 실패라도 어느 시점에서 실패했는지에 따라 다음 가설이 달라진다.

관찰된 실패 지점더 가까운 원인 후보다음 관측 지점
이름이 IP로 풀리지 않는다DNS 레코드, resolver, split-horizon 설정dig, 권한 DNS, 여러 resolver 비교
IP는 있지만 연결이 안 된다라우팅, Security Group, NACL, listener 부재nc, ss, Flow Logs, packet capture
연결은 되지만 TLS가 실패한다인증서 체인, SNI, SAN, TLS 버전, ALPNopenssl s_client, curl -v
TLS는 되지만 첫 바이트가 늦다앱 처리, upstream 호출, DB, target queuecurl -w, ALB access log, app trace
브라우저에서만 실패한다CORS, mixed content, service worker, cacheDevTools, preflight 재현, 응답 헤더

TTL(Time To Live)은 DNS 레코드나 IP 패킷에서 “얼마나 오래 유효한가”를 제한하는 값이다. DNS에서 TTL 300은 resolver가 해당 응답을 최대 300초 동안 캐시할 수 있다는 뜻이다. 그래서 dig로 새 IP가 보이더라도, 일부 클라이언트는 TTL이 끝날 때까지 이전 IP를 계속 사용할 수 있다.

DNS propagation은 DNS 변경이 여러 resolver와 캐시를 거치며 관찰자에게 점차 다르게 보이는 기간을 말한다. 실제로는 전 세계에 무언가가 동시에 “전파”되는 사건이라기보다, 기존 캐시가 각자 만료되는 과정에 가깝다.

퀴즈

DNS 레코드를 바꿨는데 일부 사용자만 예전 서버로 가는 경우, 바로 앱 롤백을 해야 할까?

힌트: 증상은 L7이어도 원인은 캐시된 이름 해석일 수 있다.

정답 보기

먼저 TTL과 resolver별 응답을 확인해야 한다. TTL이 아직 남아 있으면 일부 사용자가 이전 IP를 보는 것은 정상 후보다. 앱 롤백보다 DNS 캐시 만료, 이전 서버 유지, dual serving 같은 완충책을 먼저 검토한다.


2.5 선행 기술의 한계 - 네트워크 디버깅이 등장한 이유

섹션 제목: “2.5 선행 기술의 한계 - 네트워크 디버깅이 등장한 이유”

애플리케이션 로그만 보면 서버 코드가 실행된 뒤의 세계만 보인다. 그러나 많은 네트워크 장애는 앱 로그에 도달하기 전에 끝난다.

클라이언트
-> DNS resolver
-> 인터넷/VPC 라우팅
-> 방화벽 또는 Security Group
-> 로드밸런서
-> TLS 종료 지점
-> Pod 또는 VM 커널
-> 애플리케이션 프로세스

이 중 어느 지점에서 끊겨도 사용자에게는 “API 실패”로 보인다. 앱 로그가 비어 있을 때 “로그가 없으니 요청이 없었다”고 결론 내리면 위험하다. 요청이 방화벽에서 드롭됐거나, 커널에서 RST(reset)로 끊겼거나, 로드밸런서가 타겟을 unhealthy로 보고 전달하지 않았을 수 있다.

계층형 디버깅은 이 한계를 피하기 위해 등장한다.

  1. 사용자 증상에서 시작한다.
  2. 가장 가까운 관측 지점을 고른다.
  3. 그 지점이 볼 수 있는 것과 볼 수 없는 것을 분리한다.
  4. 하나의 가설을 반증한다.
  5. 남은 가능성이 있는 다음 계층으로 이동한다.

예를 들어 VPC Flow Logs의 action=REJECT는 앱 로그에 남지 않는다. VPC Flow Logs는 AWS VPC의 네트워크 인터페이스(ENI) 주변에서 IP 흐름을 기록하는 로그다. REJECT는 Security Group 또는 NACL(Network ACL) 같은 네트워크 정책 때문에 흐름이 거부됐다는 강한 신호다. 반면 Flow Logs가 ACCEPT라고 해서 앱이 응답했다는 뜻은 아니다. 패킷이 네트워크 정책은 통과했지만, 타겟 프로세스가 포트를 듣지 않을 수도 있다.

출처: RFC 1035, AWS VPC Flow Log Records, Google SRE Book - Effective Troubleshooting


3. 진단 루프: 관측 지점 -> 가설 -> 반증 -> 다음 계층

섹션 제목: “3. 진단 루프: 관측 지점 -> 가설 -> 반증 -> 다음 계층”

네트워크 디버깅의 단위는 도구가 아니라 루프다.

1. 증상을 계층 언어로 다시 쓴다.
2. 지금 볼 수 있는 가장 싼 관측 지점을 고른다.
3. 가설을 하나만 세운다.
4. 그 가설이 틀렸음을 보여줄 증거를 찾는다.
5. 반증되면 다음 계층으로 이동한다.
6. 반증되지 않으면 더 좁은 관측 지점으로 들어간다.

가설은 반드시 관찰 가능한 문장이어야 한다.

나쁜 가설좋은 가설
”네트워크가 이상하다""클라이언트 SYN이 서버까지 도달하지 않는다"
"DNS 문제 같다""public resolver와 VPC resolver가 서로 다른 A 레코드를 준다"
"TLS가 꼬였다""SNI 없이 접속해서 기본 인증서가 내려온다"
"로드밸런서가 불안정하다""ALB는 502를 내고 target_status_code가 비어 있다"
"쿠버네티스 네트워크 문제""Pod 안에서는 Service가 되지만 Ingress 외부에서는 443이 닫힘”

처음부터 packet capture를 뜨는 것이 능숙한 디버깅은 아니다. 관측은 비용과 침습도가 다르다.

관측 지점비용볼 수 있는 것놓치는 것
브라우저 DevTools낮음status, timing, CORS, cache, protocol서버 내부, 방화벽, 커널 드롭
curl -v, curl -w낮음DNS/TCP/TLS/TTFB 단계별 시간, HTTP 헤더브라우저 정책, 중간 패킷 손실의 정확한 위치
dig, nc, openssl s_client낮음DNS, 포트, TLS 협상앱 처리, 로드밸런서 내부 상태
ss중간로컬 소켓 상태, LISTEN, ESTAB, CLOSE-WAIT원격 경로, 방화벽에서 사라진 패킷
VPC Flow Logs, ALB access logs중간클라우드 경계, 로드밸런서 판단패킷 payload, 커널 내부 상태
packet capture높음실제 패킷, SYN/SYN-ACK/RST, 재전송, TLS alert앱 의미, 암호화된 HTTP body
커널/iptables/conntrack 관찰높음NAT, connection tracking, DROP 지점클라우드 외부 경로, 사용자 브라우저 정책

packet capture는 네트워크 인터페이스를 지나는 패킷의 헤더와 필요 시 일부 payload를 기록하는 관찰 방식이다. 대표 도구는 tcpdump와 Wireshark다. pcap(packet capture)은 캡처 결과를 저장하는 파일 형식 또는 그런 캡처 파일을 가리키는 말로 쓰인다. 프로덕션에서는 민감한 payload가 들어갈 수 있으므로 좁은 필터와 짧은 시간 창으로 시작한다.

도구 출력은 사실이 아니라 관측 지점의 시야다.

ping 실패:
ICMP가 차단됐을 수 있다.
TCP 443도 실패한다고 말하지 않는다.
curl 200:
HTTP 엔드포인트 하나는 성공했다.
모든 사용자, 모든 경로, 브라우저 정책까지 정상이라고 말하지 않는다.
Flow Logs ACCEPT:
VPC 정책은 통과했다.
앱 프로세스가 요청을 처리했다고 말하지 않는다.
tcpdump에서 SYN 수신:
NIC 또는 캡처 지점까지 SYN이 왔다.
앱이 accept()했다고 말하지 않는다.

3-3. 수치 감각: MTTR이 줄어드는 이유

섹션 제목: “3-3. 수치 감각: MTTR이 줄어드는 이유”

원문이 보존하던 운영 시나리오의 숫자는 여전히 중요하다. 단, 숫자는 “이 도구를 외우라”가 아니라 “어떤 가설을 빨리 버릴 수 있는가”를 보여줘야 한다.

관측 지점이 MTTR을 줄이는 방식

간헐적 503 또는 502

관측 없이 재현과 로그만 보면 4~8시간이 걸릴 수 있다. ALB access log에서 elb_status_code와 target_status_code를 나누고 `ss`로 타겟 소켓을 보면 30분~1시간 안에 LB-타겟 경계로 좁힐 수 있다.

반증: 앱이 502를 냈는가, ALB가 타겟 연결에 실패했는가

특정 클라이언트 TLS 실패

사용자 제보만 모으면 1~3일이 걸릴 수 있다. `openssl s_client`와 짧은 packet capture로 SNI, SAN, TLS 버전, alert를 확인하면 2~4시간 안에 재현 가능한 실패로 바꿀 수 있다.

반증: 서버 인증서 문제인가, 클라이언트 정책 문제인가

DNS 전환 지연

무작정 24~48시간 기다리는 대신 resolver별 `dig`와 TTL을 보면 즉시 '아직 캐시가 남은 정상 혼재'인지 '권한 DNS 설정 오류'인지 나눌 수 있다.

반증: 변경이 안 됐는가, 캐시가 남았는가

TCP RST 폭증

사후 로그만으로는 방향을 놓치기 쉽다. RST를 누가 보냈는지 잡으면 5분 안에 앱 종료, idle timeout 불일치, 방화벽 정책 후보로 좁힐 수 있다.

반증: 클라이언트가 끊는가, 서버나 중간 장비가 끊는가

4. 계층 지도: 무엇을 관측하고 무엇을 반증하는가

섹션 제목: “4. 계층 지도: 무엇을 관측하고 무엇을 반증하는가”

4-1. OSI 계층을 디버깅 언어로 바꾸기

섹션 제목: “4-1. OSI 계층을 디버깅 언어로 바꾸기”
계층디버깅 질문대표 관측 지점
L7 ApplicationHTTP 요청/응답의 의미가 맞는가curl -v, DevTools, ALB access log
L6 PresentationTLS 인증서, 암호화, ALPN이 맞는가openssl s_client, curl -v
L5 Session연결 재사용, 쿠키, idle timeout이 맞는가ALB logs, app logs, ss, DevTools
L4 TransportTCP/UDP 연결과 포트 상태가 맞는가nc, ss, tcpdump
L3 NetworkIP 라우팅과 방화벽 경계가 맞는가ping, traceroute, mtr, Flow Logs
L2 Data Link로컬 링크, ARP, bridge, veth가 맞는가ip neigh, ip link, namespace 관찰
L1 Physical/Virtual NIC인터페이스가 살아 있고 드롭이 없는가ethtool, NIC metric, cloud metric

L5와 L6은 실무 디버깅에서 항상 독립 계층처럼 보이지 않는다. HTTPS 요청을 보면 TLS 핸드셰이크, ALPN, HTTP 버전, keep-alive가 한 흐름에 섞여 보인다. 그래서 실제 진단에서는 “DNS -> TCP -> TLS -> HTTP -> 앱”처럼 프로토콜 단계로 읽는 편이 더 직접적이다.

4-2. 요청 한 번을 단계로 분해하기

섹션 제목: “4-2. 요청 한 번을 단계로 분해하기”
1. 이름 해석
api.example.com -> 203.0.113.10
2. 경로와 정책
클라이언트에서 203.0.113.10:443으로 나갈 수 있는가
VPC/방화벽/라우팅이 허용하는가
3. TCP 연결
SYN -> SYN-ACK -> ACK가 완성되는가
4. TLS 협상
SNI, 인증서 체인, SAN, TLS 버전, ALPN이 맞는가
5. HTTP 요청/응답
status, header, body, TTFB, redirect, CORS가 맞는가
6. 서버 내부 처리
upstream, DB, queue, thread pool, connection pool이 병목인가

SYN, SYN-ACK, RST는 TCP 플래그가 붙은 패킷 이름이다. SYN은 새 연결을 시작하자는 요청이고, SYN-ACK는 서버가 그 요청을 받아들이며 응답하는 패킷이다. RST(reset)는 연결을 정상 종료 절차 없이 즉시 끊겠다는 신호다. RST가 보이면 “누가 끊었는가”가 첫 질문이다.

ALPN(Application-Layer Protocol Negotiation)은 TLS 핸드셰이크 중 HTTP/1.1, HTTP/2 같은 상위 프로토콜을 협상하는 확장이다. HTTP/2 문제가 의심될 때 curl -v에서 ALPN: server accepted h2가 보이면 “적어도 TLS 협상에서 h2는 선택됐다”는 가설을 세울 수 있다.

TTFB(Time To First Byte)는 요청을 보낸 뒤 응답의 첫 바이트를 받기까지 걸린 시간이다. 브라우저와 curl에서 보이는 TTFB는 서버 처리 시간만이 아니라 네트워크, TLS, 로드밸런서 큐, upstream 대기까지 포함할 수 있다. curl -w의 누적 타이밍 차이를 계산해야 어느 구간이 느린지 분리된다.

4-3. 레이어를 건너뛰어도 되는 경우와 안 되는 경우

섹션 제목: “4-3. 레이어를 건너뛰어도 되는 경우와 안 되는 경우”

다음은 계층형 디버깅의 반례다. 항상 L3부터 끝까지 순서대로 돌 필요는 없다.

상황바로 볼 지점이유
HTTP 500이 응답 body와 함께 온다앱 로그, trace, upstream metricDNS/TCP/TLS는 이미 통과했다
특정 브라우저에서 mixed content만 실패DevTools Security/Console서버 packet capture보다 브라우저 정책 문제
모든 요청이 Could not resolve hostDNS resolver와 권한 DNSTCP 관측 전에 이름 해석이 깨졌다
connect timeout이 3초 이상 반복된다L4/L3, Flow Logs, SYN packet앱 로그까지 도달하지 않았을 가능성이 크다
TTFB만 1.8초이고 connect/TLS는 빠르다앱 처리, upstream, target log네트워크 연결 자체는 통과했다

반대로 다음 상황에서는 계층을 건너뛰면 위험하다.

앱 로그가 비어 있다.
-> 요청이 오지 않았다고 결론 내리지 않는다.
-> Flow Logs, ALB access log, packet capture로 더 낮은 지점을 확인한다.
브라우저 CORS 에러가 보인다.
-> 서버가 요청을 못 받았다고 결론 내리지 않는다.
-> preflight OPTIONS, 응답 헤더, DevTools blocked reason을 본다.
ping이 실패한다.
-> 서버가 죽었다고 결론 내리지 않는다.
-> TCP 443, Flow Logs, HTTP health를 별도로 본다.

5. 도구 메커니즘: 명령보다 관측 의미를 먼저 본다

섹션 제목: “5. 도구 메커니즘: 명령보다 관측 의미를 먼저 본다”

5-1. curl: 사용자 공간에서 HTTP 단계 시간을 나눈다

섹션 제목: “5-1. curl: 사용자 공간에서 HTTP 단계 시간을 나눈다”

curl은 사용자 공간에서 DNS, TCP, TLS, HTTP 응답을 한 번에 관찰한다. 좋은 점은 가볍고 어디서나 실행하기 쉽다는 것이다. 한계는 커널이나 방화벽에서 사라진 패킷의 위치를 직접 보여주지 못한다는 점이다.

핵심은 curl -w 값이 대부분 “시작부터 해당 단계까지의 누적 시간”이라는 점이다. 구간 시간은 서로 빼서 계산한다.

Terminal window
curl -o /dev/null -s -w \
"namelookup=%{time_namelookup} connect=%{time_connect} appconnect=%{time_appconnect} starttransfer=%{time_starttransfer} total=%{time_total} code=%{http_code}\n" \
https://api.example.com/health

예상 출력 하나만으로도 여러 가설을 버릴 수 있다.

namelookup=0.002
connect=0.020
appconnect=0.120
starttransfer=1.800
total=1.830
code=200

해석:

DNS = 0.002초
TCP = 0.020 - 0.002 = 0.018초
TLS = 0.120 - 0.020 = 0.100초
TTFB 대기 = 1.800 - 0.120 = 1.680초
다운로드 = 1.830 - 1.800 = 0.030초

이 출력은 “DNS가 느리다”와 “TCP 연결이 느리다”를 반증한다. 남는 가설은 서버 처리, 로드밸런서 target queue, upstream 호출, DB 대기다. 반대로 time_connect가 3초 이상이면 앱 로그보다 SYN 재전송, Security Group, NACL, 라우팅을 먼저 본다.

curl -v는 단계별 메시지를 보여준다.

* Trying 203.0.113.10:443...
* Connected to api.example.com (203.0.113.10) port 443
* ALPN: server accepted h2
* SSL connection using TLSv1.3
> GET /health HTTP/2
< HTTP/2 200

이때 반증되는 가설:

출력반증되는 가설
Trying 203.0.113.10DNS가 완전히 실패했다
ConnectedTCP 443이 전혀 닫혀 있다
SSL connection using TLSv1.3TLS 핸드셰이크가 실패했다
ALPN: server accepted h2서버가 HTTP/2를 전혀 협상하지 않았다
< HTTP/2 200이 health 경로가 HTTP 에러를 낸다

--resolve는 DNS를 임시로 우회해 같은 호스트명으로 특정 IP에 접속한다. 인증서 검증에는 호스트명이 필요하므로 /etc/hosts를 바꾸지 않고도 새 타겟, 특정 Pod, 특정 ALB를 비교할 수 있다.

Terminal window
curl --resolve api.example.com:443:10.0.1.15 https://api.example.com/health

이 명령이 성공하고 일반 요청만 실패하면 DNS, 로드밸런서, 라우팅 경로가 후보로 남는다. 반대로 이것도 실패하면 타겟 자체, TLS 인증서, 앱 경로를 본다.

5-2. dig: DNS 응답을 resolver별로 비교한다

섹션 제목: “5-2. dig: DNS 응답을 resolver별로 비교한다”

dig는 이름 해석 결과와 TTL을 관찰한다. DNS 문제의 핵심은 “누가 어떤 답을 캐시하고 있는가”다.

Terminal window
dig api.example.com +short
dig @8.8.8.8 api.example.com +short
dig @1.1.1.1 api.example.com +short
dig api.example.com +trace

관찰 포인트:

관찰의미다음 가설
NXDOMAIN해당 이름이 존재하지 않는다는 응답권한 DNS, zone, 레코드 오타
resolver별 IP가 다름캐시 만료 시점이나 split-horizon 차이TTL, 내부/외부 DNS 정책
TTL이 300최대 5분간 이전 응답이 남을 수 있음전환 중 혼재 정상 후보
+trace는 맞고 로컬만 틀림로컬 resolver 또는 OS 캐시 문제/etc/resolv.conf, nscd, CoreDNS
Pod 내부 DNS만 실패Kubernetes DNS 또는 search/ndots 문제CoreDNS, Service 이름, namespace

Kubernetes에서 ndots는 이름에 dot이 몇 개 이상 있어야 절대 도메인처럼 먼저 질의할지를 정하는 resolver 옵션이다. 예를 들어 ndots:5에서는 api.example.com도 search domain을 붙인 후보를 먼저 여러 번 시도할 수 있어, 외부 도메인 조회가 예상보다 느려지거나 다른 이름으로 질의될 수 있다.

DNS 전파 지연은 앱이 느린 것이 아니다. 이전 IP로 가는 사용자와 새 IP로 가는 사용자가 동시에 존재하는 기간이다. 안전한 전환은 TTL을 낮춘 뒤 충분히 기다리고, 전환 후에도 구 서버나 이전 타겟을 TTL 이상 유지하는 방식으로 설계한다.

5-3. openssl s_client: TLS 협상과 인증서를 본다

섹션 제목: “5-3. openssl s_client: TLS 협상과 인증서를 본다”

openssl s_client는 TLS 핸드셰이크를 관찰한다. 여기서 보는 것은 HTTP body가 아니라 서버가 어떤 인증서를 제시했고, 어떤 TLS 버전과 cipher가 선택됐으며, ALPN이 무엇으로 합의됐는가다.

Terminal window
openssl s_client -connect api.example.com:443 -servername api.example.com -alpn h2 </dev/null

-servername은 SNI(Server Name Indication)를 넣는다. SNI는 TLS ClientHello 안에 목적지 호스트명을 담아, 하나의 IP나 로드밸런서가 여러 인증서 중 어떤 것을 제시할지 고르게 하는 확장이다. SNI 없이 IP로만 접속하면 기본 인증서가 내려와 SAN(Subject Alternative Name) 불일치가 날 수 있다.

관찰 포인트:

출력읽는 법
Verify return code: 0클라이언트 기준 인증서 체인 검증 성공
subject=서버가 제시한 인증서의 주체
issuer=서명한 CA
ALPN protocol: h2HTTP/2로 넘기기로 협상됨
no peer certificateTLS 종료 지점이 없거나 잘못된 포트일 수 있음
handshake failure버전, cipher, SNI, 클라이언트 인증 요구 등을 의심

TLS 디버깅의 흔한 반례는 “브라우저는 실패하는데 curl -k는 성공한다”다. -k는 인증서 검증을 끄므로, 인증서 만료나 SAN 불일치 문제를 숨긴다. 원인을 찾을 때는 -k 성공을 정상으로 보지 말고 “TCP와 TLS record 교환은 가능하지만 신뢰 검증이 실패한다”로 읽어야 한다.

5-4. ss: 로컬 커널의 소켓 상태를 본다

섹션 제목: “5-4. ss: 로컬 커널의 소켓 상태를 본다”

ss(Socket Statistics)는 로컬 머신의 소켓 상태를 보여준다. 포트가 LISTEN 중인지, 연결이 ESTAB(established, 수립됨) 상태인지, CLOSE-WAIT가 쌓이는지, TIME-WAIT가 많은지 확인할 수 있다.

Terminal window
ss -tlnp
ss -tan
ss -tn state established

소켓 상태는 원격 경로 전체를 보여주지 않는다. 대신 “로컬 프로세스와 커널이 어떤 상태로 연결을 보고 있는가”를 보여준다.

상태의미디버깅 질문
LISTEN프로세스가 해당 포트에서 연결을 기다림올바른 IP와 포트에 바인딩했는가
ESTABTCP 연결이 수립됨연결 수가 평소보다 많은가, 특정 peer인가
SYN-SENT로컬이 SYN을 보냈고 응답을 기다림원격 SYN-ACK가 돌아오는가
SYN-RECVSYN을 받았고 handshake 완료를 기다림SYN flood, backlog, 응답 경로 문제인가
TIME-WAIT먼저 닫은 쪽이 지연 패킷을 피하려고 대기단기 연결이 많은가, 포트 고갈 신호가 있는가
CLOSE-WAIT원격이 닫았는데 로컬 앱이 아직 close하지 않음코드가 socket close를 누락했는가

TIME-WAIT는 낭비가 아니라 TCP 안전장치다. 문제는 많다는 사실 자체가 아니라 ephemeral port가 고갈되거나 연결 생성률이 구조적으로 너무 높은 경우다. ephemeral port는 클라이언트 OS가 outbound 연결을 만들 때 임시로 고르는 출발지 포트 범위다. CLOSE-WAIT는 다르다. 계속 늘어난다면 애플리케이션이 연결을 닫지 않는 버그일 가능성이 높다.

짧은 예:

정상에 가까운 상태:
245 ESTAB
42 TIME-WAIT
5 LISTEN
위험 신호:
1240 CLOSE-WAIT
150 ESTAB
12 TIME-WAIT

두 번째는 네트워크보다 애플리케이션 close 처리 누락을 먼저 본다. tcp_tw_reuse 같은 커널 파라미터로 도망가면 원인과 멀어진다.

5-5. tcpdump와 BPF: 커널 경계의 패킷을 좁게 캡처한다

섹션 제목: “5-5. tcpdump와 BPF: 커널 경계의 패킷을 좁게 캡처한다”

tcpdump는 packet capture를 수행한다. 앱 로그나 curl이 “안 된다”고 말할 때, 실제로 SYN이 나갔는지, SYN-ACK가 돌아왔는지, 누가 RST를 보냈는지 볼 수 있다.

프로덕션에서 중요한 개념은 BPF(Berkeley Packet Filter)다. BPF는 tcpdump 필터 표현식을 커널에서 실행 가능한 필터로 바꿔, 매칭되는 패킷만 사용자 공간으로 복사하게 한다. 필터 없이 모든 패킷을 사용자 공간으로 복사하면 CPU와 디스크, 민감정보 노출 위험이 커진다. 1Gbps 링크에서는 초당 수십만 개의 패킷이 지나갈 수 있다. 이때 tcpdump -i eth0처럼 넓게 뜨면 관측 자체가 CPU 사용량과 packet drop을 만들 수 있으므로, host·port·protocol을 좁혀서 “내 가설을 반증할 패킷”만 본다.

필터 없음:
NIC -> 커널 -> 모든 패킷을 사용자 공간으로 복사 -> tcpdump가 출력
BPF 필터:
NIC -> 커널 BPF 필터 -> 매칭된 패킷만 사용자 공간으로 복사

좋은 packet capture 질문:

나쁜 질문:
네트워크가 이상한지 보자.
좋은 질문:
클라이언트 10.0.1.5의 SYN이 10.0.2.10:8080까지 도착하는가?
서버가 SYN-ACK를 보내는가?
RST를 보내는 쪽은 클라이언트인가 서버인가?

짧은 관찰 스니펫:

Terminal window
sudo tcpdump -i eth0 -n 'host 10.0.1.5 and port 8080' -c 20
sudo tcpdump -i eth0 -n 'tcp[13] & 4 != 0' -c 10
sudo tcpdump -i eth0 -n 'port 8080 and (tcp[13] == 2 or tcp[13] == 18)' -c 20

tcp[13]은 TCP 헤더의 flags 바이트를 보는 BPF 표현식이다. 2는 SYN, 18은 SYN+ACK, 4는 RST 비트다. 모든 값을 외우는 것이 목적은 아니다. “연결 시작 신호와 강제 종료 신호를 패킷에서 직접 구분할 수 있다”가 학습 포인트다.

pcap 파일로 저장할 때는 화면에 payload를 길게 출력하지 않는다.

Terminal window
sudo tcpdump -i eth0 -n -w /tmp/api-443.pcap 'host 10.0.1.5 and port 443'

pcap은 Wireshark나 tcpdump -r로 나중에 읽을 수 있다. 민감한 쿠키나 토큰이 들어갈 수 있으므로 공유 전에 캡처 범위와 payload 포함 여부를 확인한다.

5-6. Wireshark: pcap을 프로토콜 의미로 해석한다

섹션 제목: “5-6. Wireshark: pcap을 프로토콜 의미로 해석한다”

Wireshark는 packet capture를 GUI로 분석하는 도구다. 프로덕션 서버에서 직접 GUI를 띄우는 것이 아니라, 보통 서버에서 좁게 pcap을 저장한 뒤 로컬에서 연다.

캡처 필터와 디스플레이 필터를 구분해야 한다.

구분적용 시점문법예시
캡처 필터수집 전BPFhost 10.0.1.5 and port 443
디스플레이 필터수집 후Wireshark 문법tcp.flags.reset == 1

Wireshark의 좋은 쓰임:

  1. Follow TCP Stream으로 한 연결의 흐름을 시간순으로 본다.
  2. tcp.analysis.retransmission으로 재전송을 찾는다.
  3. tls.alert로 TLS alert 위치를 찾는다.
  4. DNS query/response가 어떤 resolver와 오갔는지 본다.

TLS payload는 보통 암호화되어 HTTP body가 보이지 않는다. 개발/스테이징에서 SSLKEYLOGFILE이라는 세션 키 로그 파일을 남기면 Wireshark가 해당 TLS 세션을 복호화할 수 있지만, 프로덕션에서는 키와 개인정보 취급 위험이 커진다. 첫 회독에서는 “Wireshark는 패킷 순서와 프로토콜 메타데이터를 잘 보여준다” 정도로 잡으면 충분하다.

5-7. traceroutemtr: 경로와 손실을 오해하지 않는다

섹션 제목: “5-7. traceroute와 mtr: 경로와 손실을 오해하지 않는다”

traceroute는 TTL을 1, 2, 3…으로 늘린 probe를 보내며 각 홉에서 돌아오는 ICMP TTL exceeded 응답을 이용해 경로를 추정한다. mtr(My Traceroute)은 traceroute를 반복 실행해 Loss%, Avg, StDev 같은 누적 통계를 보여준다.

중요한 경계:

중간 홉의 * * *:
그 홉이 ICMP 응답을 차단하거나 rate limit했을 수 있다.
다음 홉과 목적지가 정상 응답하면 그 자체로 장애 증거는 약하다.
목적지 홉의 손실:
실제 사용자 트래픽 손실과 더 가까운 신호다.
ICMP 기반 mtr:
방화벽 정책과 ICMP rate limit의 영향을 받는다.
TCP 모드 mtr:
실제 서비스 포트와 더 비슷한 경로 정책을 탈 수 있다.

짧은 판단 기준:

질문도구
지금 경로가 어디로 가는가traceroute
간헐적 손실이 있는가mtr -r -c 100
443 정책과 비슷하게 보고 싶다mtr -T -P 443
중간 홉만 손실이 높다이후 홉 손실 여부로 판단

Security Group은 AWS ENI에 붙는 stateful 방화벽이다. Stateful이라는 말은 인바운드가 허용된 연결의 응답 트래픽을 별도 규칙 없이 추적해 허용한다는 뜻이다.

NACL(Network ACL)은 서브넷에 적용되는 stateless 방화벽이다. Stateless라는 말은 인바운드와 아웃바운드가 서로 독립적으로 평가된다는 뜻이다. 요청 포트 8080을 열어도 응답이 나가는 ephemeral port 범위를 막으면 연결이 실패할 수 있다.

VPC Flow Logs는 VPC 네트워크 인터페이스 수준에서 흐름의 ACCEPT/REJECT, 주소, 포트, 프로토콜, bytes 등을 기록한다. payload는 보이지 않는다.

Flow Log 예시:
src=10.0.1.5 dst=10.0.2.10 srcport=54321 dstport=8080 protocol=6 action=REJECT

해석:

  1. 앱 로그가 비어 있어도 네트워크 정책 경계에서 거부됐을 수 있다.
  2. REJECT만으로 Security Group과 NACL 중 어느 쪽인지 바로 확정하지 않는다.
  3. Security Group은 stateful, NACL은 stateless라는 차이로 응답 경로를 함께 본다.
  4. ACCEPT는 네트워크 정책 통과이지 앱 처리 성공이 아니다.

5-9. ALB access log: 로드밸런서와 타겟을 분리한다

섹션 제목: “5-9. ALB access log: 로드밸런서와 타겟을 분리한다”

ALB(Application Load Balancer)는 클라이언트와 타겟 사이에서 HTTP/TLS 요청을 받아 분배한다. ALB access log는 elb_status_code, target_status_code, request_processing_time, target_processing_time, response_processing_time처럼 경계를 나눠 기록한다.

502를 볼 때 가장 먼저 나눌 질문:

관찰의미
elb_status_code=502, target_status_code=-ALB가 타겟과 정상 대화를 못 했을 가능성
elb_status_code=502, target_status_code=502타겟 앱이 502를 응답했을 가능성
target_processing_time만 높다타겟 처리 또는 upstream 대기 후보
request_processing_time이 높다ALB가 타겟에 요청을 보내기 전 병목 후보

이 로그는 앱 로그보다 앞쪽에 있다. 서버 로그가 비어 있는데 ALB log에 요청이 있다면, 타겟 연결 실패, health check, target group, SG/NACL, Pod readiness를 봐야 한다.

5-10. Kubernetes와 컨테이너: 네임스페이스를 구분한다

섹션 제목: “5-10. Kubernetes와 컨테이너: 네임스페이스를 구분한다”

컨테이너는 별도 네트워크 네임스페이스를 갖는다. 네트워크 네임스페이스는 인터페이스, 라우팅 테이블, iptables 관점을 분리하는 커널 기능이다. 호스트에서 보는 ss와 Pod 안에서 보는 ss가 다를 수 있다.

관측 지점은 셋으로 나눈다.

관측 지점무엇을 보는가
Pod 내부 kubectl exec컨테이너 입장에서 DNS, Service, loopback
debug/ephemeral container도구가 없는 이미지에 임시 관측 도구 추가
호스트 nsenter컨테이너 네트워크 네임스페이스를 호스트 도구로 관찰

Ephemeral Container는 실행 중인 Pod에 임시 디버그 컨테이너를 붙이는 Kubernetes 기능이다. 네트워크 네임스페이스를 공유할 수 있지만, 파일시스템과 프로세스 권한은 기존 컨테이너와 다를 수 있다. 따라서 “debug container에서 된다”를 “앱 컨테이너에서도 된다”로 바로 일반화하지 않는다.

5-11. conntrack: 커널이 연결 흐름을 기억하는 표

섹션 제목: “5-11. conntrack: 커널이 연결 흐름을 기억하는 표”

conntrack(connection tracking)은 Linux netfilter가 연결 흐름을 추적하기 위해 유지하는 상태 테이블이다. NAT, 방화벽, Kubernetes Service 구현은 conntrack에 의존하는 경우가 많다.

conntrack이 가득 차면 새 연결이 드롭되거나 NAT 상태가 만들어지지 않는다. 이때 앱 로그에는 요청이 없고, tcpdump에는 SYN이 보일 수 있으며, 사용자는 timeout을 본다.

관찰 포인트:

Terminal window
sudo conntrack -C
sysctl net.netfilter.nf_conntrack_max

읽는 법:

현재 엔트리 수가 최대값에 가까움:
새 연결 생성이 실패하거나 지연될 수 있다.
특정 peer 흐름이 계속 남아 있음:
idle timeout, NAT, long-lived connection 문제를 본다.
tcpdump에는 SYN이 보이는데 앱 로그가 없음:
iptables DROP, conntrack 고갈, local firewall 후보를 본다.

바로 최대값을 키우는 것은 마지막 수단이다. 먼저 연결 생성률, keep-alive, Service/Endpoint 변화, NAT gateway, Pod churn을 함께 본다.

5-12. 브라우저 DevTools: 브라우저 정책을 네트워크와 분리한다

섹션 제목: “5-12. 브라우저 DevTools: 브라우저 정책을 네트워크와 분리한다”

브라우저 DevTools는 서버 도구와 다른 것을 본다. Network 탭의 Timing은 DNS, initial connection, SSL, waiting(TTFB), content download를 보여주지만, 브라우저 캐시, service worker, CORS, mixed content 같은 브라우저 정책도 함께 드러난다.

짧은 예:

브라우저:
CORS error
서버:
OPTIONS 요청에 200 응답
가능한 원인:
Access-Control-Allow-Origin 누락
Access-Control-Allow-Headers에 Authorization 누락
credential 요청인데 wildcard origin 사용

mixed content는 HTTPS 페이지가 HTTP 리소스를 불러오려 할 때 브라우저가 차단하는 보안 정책이다. 서버에서 curl http://...가 성공해도 브라우저는 차단할 수 있다. 이 경우 packet capture보다 DevTools Console과 Security 탭이 더 빠른 관측 지점이다.

프론트엔드 경험은 여기서 도움이 되지만, 이 문서의 중심은 브라우저가 아니다. DevTools는 전체 네트워크 진단 루프의 한 관측 지점으로만 사용한다.


6. 도구 선택표: “무엇을 관측하고 어떤 가설을 버릴까”

섹션 제목: “6. 도구 선택표: “무엇을 관측하고 어떤 가설을 버릴까””
도구/로그관측 지점반증할 수 있는 가설조심할 오해
DevTools브라우저CORS, cache, mixed content, protocol 문제서버 내부 처리 원인을 확정할 수 없음
curl -v사용자 공간 HTTP/TLSDNS/TCP/TLS/HTTP 단계 일부 성공 여부브라우저 정책과 다를 수 있음
curl -w단계별 누적 시간어느 구간이 느린지값을 구간 시간으로 착각하면 안 됨
digDNS resolver이름 해석, TTL, resolver 차이TCP 연결 가능성을 말하지 않음
openssl s_clientTLS 핸드셰이크SNI, 인증서, TLS 버전, ALPNHTTP 앱 응답 성공을 말하지 않음
ncTCP 포트 연결포트가 완전히 닫혔는지TLS/HTTP 정상 여부는 말하지 않음
ss로컬 커널 소켓LISTEN, ESTAB, CLOSE-WAIT, TIME-WAIT 상태원격 방화벽에서 사라진 패킷은 안 보임
tcpdump커널 패킷 캡처 지점SYN/SYN-ACK/RST, 재전송, 캡처 지점 도달 여부앱이 처리했다는 뜻은 아님
Wiresharkpcap 오프라인 분석프로토콜 흐름, 재전송, TLS alert암호화된 payload는 기본적으로 보이지 않음
traceroute1회 경로 추정어느 홉까지 ICMP 응답이 오는지중간 홉 무응답을 장애로 단정하면 안 됨
mtr반복 경로 통계간헐적 손실과 jitterICMP rate limit에 속을 수 있음
VPC Flow LogsAWS VPC 정책 경계SG/NACL/라우팅 정책 거부 후보앱 처리 성공을 말하지 않음
ALB access log로드밸런서-타겟 경계ALB 5xx와 앱 5xx 분리타겟 내부 원인은 별도 로그가 필요
kubectl exec/debugPod 네트워크 관점클러스터 내부 DNS/Service/포트외부 Ingress 경로와 다를 수 있음
conntrack커널 연결 추적 테이블NAT/방화벽 상태 고갈설정값 조정 전 연결 생성 원인을 봐야 함

7. Worked Scenario A - “사이트가 느려요”

섹션 제목: “7. Worked Scenario A - “사이트가 느려요””

사용자 신고:

사이트가 느려요.

디버깅 문장:

HTTP 요청의 전체 시간이 증가했다.
DNS, TCP, TLS, TTFB, 다운로드 중 어느 구간이 증가했는지 아직 모른다.

처음 가설을 하나로 고정하지 않는다. “DB가 느리다”는 아직 이른 결론이다.

짧은 스니펫:

Terminal window
curl -o /dev/null -s -w \
"dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code}\n" \
https://api.example.com/health

결과 1:

dns=0.002 tcp=0.025 tls=0.080 ttfb=1.750 total=1.790 code=200

반증:

  1. DNS 지연이 주원인이라는 가설은 약해진다.
  2. TCP handshake가 주원인이라는 가설도 약해진다.
  3. TLS handshake도 전체 1.79초 중 55ms 정도다.
  4. TTFB 대기 약 1.67초가 핵심이다.

다음 계층:

ALB target_processing_time
앱 로그의 request duration
upstream/DB trace
connection pool 대기

결과 2:

dns=0.001 tcp=3.007 tls=0.000 ttfb=0.000 total=3.010 code=000

반증:

  1. 앱 코드가 3초 처리했다는 가설은 약해진다.
  2. TCP 연결 단계에서 막힌다.
  3. Security Group, NACL, listener, SYN 재전송, 라우팅을 본다.

다음 계층:

nc -zv api.example.com 443
VPC Flow Logs
tcpdump로 SYN/SYN-ACK 확인
ALB target health

결과 3:

dns=1.200 tcp=1.220 tls=1.280 ttfb=1.360 total=1.390 code=200

반증:

  1. HTTP 서버 처리만 느리다는 가설은 약해진다.
  2. DNS 조회가 전체 시간의 대부분이다.

다음 계층:

resolver별 dig 비교
OS DNS cache
CoreDNS 또는 VPC resolver 상태
DNS TTL과 query volume

“정상 TCP 연결은 몇 ms인가”는 리전, 사용자 위치, 네트워크에 따라 다르다. 그러나 같은 환경에서 갑자기 20ms가 3초로 바뀌는 것은 강한 신호다.

좋은 기준은 절대값 하나가 아니라 비교다.

비교의미
같은 리전 내부 health check인프라 내부 기준선
사용자 지역별 RUM/DevTools지역 네트워크 또는 CDN 경계
배포 전후 같은 경로변경 영향
health 경로와 실제 API 경로앱 처리와 네트워크 공통 비용 분리
HTTP/1.1과 HTTP/2 비교ALPN/프로토콜 선택 또는 multiplexing

8. Worked Scenario B - “특정 API만 502가 난다”

섹션 제목: “8. Worked Scenario B - “특정 API만 502가 난다””

8-1. 첫 질문: 누가 502를 만들었나

섹션 제목: “8-1. 첫 질문: 누가 502를 만들었나”

502 Bad Gateway는 중간 프록시나 게이트웨이가 upstream과 정상 대화하지 못했다는 뜻으로 자주 등장한다. 그러나 “502”라는 숫자만으로 ALB, Nginx, 앱, upstream 중 누가 만들었는지 알 수 없다.

관측 지점:

클라이언트 HTTP status
ALB access log
target app log
Pod restart/readiness
target group health

ALB access log에서 나누는 기준:

elb_status_code=502, target_status_code=-
-> ALB가 타겟 응답을 받지 못했을 가능성
-> 타겟 포트, health, SG/NACL, RST, timeout 확인
elb_status_code=502, target_status_code=502
-> 앱 또는 앱 앞 프록시가 502를 응답했을 가능성
-> 앱 로그, upstream 호출, dependency 확인

8-2. Pod 직접 호출이 가설을 줄이는 방식

섹션 제목: “8-2. Pod 직접 호출이 가설을 줄이는 방식”

ALB를 우회해 Pod IP 또는 Service를 직접 호출하는 것은 “로드밸런서와 타겟 사이” 가설을 줄이기 위한 관측이다.

Terminal window
kubectl exec debug-pod -- curl -s -o /dev/null -w "code=%{http_code} ttfb=%{time_starttransfer}\n" http://my-service:8080/api/problem

해석:

직접 호출 결과남는 후보
직접 호출도 502앱, upstream, config, dependency
직접 호출은 200ALB routing, target group, Ingress, SG/NACL
직접 호출이 timeoutService/Endpoint, Pod listener, NetworkPolicy
Pod IP는 되는데 Service 실패kube-proxy, EndpointSlice, DNS, Service selector

RST는 연결을 강제로 끊는 TCP 신호다. 중요한 것은 RST가 존재한다는 사실보다 방향이다.

client -> server: RST
클라이언트 timeout, 브라우저 중단, keepalive 재사용 실패 후보
server -> client: RST
서버 프로세스 종료, 포트 미청취, backlog/connection limit, 앱 close 후보
middlebox -> client/server: RST
방화벽, 로드밸런서, idle timeout, 정책 차단 후보

짧은 capture:

Terminal window
sudo tcpdump -i eth0 -n 'tcp[13] & 4 != 0 and host 10.0.1.5' -c 10

예상 출력:

10.0.2.10.8080 > 10.0.1.5.45231: Flags [R.], seq 0, ack 1

서버 포트에서 클라이언트로 RST가 나간다. 앱 크래시, listener 종료, connection limit, idle timeout 불일치를 먼저 본다.

ALB idle timeout이 60초이고, backend Node.js 서버의 keepAliveTimeout이 5초라고 하자.

0초:
요청 처리 완료. ALB와 backend 사이 연결 idle.
5초:
backend 서버가 먼저 연결 종료.
30초:
ALB는 아직 재사용 가능한 연결이라고 보고 새 요청 전달 시도.
결과:
이미 닫힌 연결에 요청이 들어가거나 RST가 발생.
산발적 502가 난다.

이 사례의 교훈은 “timeout을 무조건 길게”가 아니다. 연결 생애주기의 주도권을 어느 쪽이 갖는지 맞춰야 한다. 보통은 로드밸런서가 먼저 idle 연결을 관리하고, backend는 그보다 조금 긴 timeout을 갖게 만든다. 이 내용의 TCP 생애주기 원리는 앞선 tcp-flow-congestion.mdx가 더 자세히 다룬다.


9. Worked Scenario C - “DNS를 바꿨는데 일부 사용자만 실패한다”

섹션 제목: “9. Worked Scenario C - “DNS를 바꿨는데 일부 사용자만 실패한다””

사용자 신고:

배포 후 일부 사용자는 접속되고 일부 사용자는 예전 서버로 갑니다.

디버깅 문장:

resolver별 DNS 응답이 일시적으로 다르거나, 일부 클라이언트가 이전 TTL 캐시를 사용 중일 수 있다.

먼저 앱 인스턴스 문제로 들어가지 않는다. DNS가 어떤 IP를 주는지부터 비교한다.

Terminal window
dig @8.8.8.8 api.example.com +short
dig @1.1.1.1 api.example.com +short
dig @169.254.169.253 api.example.com +short
dig api.example.com | grep -A2 'ANSWER SECTION'

예:

8.8.8.8: 203.0.113.10
1.1.1.1: 203.0.113.20
VPC resolver: 203.0.113.20
TTL: 240

해석:

  1. DNS가 완전히 죽은 것은 아니다.
  2. resolver별 캐시 상태가 다르다.
  3. TTL이 남아 있으므로 4분가량 혼재가 정상 후보다.
  4. 이전 IP의 서버를 TTL 이상 살려둬야 한다.

TTL 86400초를 가진 레코드를 바로 바꾸면 일부 resolver는 최대 24시간 이전 IP를 줄 수 있다. 안전한 전환은 “변경 직전에 TTL을 낮춘다”가 아니라, 기존 긴 TTL이 만료될 시간을 먼저 기다리는 것이다.

T-48h:
TTL 86400 -> 300으로 변경
기존 86400 캐시가 빠질 시간을 둔다.
T-0:
IP 변경
새 TTL 300 기준으로 최대 5분 혼재를 기대한다.
T+1h:
안정 확인 뒤 TTL을 다시 높인다.

직전 DNS/CDN 문서에서 TTL, DNSSEC, CDN 캐시, Route 53 라우팅 정책을 깊게 다뤘다. 여기서는 그 지식을 다시 설명하기보다, resolver별 응답과 TTL을 네트워크 디버깅의 첫 관측 지점으로 사용한다.

9-3. 반례: dig +trace는 맞는데 앱은 틀리다

섹션 제목: “9-3. 반례: dig +trace는 맞는데 앱은 틀리다”

dig +trace는 권한 DNS까지의 위임 경로와 최종 응답을 보여준다. 그러나 실제 앱이 사용하는 resolver가 같은 답을 쓰는지는 별도 문제다.

dig +trace:
권한 DNS에는 새 IP가 있음.
앱 컨테이너:
/etc/resolv.conf가 CoreDNS를 가리킴.
CoreDNS 또는 node-local DNS cache가 이전 응답을 캐시 중.

그래서 Kubernetes 안에서는 Pod 내부에서 직접 dig를 실행해야 한다.

Terminal window
kubectl exec my-pod -- dig api.example.com +short
kubectl exec my-pod -- cat /etc/resolv.conf

Pod 내부와 외부 resolver가 다르면 CoreDNS, node-local-dns, search domain, ndots 옵션을 본다. 이때도 긴 CoreDNS 로그 런북보다 “앱이 실제로 묻는 resolver는 누구인가”가 먼저다.


10. Worked Scenario D - “VPC Flow Logs는 REJECT인데 Security Group은 열려 있다”

섹션 제목: “10. Worked Scenario D - “VPC Flow Logs는 REJECT인데 Security Group은 열려 있다””

Flow Logs에서 다음과 같은 흐름이 계속 보인다고 하자.

src=10.0.1.5 dst=10.0.2.10 srcport=54321 dstport=8080 protocol=6 action=REJECT

그리고 Security Group 인바운드에는 8080 허용 규칙이 있다.

서두르지 말고 질문을 바꾼다.

Security Group 인바운드가 열려 있는가?
예.
그럼 응답 경로의 NACL 아웃바운드와 ephemeral port도 열려 있는가?
아직 모른다.

Security Group은 stateful이고 NACL은 stateless다. 클라이언트가 54321 -> 8080으로 접속하면 서버의 응답은 8080 -> 54321로 돌아간다. NACL이 ephemeral port 범위를 막고 있으면 응답이 막힌다. 범위는 OS와 클라우드 정책마다 확인해야 하지만, 학습 예시로는 1024-65535 또는 리눅스의 /proc/sys/net/ipv4/ip_local_port_range에 잡힌 범위를 떠올리면 된다.

요청:
10.0.1.5:54321 -> 10.0.2.10:8080
응답:
10.0.2.10:8080 -> 10.0.1.5:54321
Security Group:
stateful이므로 허용된 요청의 응답을 기억한다.
NACL:
stateless이므로 응답 방향도 별도 규칙이 필요하다.

따라서 Flow Logs REJECT와 SG 허용이 동시에 참일 수 있다. 이것이 네트워크 디버깅에서 반례가 중요한 이유다. “SG가 열려 있으니 방화벽 문제는 아니다”는 틀린 결론이다.

상세 AWS CLI 런북은 부록으로 미루고, 본문에서는 관찰 포인트만 남긴다.

확인할 것:
대상 ENI의 Flow Logs action
소스/대상 subnet의 NACL inbound/outbound
요청 포트와 응답 ephemeral port 범위
Security Group stateful 규칙
라우팅 테이블과 return path

NACL을 수정하기 전에는 “어느 subnet의 어느 방향에서 어떤 port가 막혔는가”를 먼저 적는다. 그렇지 않으면 양방향으로 과하게 열어 문제를 숨길 수 있다.


11. Worked Scenario E - “tcpdump에는 패킷이 보이는데 앱 로그가 없다”

섹션 제목: “11. Worked Scenario E - “tcpdump에는 패킷이 보이는데 앱 로그가 없다””

tcpdump에서 SYN이 보이는데 앱 로그가 비어 있다면, 요청은 적어도 캡처 지점까지 왔다. 하지만 앱이 받았다는 뜻은 아니다.

가능한 지점:
NIC에서 수신됨
커널 네트워크 스택 진입
iptables 또는 nftables에서 DROP
conntrack 상태 생성 실패
소켓 backlog 초과
프로세스가 0.0.0.0이 아니라 127.0.0.1에만 LISTEN
Service/Pod 네임스페이스가 다름

먼저 LISTEN을 확인한다.

Terminal window
ss -tlnp

해석:

0.0.0.0:8080 LISTEN:
모든 인터페이스에서 받을 수 있다.
127.0.0.1:8080 LISTEN:
같은 네임스페이스의 loopback에서만 받을 수 있다.
외부 Pod나 ALB에서는 접근할 수 없다.
LISTEN 없음:
앱이 포트를 열지 않았거나 다른 포트에 떠 있다.

그다음 conntrack과 firewall을 본다.

Terminal window
sudo conntrack -C
sysctl net.netfilter.nf_conntrack_max

현재 conntrack 수가 max에 근접하면 새 연결이 상태를 만들지 못할 수 있다. 이때 증상은 “간헐적 timeout”, “Pod 재시작 때만 실패”, “대량 배포 직후 연결 실패”로 나타날 수 있다.

11-3. 반례: packet capture도 위치에 따라 다르다

섹션 제목: “11-3. 반례: packet capture도 위치에 따라 다르다”

호스트의 -i any에서 보인 패킷이 컨테이너 내부 eth0까지 들어갔다는 뜻은 아니다. bridge, veth, DNAT, network policy 경계를 지날 수 있다.

관측 지점을 명시한다.

host eth0:
노드의 외부 인터페이스까지 도달했는가
cni0 또는 bridge:
Pod 네트워크로 라우팅됐는가
Pod namespace eth0:
Pod 입장에서 수신됐는가
localhost:
앱이 loopback에만 묶여 있는가

이 구분이 없으면 “패킷이 보이는데 앱이 왜 못 받지”에서 멈춘다.


12. Worked Scenario F - “브라우저에서만 실패한다”

섹션 제목: “12. Worked Scenario F - “브라우저에서만 실패한다””

12-1. CORS는 서버 도달 여부와 다르다

섹션 제목: “12-1. CORS는 서버 도달 여부와 다르다”

CORS(Cross-Origin Resource Sharing)는 브라우저가 다른 origin으로 요청할 때 서버가 허용한 origin인지 확인하는 정책이다. CORS 에러가 났다고 해서 서버가 요청을 못 받았다는 뜻은 아니다.

브라우저는 실제 요청 전에 preflight OPTIONS 요청을 보낼 수 있다.

Terminal window
curl -X OPTIONS https://api.example.com/data \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization" \
-i

관찰할 헤더:

Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials

반증:

관찰의미
OPTIONS가 404/405preflight 경로가 서버에서 처리되지 않음
Allow-Origin 누락브라우저가 응답을 JS에 노출하지 않음
Authorization이 Allow-Headers에 없음인증 헤더가 있는 실제 요청이 차단됨
curl GET은 200, 브라우저는 CORS 에러서버 처리보다 브라우저 정책 후보가 큼

HTTPS 페이지에서 HTTP 리소스를 로드하면 브라우저는 mixed content로 차단할 수 있다. 서버에서 HTTP 리소스가 정상 응답해도 브라우저 보안 정책은 별개다.

TLS 종료 위치도 관찰을 흐리게 만든다.

클라이언트 --HTTPS--> ALB --HTTP--> Pod

이 구조에서 Pod 로그는 HTTP로 보일 수 있지만, 사용자는 HTTPS로 접속한다. 앱이 redirect URL이나 absolute URL을 만들 때 X-Forwarded-Proto를 잘못 읽으면 HTTPS 페이지 안에 HTTP 링크가 생길 수 있다. 이 문제는 packet capture보다 DevTools와 응답 헤더가 빠른 증거다.

브라우저 Network 탭의 Protocol이 h2인지 http/1.1인지 확인하면 ALPN 협상이 어떤 결과였는지 짐작할 수 있다. curl -v --http2에서도 ALPN 메시지를 볼 수 있다.

ALPN: server accepted h2
< HTTP/2 200

이 출력은 “브라우저가 HTTP/2를 전혀 쓰지 못한다”는 가설을 약하게 한다. 그러나 HTTP/2로 협상됐다고 해서 애플리케이션의 fetch가 병렬로 작성됐다는 뜻은 아니다. JavaScript가 순차적으로 await하면 Waterfall은 여전히 계단식으로 보일 수 있다.


13. 실패 신호: 증상에서 의심 계층으로 되돌아가기

섹션 제목: “13. 실패 신호: 증상에서 의심 계층으로 되돌아가기”
증상먼저 의심할 계층/개념관측 지점
Could not resolve hostDNS resolver, 레코드, search domaindig, /etc/resolv.conf
일부 사용자만 이전 IP로 접속DNS TTL, resolver cacheresolver별 dig, TTL
Connection refused포트 미청취, 즉시 RSTnc, ss -tlnp, tcpdump
Connection timeoutDROP, 라우팅, SG/NACL, SYN 재전송Flow Logs, packet capture, route
TLS certificate verify failed인증서 체인, SAN, SNIopenssl s_client -servername
ALPN이 기대와 다름HTTP/2 설정, TLS 종료 지점curl -v, DevTools Protocol
TTFB만 느림앱 처리, upstream, DB, target queuecurl -w, ALB log, trace
다운로드만 느림payload 크기, bandwidth, client pathcurl -w, CDN log, mtr
CLOSE-WAIT 증가앱 close 누락ss, app connection handling
TIME-WAIT 증가와 포트 고갈단기 연결 폭증, keep-alive 미사용ss, ephemeral port range
Flow Logs REJECTSG/NACL/network policyFlow Logs, SG/NACL
tcpdump SYN 보임, 앱 로그 없음local firewall, conntrack, bind addressss, conntrack, iptables/nftables
Pod 내부는 성공, 외부는 실패Ingress, ALB, external SG, DNSkubectl, ALB log, external curl
브라우저만 실패CORS, mixed content, cache, service workerDevTools, OPTIONS 재현

대역폭 자체가 가설일 때만 iperf3 같은 도구를 선택 부록 후보로 생각한다. 대부분의 웹 장애에서는 먼저 DNS/TCP/TLS/TTFB/payload 구간을 나눈 뒤, 다운로드 구간만 느리다는 증거가 있을 때 경로 대역폭 측정을 분리하는 편이 낫다.

단정왜 위험한가더 좋은 판단
”ping이 안 되니 서버가 죽었다”ICMP만 차단될 수 있다TCP 서비스 포트와 HTTP health를 별도 확인
”SG가 열려 있으니 방화벽은 아니다”NACL은 stateless이고 응답 포트를 막을 수 있다요청/응답 방향과 ephemeral port를 함께 본다
”curl -k가 되니 TLS는 정상이다”인증서 검증을 끈 것이다-k 없이 SAN/체인/SNI를 확인한다
”Flow Logs ACCEPT면 앱도 받았다”VPC 정책 통과와 앱 처리 성공은 다르다ALB log, ss, app log로 다음 경계를 본다
”tcpdump에 없으니 패킷이 없다”인터페이스 선택이나 필터가 틀릴 수 있다route와 namespace 기준으로 캡처 지점을 다시 고른다
”TIME-WAIT가 많으니 커널 값을 바꾸자”TIME-WAIT는 정상 종료 안전장치다포트 고갈, 연결 생성률, keep-alive를 먼저 본다
”mtr 중간 홉 손실이 장애다”중간 홉 ICMP rate limit일 수 있다이후 홉과 목적지 손실이 이어지는지 본다

Silent failure는 시스템 일부가 실패했지만 사용자 증상이나 상위 로그만으로는 즉시 드러나지 않는 상태다. 네트워크에서는 특히 흔하다.

Flow Logs REJECT:
앱 로그는 조용하지만 네트워크 정책은 거부 중.
Route 53 health check Healthy:
shallow health만 보고 실제 사용자 경로는 5xx일 수 있음.
ALB target healthy:
/health는 200이지만 특정 API의 upstream만 timeout일 수 있음.
DevTools from disk cache:
서버가 고쳐졌는데 브라우저가 이전 리소스를 보여줄 수 있음.
CoreDNS 일부 Pod만 지연:
외부 dig는 정상인데 클러스터 내부 이름 해석만 느릴 수 있음.

Silent failure를 줄이는 기준은 “상위 health 하나”가 아니라 경계별 관측을 맞추는 것이다.


14-1. 이 문서는 네트워크 도구 암기장이 아니다

섹션 제목: “14-1. 이 문서는 네트워크 도구 암기장이 아니다”

도구 옵션을 모두 외우는 것은 목표가 아니다. 처음에는 아래 질문만 잡으면 된다.

curl:
사용자 공간에서 HTTP 요청 단계가 어디까지 갔는가?
dig:
어떤 resolver가 어떤 IP와 TTL을 주는가?
openssl:
TLS 신뢰와 ALPN이 어떻게 협상됐는가?
ss:
로컬 커널과 프로세스가 연결을 어떤 상태로 보는가?
tcpdump:
캡처 지점까지 실제 패킷이 왔는가?
Flow Logs:
VPC 정책 경계에서 ACCEPT인가 REJECT인가?
ALB logs:
로드밸런서가 만든 에러인가, 타겟이 만든 에러인가?

14-2. 계층은 직선이 아니라 되돌아가는 지도다

섹션 제목: “14-2. 계층은 직선이 아니라 되돌아가는 지도다”

L7에서 L3으로 내려갔다가 다시 L7로 올라오는 일이 정상이다.

브라우저 502
-> ALB log 확인
-> target_status_code가 비어 있음
-> L4 연결 실패 의심
-> Flow Logs ACCEPT
-> tcpdump에서 SYN/SYN-ACK/RST 확인
-> 서버가 RST
-> 다시 앱 프로세스와 keep-alive timeout 확인

계층형이라는 말은 항상 아래로만 간다는 뜻이 아니다. 관측 지점을 바꿔 가며 가설의 공간을 줄인다는 뜻이다.

14-3. 클라우드와 Kubernetes는 별도 계층이 아니라 관측 지점을 늘린다

섹션 제목: “14-3. 클라우드와 Kubernetes는 별도 계층이 아니라 관측 지점을 늘린다”

AWS, Kubernetes, Docker는 네트워크 계층 위에 새로운 추상화를 만든다. 그러나 실패 원리는 여전히 DNS, routing, firewall, TCP, TLS, HTTP로 분해된다.

Kubernetes Service 실패:
DNS 이름 해석
Service selector와 EndpointSlice
kube-proxy 또는 CNI routing
Pod listener
NetworkPolicy
AWS ALB 실패:
DNS alias
listener rule
target group health
SG/NACL
backend protocol
target response

추상화 이름을 먼저 외우지 말고, 그 추상화가 어느 관측 지점을 추가하는지 본다.


15. 선택 기준: 다음에 무엇을 볼 것인가

섹션 제목: “15. 선택 기준: 다음에 무엇을 볼 것인가”
1. 이름이 IP로 풀리는가?
아니오 -> DNS resolver, TTL, zone, CoreDNS
예 -> 2
2. TCP 포트가 연결되는가?
connection refused -> listener, bind address, RST 방향
timeout -> SG/NACL, route, packet drop
예 -> 3
3. TLS가 검증되는가?
아니오 -> SNI, SAN, chain, TLS version, ALPN
예 -> 4
4. HTTP status와 TTFB는 어떤가?
5xx -> ALB/app/upstream 경계 분리
TTFB 높음 -> 앱 처리, target queue, DB/upstream
2xx인데 브라우저 실패 -> CORS/cache/mixed content
5. 앱 로그와 하위 관측이 모순되는가?
예 -> packet capture, Flow Logs, conntrack, namespace

15-2. 도구가 없는 컨테이너에서의 선택

섹션 제목: “15-2. 도구가 없는 컨테이너에서의 선택”

프로덕션 이미지는 curl, dig, tcpdump를 넣지 않는 경우가 많다. 이때 이미지를 바꾸는 것은 마지막 선택이다.

우선순위:

  1. 같은 네임스페이스에서 debug/ephemeral container를 붙인다.
  2. kubectl exec로 이미 있는 최소 도구를 쓴다.
  3. 호스트에서 nsenter로 해당 network namespace에 들어간다.
  4. 그래도 안 되면 스테이징 재현이나 임시 디버그 이미지를 쓴다.

각 방법의 한계:

방법장점한계
kubectl exec실제 컨테이너 내부 관점도구가 없을 수 있음
Ephemeral Container도구를 임시로 추가파일시스템/프로세스 관점은 다를 수 있음
nsenter호스트 도구 사용 가능노드 접근 권한 필요, 캡처 지점 주의
이미지 수정재현과 반복 관찰 쉬움배포 변경이 되어 장애 상황을 바꿀 수 있음

packet capture는 강력하지만 마지막 근거에 가깝다. 켜기 전에 아래를 적는다.

캡처 목적:
10.0.1.5 -> 10.0.2.10:8080 SYN/SYN-ACK/RST 확인
캡처 위치:
backend node eth0
필터:
host 10.0.1.5 and port 8080
종료 조건:
20 packets 또는 30초
민감정보 위험:
TLS 443 헤더 위주. HTTP 평문 payload 캡처 금지.

이렇게 쓰면 캡처 결과가 비어 있을 때도 해석이 쉬워진다. 빈 결과는 “트래픽 없음”일 수도 있고, “캡처 위치가 틀림”, “필터가 과함”, “DNS가 다른 IP를 가리킴”일 수도 있다.


16. 선택 부록: 짧은 관찰 스니펫

섹션 제목: “16. 선택 부록: 짧은 관찰 스니펫”

이 부록은 본문 이해에 필수는 아니다. 실제 장애에서 관측 지점을 빠르게 잡기 위한 최소 스니펫만 남긴다. 긴 복구 절차나 서비스별 설정 변경은 각 운영 문서로 분리하는 편이 낫다.

DNS 관찰
Terminal window
dig api.example.com +short
dig @8.8.8.8 api.example.com +short
dig @1.1.1.1 api.example.com +short
dig api.example.com | grep -A2 'ANSWER SECTION'
dig api.example.com +trace

관찰:

resolver별 응답이 같은가?
TTL이 얼마나 남았는가?
NXDOMAIN, SERVFAIL, NOERROR/NODATA를 구분했는가?
Pod 내부 resolver와 외부 resolver가 같은 답을 주는가?
TCP 포트와 소켓 상태
Terminal window
nc -zv api.example.com 443
ss -tlnp
ss -tan
ss -tn state established
ss -tn state time-wait

관찰:

Connection refused:
원격이 즉시 거부하거나 포트가 닫힘.
Connection timeout:
DROP, 라우팅, SG/NACL, SYN 응답 없음 후보.
LISTEN 127.0.0.1:8080:
외부 네임스페이스에서 접근 불가.
CLOSE-WAIT 증가:
애플리케이션 close 누락 후보.
curl 단계별 타이밍
Terminal window
curl -o /dev/null -s -w \
"dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code}\n" \
https://api.example.com/health

구간 계산:

DNS = time_namelookup
TCP = time_connect - time_namelookup
TLS = time_appconnect - time_connect
TTFB wait = time_starttransfer - time_appconnect
Download = time_total - time_starttransfer

관찰:

connect만 느림 -> L4/L3
appconnect만 느림 -> TLS
starttransfer만 느림 -> 앱/업스트림/LB target
total만 느림 -> payload 또는 다운로드 경로
TLS와 ALPN 관찰
Terminal window
openssl s_client -connect api.example.com:443 -servername api.example.com -alpn h2 </dev/null
curl -v --http2 https://api.example.com/health

관찰:

Verify return code: 0:
인증서 체인 검증 성공.
ALPN protocol: h2:
TLS 협상에서 HTTP/2 선택.
certificate verify failed:
SAN, chain, 만료, trust store 확인.
handshake failure:
TLS 버전, cipher, SNI, mTLS 요구 확인.
packet capture
Terminal window
sudo tcpdump -i eth0 -n 'host 10.0.1.5 and port 8080' -c 20
sudo tcpdump -i eth0 -n 'tcp[13] & 4 != 0' -c 10
sudo tcpdump -i eth0 -n -w /tmp/api.pcap 'host 10.0.1.5 and port 443'
tcpdump -r /tmp/api.pcap -n -c 20

관찰:

SYN만 반복:
응답 경로, listener, 방화벽 후보.
SYN -> SYN-ACK -> ACK:
TCP 연결 자체는 수립됨.
RST 방향:
누가 강제로 끊는지 확인.
캡처 없음:
인터페이스, namespace, route, 필터, DNS IP 확인.
traceroute와 mtr
Terminal window
traceroute api.example.com
traceroute -T -p 443 api.example.com
mtr -r -c 100 api.example.com
mtr -T -P 443 -r -c 100 api.example.com

관찰:

중간 홉 * * *:
ICMP 차단일 수 있음. 이후 홉이 정상인지 본다.
목적지 Loss%:
사용자 트래픽 손실과 더 가까운 신호.
StDev 증가:
jitter 후보. 실시간 서비스 영향 확인.
AWS 경계 관찰
VPC Flow Logs:
fields @timestamp, srcAddr, dstAddr, srcPort, dstPort, action
| filter action = "REJECT"
| sort @timestamp desc
| limit 100
ALB access log:
elb_status_code
target_status_code
request_processing_time
target_processing_time
response_processing_time

관찰:

Flow Logs REJECT:
SG/NACL/route/network policy 경계 확인.
target_status_code=-:
ALB가 타겟 응답을 못 받음.
target_processing_time 높음:
타겟 앱 또는 upstream 대기 후보.

주의:

상세 AWS CLI로 바로 수정하지 않는다.
먼저 어느 경계가 어떤 방향으로 막혔는지 기록한다.
Kubernetes 관찰
Terminal window
kubectl exec my-pod -- cat /etc/resolv.conf
kubectl exec my-pod -- nslookup my-service.default.svc.cluster.local
kubectl exec my-pod -- curl -s -o /dev/null -w "code=%{http_code}\n" http://my-service:8080/health
kubectl debug -it my-pod --image=nicolaka/netshoot --target=my-container

관찰:

Pod 내부 Service 성공, 외부 실패:
Ingress, ALB, DNS, external SG 후보.
Pod IP 성공, Service 실패:
Service selector, EndpointSlice, kube-proxy 후보.
debug container 성공, app container 실패:
파일시스템/환경변수/process 권한 차이 확인.
conntrack 관찰
Terminal window
sudo conntrack -C
sysctl net.netfilter.nf_conntrack_max
sudo conntrack -L | grep '10.0.1.5'

관찰:

current ~= max:
새 연결 상태 생성 실패 후보.
특정 목적지 흐름 폭증:
connection pool, retry storm, NAT, idle timeout 확인.

주의:

max만 키우면 증상이 늦게 다시 온다.
연결 생성률과 retry 원인을 함께 본다.

  1. 네트워크 디버깅은 명령어 암기가 아니라 관측 지점으로 가설을 반증하는 루프다.
  2. OSI 모델은 실제 구현도라기보다 “어느 계층의 약속이 깨졌는가”를 묻는 학습 지도다.
  3. curl -w는 DNS, TCP, TLS, TTFB를 누적 시간으로 보여준다. 구간 시간은 빼서 계산한다.
  4. dig는 resolver별 답과 TTL을 비교한다. DNS propagation은 대개 캐시 만료의 문제다.
  5. openssl s_client는 SNI, 인증서, TLS 버전, ALPN을 확인한다. curl -k 성공은 TLS 정상의 증거가 아니다.
  6. ss는 로컬 소켓 상태를 보여준다. TIME-WAIT와 CLOSE-WAIT는 의미가 다르다.
  7. tcpdump는 캡처 지점까지 온 실제 패킷을 보여준다. BPF로 좁게 필터링해야 안전하다.
  8. VPC Flow Logs의 ACCEPT/REJECT는 네트워크 정책 경계의 증거다. 앱 처리 성공 여부와는 다르다.
  9. ALB access log는 로드밸런서가 만든 오류와 타겟이 만든 오류를 분리한다.
  10. 브라우저 DevTools는 CORS, mixed content, cache 같은 브라우저 정책을 보는 관측 지점이다.


목표는 성공/실패를 한 번에 맞히는 것이 아니라, 각 출력이 어떤 가설을 줄이는지 말해보는 것이다.

Terminal window
TARGET="api.example.com"
dig "$TARGET" +short
nc -zv "$TARGET" 443
openssl s_client -connect "$TARGET:443" -servername "$TARGET" -alpn h2 </dev/null
curl -o /dev/null -s -w \
"dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} code=%{http_code}\n" \
"https://$TARGET/health"

예상 해석:

dig가 IP를 반환한다:
이름 해석은 적어도 현재 resolver에서는 성공했다.
nc가 succeeded를 반환한다:
TCP 443 연결은 수립 가능하다.
openssl Verify return code가 0이다:
이 클라이언트 기준 인증서 체인은 검증된다.
curl code가 200이고 ttfb가 낮다:
이 경로와 health endpoint는 정상 기준선으로 쓸 수 있다.

다음 변형 질문:

1. resolver를 8.8.8.8과 1.1.1.1로 바꾸면 답이 같은가?
2. `--resolve`로 특정 IP를 강제하면 결과가 달라지는가?
3. `curl -w`에서 connect만 느린가, starttransfer만 느린가?
4. 브라우저 DevTools의 Protocol과 curl의 ALPN 결과가 같은가?
5. 앱 로그가 비어 있다면 Flow Logs나 packet capture는 무엇을 말하는가?