네트워크 디버깅
네트워크 디버깅: 장애를 체계적으로 찾아내는 기술
섹션 제목: “네트워크 디버깅: 장애를 체계적으로 찾아내는 기술”1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”네트워크 디버깅이란 “어느 레이어에서 무엇이 왜 깨졌는가”를 OSI 계층 구조를 기준으로 위→아래(또는 아래→위)로 좁혀가며 정확히 지목하는 과정이다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”- 증상은 위에서, 원인은 아래에 있다: “API가 안 된다”는 L7 증상이지만 원인은 DNS 오설정(L3), Security Group 누락(방화벽), TCP 타임아웃(L4) 어디에든 있을 수 있다.
- 추측 디버깅은 MTTR을 늘린다: 원인을 모른 채 서버 재시작, 배포 롤백을 반복하면 장애가 길어진다. 계층별 체크리스트가 있으면 10분 안에 좁혀진다.
- 플랫폼 엔지니어의 핵심 역량: SRE/플랫폼 팀의 가치는 장애 시 “무엇을 봐야 하는지 바로 아는 것”이다.
- 프론트엔드 경험이 자산이다: 브라우저 DevTools에서 보던 Network 탭 지식을 서버 사이드 도구와 연결하면 풀스택 디버깅이 가능해진다.
3. 멘탈 모델: OSI 레이어별 점검 순서
섹션 제목: “3. 멘탈 모델: OSI 레이어별 점검 순서”3-1. 레이어별 담당 영역과 점검 도구
섹션 제목: “3-1. 레이어별 담당 영역과 점검 도구”OSI Layer 담당 영역 점검 도구─────────────────────────────────────────────────────────L7 App HTTP 응답 코드, 헤더, 바디 curl -v, 브라우저 DevToolsL6 Pres TLS 인증서, 암호화 openssl s_client, WiresharkL5 Session 세션 유지, 쿠키 curl --cookie, 브라우저L4 Transport TCP 연결, 포트, 소켓 상태 ss, netstat, tcpdumpL3 Network IP 라우팅, ICMP ping, traceroute, mtrL2 Data ARP, MAC, VLAN arp, ip neigh (컨테이너 브릿지)L1 Physical 케이블, NIC, 링크 상태 ethtool, ip link3-2. 계층적 좁히기 전략
섹션 제목: “3-2. 계층적 좁히기 전략”증상: “API 서버에 연결이 안 된다”
1단계 (L3) - 기본 연결 확인 ping api.example.com → 응답 없음 → DNS 문제 또는 방화벽(ICMP 차단)
2단계 (DNS) - 이름 해석 확인 dig api.example.com → NXDOMAIN → DNS 설정 오류 → 올바른 IP 반환 → L4로 진행
3단계 (L4) - 포트 열려 있는지 확인 nc -zv api.example.com 443 telnet api.example.com 443 → Connection refused → 서버 미기동 또는 방화벽 → Connection timeout → 방화벽(패킷 DROP) → Connected → L7로 진행
4단계 (L7) - HTTP 레벨 확인 curl -v https://api.example.com/health → 연결은 되나 503 → 앱 또는 로드밸런서 문제 → TLS 에러 → 인증서 문제4. tcpdump: 패킷을 직접 본다
섹션 제목: “4. tcpdump: 패킷을 직접 본다”4-1. 기본 사용법
섹션 제목: “4-1. 기본 사용법”# 인터페이스 eth0에서 모든 패킷 캡처 (화면 출력)sudo tcpdump -i eth0
# 특정 호스트의 패킷만sudo tcpdump -i eth0 host 10.0.1.100
# 특정 포트만sudo tcpdump -i eth0 port 443
# 파일로 저장 (Wireshark에서 열 수 있음)sudo tcpdump -i eth0 -w /tmp/capture.pcap
# 저장된 파일 읽기 (-n: DNS 역조회 안 함, -v: 상세)sudo tcpdump -r /tmp/capture.pcap -n -v4-2. BPF(Berkeley Packet Filter) 필터 문법
섹션 제목: “4-2. BPF(Berkeley Packet Filter) 필터 문법”BPF는 커널 레벨에서 필터링하므로 불필요한 패킷이 유저스페이스로 올라오지 않는다. 트래픽이 많은 환경에서 필수다.
BPF가 커널 레벨에서 동작하는 이유:
tcpdump 없이: 모든 패킷 → NIC → 커널 → 유저스페이스(tcpdump) → 필터 적용 → 출력 → 1Gbps 링크에서 수십만 패킷/초를 전부 유저스페이스로 복사 → CPU 폭발
BPF 필터 사용: 모든 패킷 → NIC → 커널(BPF 가상머신에서 즉시 필터) → 매칭된 것만 → 유저스페이스 → CPU 부하 최소화, 프로덕션에서도 안전하게 사용 가능
BPF 컴파일 과정: "port 443 and host 10.0.1.5" ↓ libpcap이 BPF 바이트코드로 컴파일 ldh [12] # ethertype 로드 jeq #0x800 # IPv4인지 확인 ld [26] # src IP 로드 jeq #10.0.1.5 # IP 매칭 ... # port 443 확인 ↓ 커널 BPF 가상머신에서 실행 매칭된 패킷만 복사📖 더 보기: BPF: The Forgotten Bytecode — Cloudflare — BPF 가상머신 내부 구조, JIT 컴파일, eBPF로의 발전. 이 섹션의 BPF 필터링 원리와 직접 연결됨
# 조합 연산자: and (&&), or (||), not (!)
# 특정 호스트와 포트 조합sudo tcpdump -i eth0 host 10.0.1.5 and port 8080
# 출발지 IP 지정sudo tcpdump -i eth0 src 10.0.1.5
# 목적지 포트 지정sudo tcpdump -i eth0 dst port 80
# SYN 패킷만 (TCP 플래그 필터링)sudo tcpdump -i eth0 'tcp[13] == 2'
# SYN+ACK 패킷만sudo tcpdump -i eth0 'tcp[13] == 18'
# RST 패킷만 (연결 강제 종료 감지)sudo tcpdump -i eth0 'tcp[13] & 4 != 0'
# HTTP GET 요청만 (페이로드 패턴 매칭)sudo tcpdump -i eth0 -A 'tcp port 80 and (tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x47455420)'
# ICMP만sudo tcpdump -i eth0 icmp
# 특정 서브넷sudo tcpdump -i eth0 net 10.0.0.0/8
# 루프백 제외sudo tcpdump -i eth0 not lo4-3. 실무 활용 시나리오
섹션 제목: “4-3. 실무 활용 시나리오”시나리오 1: 연결이 갑자기 끊긴다 (RST 탐지)
# RST를 보내는 쪽 확인sudo tcpdump -i eth0 'tcp[13] & 4 != 0' -n -v
# 예상 출력# 10.0.1.5.45231 > 10.0.1.10.8080: Flags [R.], seq 0, ack 1, win 0# → 서버가 RST를 보내고 있음 → 앱 크래시 or 연결 수 초과시나리오 2: 3-way handshake 확인
sudo tcpdump -i eth0 'port 8080 and (tcp[13] == 2 or tcp[13] == 18 or tcp[13] == 16)' -n
# SYN(2) → SYN-ACK(18) → ACK(16) 순서로 나와야 정상# SYN만 계속 나오고 SYN-ACK가 없으면 → 서버가 응답 안 함시나리오 3: 레이턴시 측정
# -tt: 타임스탬프 출력, 같은 seq 번호 찾아서 RTT 계산sudo tcpdump -i eth0 -tt port 8080 | head -204-4. 유용한 옵션 정리
섹션 제목: “4-4. 유용한 옵션 정리”-i any # 모든 인터페이스-n # DNS 역조회 하지 않음 (빠름)-nn # IP도 이름으로 변환하지 않음-v / -vv / -vvv # 상세도 증가-c 100 # 100개만 캡처 후 종료-s 0 # 전체 패킷 캡처 (기본은 65535 바이트로 이미 충분)-A # ASCII로 페이로드 출력-X # HEX + ASCII 출력-q # 간략 출력5. Wireshark: GUI 패킷 분석기
섹션 제목: “5. Wireshark: GUI 패킷 분석기”5-1. 캡처 필터 vs 디스플레이 필터
섹션 제목: “5-1. 캡처 필터 vs 디스플레이 필터”두 가지를 혼동하면 안 된다. 문법도 다르다.
| 구분 | 적용 시점 | 문법 | 예시 |
|---|---|---|---|
| 캡처 필터 | 수집 전 | BPF 문법 | port 443 |
| 디스플레이 필터 | 수집 후 분석 | Wireshark 전용 | http.response.code == 200 |
캡처 필터 (BPF 문법, tcpdump와 동일)
port 443host 10.0.1.5src port 80 and dst host 10.0.1.5not port 22디스플레이 필터 (Wireshark 전용)
# HTTP 관련http.response.code == 500http.request.method == "POST"http.host contains "api.example.com"
# TCP 관련tcp.flags.reset == 1 # RST 패킷tcp.flags.syn == 1 # SYN 패킷tcp.analysis.retransmission # 재전송 패킷tcp.analysis.out_of_order # 순서 바뀐 패킷tcp.port == 8080
# TLS 관련tls.handshake.type == 1 # ClientHellotls.handshake.type == 2 # ServerHellotls.alert # TLS 에러
# DNSdns.qry.name contains "example.com"dns.flags.response == 15-2. TCP 스트림 추적 (Follow TCP Stream)
섹션 제목: “5-2. TCP 스트림 추적 (Follow TCP Stream)”특정 TCP 연결의 전체 대화를 순서대로 보는 기능. 패킷 개별로 보는 것보다 훨씬 직관적이다.
방법:1. 패킷 목록에서 원하는 패킷 우클릭2. Follow → TCP Stream 선택3. 빨간색 = 클라이언트 → 서버, 파란색 = 서버 → 클라이언트실무 활용: HTTP/1.1 요청/응답을 평문으로 보거나, 잘못된 헤더를 정확히 확인할 때.
5-3. TLS 복호화
섹션 제목: “5-3. TLS 복호화”프로덕션에서는 불가능하지만 개발/스테이징에서 HTTPS 패킷을 복호화할 수 있다.
방법 1: 서버 Private Key 이용 (TLS 1.2까지, RSA 키 교환만)
Wireshark → Edit → Preferences → Protocols → TLS→ RSA keys list에 서버 IP, 포트, 프로토콜, 키 파일 경로 입력방법 2: SSLKEYLOGFILE (권장, TLS 1.3 포함)
# 브라우저나 curl이 세션 키를 파일에 기록export SSLKEYLOGFILE=/tmp/sslkeys.logcurl https://api.example.com/data
# Wireshark → Edit → Preferences → Protocols → TLS# → (Pre)-Master-Secret log filename에 /tmp/sslkeys.log 입력6. curl 심화: HTTP 레벨 디버깅
섹션 제목: “6. curl 심화: HTTP 레벨 디버깅”6-1. -v 로 헤더 전체 보기
섹션 제목: “6-1. -v 로 헤더 전체 보기”curl -v https://api.example.com/health
# 출력 해석# * Trying 54.12.34.56:443... → DNS 해석 결과, TCP 연결 시도# * Connected to api.example.com → TCP 연결 성공# * SSL connection using TLSv1.3 → TLS 버전# > GET /health HTTP/2 → 송신 요청 헤더 (> 는 클라이언트 → 서버)# < HTTP/2 200 → 수신 응답 (< 는 서버 → 클라이언트)# < content-type: application/json6-2. -w 포맷으로 타이밍 분석
섹션 제목: “6-2. -w 포맷으로 타이밍 분석”curl -o /dev/null -s -w "DNS 조회: %{time_namelookup}sTCP 연결: %{time_connect}sTLS 핸드셰이크: %{time_appconnect}s첫 바이트 수신: %{time_starttransfer}s총 소요: %{time_total}sHTTP 상태코드: %{http_code}전송 크기: %{size_download} bytes" https://api.example.com/health출력 예시 및 해석
DNS 조회: 0.002s ← 빠름 (캐시 히트)TCP 연결: 0.050s ← 정상 (같은 리전 내 ~50ms)TLS 핸드셰이크: 0.120s ← 약간 높음 (인증서 체인 검증)첫 바이트 수신: 0.350s ← TTFB, 서버 처리 시간 포함총 소요: 0.360sHTTP 상태코드: 200전송 크기: 1024 bytestime_appconnect - time_connect = TLS 핸드셰이크 시간
time_starttransfer - time_appconnect = 서버 처리 시간 (TTFB)
6-3. —resolve: DNS 우회해서 특정 서버 직접 테스트
섹션 제목: “6-3. —resolve: DNS 우회해서 특정 서버 직접 테스트”# 로드밸런서를 거치지 않고 특정 서버 인스턴스만 테스트curl --resolve api.example.com:443:10.0.1.15 https://api.example.com/health
# 배포 전 새 서버 검증에 유용# /etc/hosts 수정 없이 임시로 IP를 오버라이드6-4. 기타 자주 쓰는 옵션
섹션 제목: “6-4. 기타 자주 쓰는 옵션”# TLS 인증서 검증 무시 (개발 환경 자체 서명 인증서)curl -k https://localhost:8443/health
# 헤더만 출력 (-I: HEAD 요청)curl -I https://api.example.com
# 응답 바디 버리고 헤더만 (GET 유지)curl -s -o /dev/null -D - https://api.example.com/health
# POST + JSON 바디curl -X POST -H "Content-Type: application/json" \ -d '{"key":"value"}' \ https://api.example.com/data
# Authorization 헤더curl -H "Authorization: Bearer $TOKEN" https://api.example.com/me
# 프록시 경유 (Burp Suite 같은 디버깅 프록시)curl -x http://localhost:8080 https://api.example.com/health
# 리다이렉트 따라가기curl -L https://api.example.com/redirect
# 타임아웃 설정curl --connect-timeout 5 --max-time 30 https://api.example.com/health
# 특정 TLS 버전 강제curl --tlsv1.2 https://api.example.com/health
# HTTP/1.1 강제 (HTTP/2 비교 테스트)curl --http1.1 https://api.example.com/health7. dig / nslookup: DNS 문제 진단
섹션 제목: “7. dig / nslookup: DNS 문제 진단”7-1. dig 기본 사용법
섹션 제목: “7-1. dig 기본 사용법”# A 레코드 조회dig api.example.com
# 특정 레코드 타입dig api.example.com MX # 메일 서버dig api.example.com AAAA # IPv6dig api.example.com CNAME # 별칭dig api.example.com TXT # 텍스트 (SPF, DKIM 등)dig api.example.com NS # 네임서버
# 간결한 출력 (+short)dig api.example.com +short# 출력: 54.12.34.56
# 특정 DNS 서버에 질의 (@)dig @8.8.8.8 api.example.com # Google DNSdig @1.1.1.1 api.example.com # Cloudflare DNSdig @169.254.169.253 api.example.com # AWS VPC DNS (메타데이터 서버)
# 전체 해석 경로 추적 (+trace)dig api.example.com +trace7-2. DNS 문제 진단 시나리오
섹션 제목: “7-2. DNS 문제 진단 시나리오”시나리오 1: 내부 DNS는 되는데 외부는 안 된다
# 로컬 DNS 서버 확인cat /etc/resolv.conf# nameserver 10.0.0.2 ← VPC DNS 서버
# VPC DNS로 질의dig @10.0.0.2 api.example.com
# 외부 DNS로도 질의 비교dig @8.8.8.8 api.example.com
# 결과 다르면 내부 DNS Zone 설정 문제# 결과 같은데 앱만 안 되면 앱의 DNS 캐시 TTL 문제시나리오 2: Kubernetes 내부 서비스 DNS
# Pod 안에서 서비스 이름으로 DNS 확인dig my-service.default.svc.cluster.local
# CoreDNS 서버 확인cat /etc/resolv.conf# nameserver 10.96.0.10 ← CoreDNS 클러스터 IP
# CoreDNS가 응답 안 하면kubectl get pods -n kube-system -l k8s-app=kube-dnskubectl logs -n kube-system -l k8s-app=kube-dns시나리오 3: TTL 확인 (캐시 만료 시간)
dig api.example.com | grep -A2 'ANSWER SECTION'# api.example.com. 300 IN A 54.12.34.56# ↑ TTL 300초 → 최대 5분간 이전 IP 캐시됨# 배포 후 DNS 변경이 반영 안 될 때 TTL 확인 필수7-3. nslookup (Windows 환경 및 빠른 확인용)
섹션 제목: “7-3. nslookup (Windows 환경 및 빠른 확인용)”nslookup api.example.comnslookup api.example.com 8.8.8.8 # 특정 DNS 서버 지정
# 역방향 조회 (IP → 도메인)nslookup 54.12.34.568. ss / netstat: 소켓 상태 확인
섹션 제목: “8. ss / netstat: 소켓 상태 확인”8-1. ss (Socket Statistics) — netstat의 현대적 대안
섹션 제목: “8-1. ss (Socket Statistics) — netstat의 현대적 대안”# 모든 TCP 연결ss -t
# 모든 소켓 (TCP + UDP + Unix)ss -a
# LISTEN 중인 포트만ss -tlnp# -t: TCP, -l: LISTEN, -n: 숫자 IP/포트, -p: 프로세스 정보
# 예상 출력# State Recv-Q Send-Q Local Address:Port Peer Address:Port Process# LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=1234,fd=15))# ESTAB 0 0 10.0.1.5:8080 10.0.0.1:54321 users:(("node",pid=1234,fd=25))
# UDP 소켓ss -un
# 특정 포트 필터링ss -tnp sport = :8080ss -tnp dport = :443
# 연결 수 집계ss -tn state established | wc -l8-2. TIME_WAIT 모니터링
섹션 제목: “8-2. TIME_WAIT 모니터링”TIME_WAIT는 TCP 연결 종료 후 같은 소켓을 재사용하지 않기 위해 보통 60초간 대기하는 상태다. 고트래픽 환경에서 포트 고갈을 일으킬 수 있다.
# TIME_WAIT 개수 확인ss -tn state time-wait | wc -l
# TIME_WAIT 상세 목록ss -tn state time-wait
# 상태별 집계ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn# 출력 예시:# 1250 ESTAB# 342 TIME-WAIT# 12 CLOSE-WAIT# 1 State
# CLOSE_WAIT 폭증 → 서버가 연결을 제때 닫지 않음 → 코드 버그# TIME_WAIT 폭증 → 짧은 수명의 연결이 많음 → Connection Pooling 검토TIME_WAIT 줄이는 커널 파라미터
# 현재 설정 확인sysctl net.ipv4.tcp_tw_reusesysctl net.ipv4.ip_local_port_range
# 로컬 포트 범위 확대 (기본 32768-60999)sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"
# TIME_WAIT 소켓 재사용 허용sudo sysctl -w net.ipv4.tcp_tw_reuse=1
# 영구 적용echo "net.ipv4.tcp_tw_reuse=1" >> /etc/sysctl.conf8-3. netstat (레거시 환경 대비)
섹션 제목: “8-3. netstat (레거시 환경 대비)”# LISTEN 포트netstat -tlnp
# 모든 연결netstat -an
# 상태별 집계netstat -an | awk '/^tcp/ {print $6}' | sort | uniq -c
# 특정 포트 연결만netstat -an | grep :80809. traceroute / mtr: 경로 추적과 패킷 로스 진단
섹션 제목: “9. traceroute / mtr: 경로 추적과 패킷 로스 진단”9-1. traceroute 기본
섹션 제목: “9-1. traceroute 기본”# 기본 사용 (UDP 프로브)traceroute api.example.com
# ICMP 사용 (방화벽에서 UDP 막을 때)traceroute -I api.example.com
# TCP SYN 사용 (80 포트, 방화벽 통과율 높음)traceroute -T -p 443 api.example.com
# IPv6traceroute6 api.example.com
# 출력 해석# 1 10.0.0.1 (10.0.0.1) 0.5 ms 0.4 ms 0.4 ms ← 게이트웨이 (1홉)# 2 * * * ← ICMP 차단 (방화벽)# 3 52.93.0.1 (52.93.0.1) 1.2 ms 1.1 ms 1.3 ms ← AWS 백본# 4 api.example.com 2.0 ms 1.9 ms 2.1 ms ← 목적지* * * 해석: ICMP TTL exceeded를 해당 홉이 차단한 것. 다음 홉이 응답한다면 경로는 살아있다. 마지막 홉이 * * *이면 방화벽 또는 서버 문제.
9-2. mtr (My TraceRoute): 실시간 경로 모니터링
섹션 제목: “9-2. mtr (My TraceRoute): 실시간 경로 모니터링”traceroute를 지속적으로 실행하며 패킷 로스율과 레이턴시를 집계한다.
# 인터랙티브 모드mtr api.example.com
# 보고서 모드 (100회 후 종료)mtr -r -c 100 api.example.com
# TCP 모드 (443 포트)mtr -T -P 443 api.example.com
# 출력 해석# Host Loss% Snt Last Avg Best Wrst StDev# 1. 10.0.0.1 0.0% 100 0.4 0.4 0.3 1.1 0.1# 2. ??? ← ICMP 차단# 3. 52.93.0.1 0.0% 100 1.2 1.1 1.0 2.3 0.1# 4. api.example.com 5.0% 100 2.1 2.3 1.9 12.4 0.8 ← 5% 로스!패킷 로스 해석: 중간 홉의 로스는 ICMP rate limiting일 수 있다. 이후 홉들의 로스가 없으면 무시해도 된다. 목적지 홉의 로스는 실제 문제다.
10. AWS 환경 디버깅
섹션 제목: “10. AWS 환경 디버깅”10-1. VPC Flow Logs
섹션 제목: “10-1. VPC Flow Logs”VPC에서 오가는 모든 IP 트래픽을 기록한다. tcpdump는 서버 안에서만 보지만, Flow Logs는 VPC 경계에서 본다 — Security Group이 패킷을 드랍한 것도 확인 가능.
Flow Logs 활성화
AWS Console → VPC → Flow Logs → Create대상: CloudWatch Logs 또는 S3필터: All (Accept + Reject 모두)Flow Log 레코드 형식
version account-id interface-id src-addr dst-addr srcport dstport protocol packets bytes start end action log-status
예시:2 123456789012 eni-abc12345 10.0.1.5 10.0.2.10 54321 8080 6 10 5000 1617000000 1617000060 ACCEPT OK2 123456789012 eni-abc12345 10.0.1.5 10.0.2.10 54322 8080 6 0 0 1617000000 1617000060 REJECT OKCloudWatch Insights로 분석
-- 거부된 트래픽 찾기fields @timestamp, srcAddr, dstAddr, srcPort, dstPort, action| filter action = "REJECT"| sort @timestamp desc| limit 100
-- 특정 IP의 트래픽 볼륨fields @timestamp, srcAddr, dstAddr, bytes| filter srcAddr = "10.0.1.5"| stats sum(bytes) by dstAddr10-2. Security Group 체크리스트
섹션 제목: “10-2. Security Group 체크리스트”가장 흔한 연결 실패 원인. “Security Group이 막고 있다”는 Flow Log에서 REJECT로 확인한다.
# AWS CLI로 인바운드 규칙 확인aws ec2 describe-security-groups \ --group-ids sg-12345678 \ --query 'SecurityGroups[0].IpPermissions' \ --output table
# 특정 포트의 인바운드 허용 여부 확인aws ec2 describe-security-groups \ --filters "Name=group-id,Values=sg-12345678" \ --query 'SecurityGroups[0].IpPermissions[?ToPort==`8080`]'Security Group vs NACL
| 항목 | Security Group | Network ACL |
|---|---|---|
| 적용 범위 | 인스턴스 레벨 | 서브넷 레벨 |
| 상태 추적 | Stateful (응답 자동 허용) | Stateless (인/아웃 별도) |
| 기본 정책 | 인바운드 거부, 아웃바운드 허용 | 허용 |
| 로그 | Flow Logs에서 REJECT | Flow Logs에서 REJECT |
10-3. ALB 액세스 로그
섹션 제목: “10-3. ALB 액세스 로그”ALB에서 L7 레벨의 요청/응답을 기록한다. 서버에서 로그가 안 보이면 ALB 단에서 막힌 것.
ALB 액세스 로그 활성화
AWS Console → EC2 → Load Balancers → 선택 → Attributes→ Access logs → Enable → S3 bucket 지정로그 형식 주요 필드
type time elb client:port target:port request_processing_timetarget_processing_time response_processing_time elb_status_codetarget_status_code received_bytes sent_bytes request user_agent ssl_cipher ssl_protocolAthena로 ALB 로그 분석
-- 5xx 에러가 많은 타겟 찾기SELECT target_ip, target_port, target_status_code, COUNT(*) as countFROM alb_logsWHERE target_status_code >= 500 AND time > '2024-01-01T00:00:00Z'GROUP BY target_ip, target_port, target_status_codeORDER BY count DESCLIMIT 20;
-- 느린 요청 찾기 (TTFB > 1초)SELECT request_url, target_processing_time, client_ipFROM alb_logsWHERE target_processing_time > 1.0ORDER BY target_processing_time DESCLIMIT 50;11. 컨테이너 환경 디버깅
섹션 제목: “11. 컨테이너 환경 디버깅”11-1. nsenter: 컨테이너 네트워크 네임스페이스 진입
섹션 제목: “11-1. nsenter: 컨테이너 네트워크 네임스페이스 진입”컨테이너는 별도 네트워크 네임스페이스를 갖는다. 호스트에서 tcpdump를 실행하면 컨테이너 가상 인터페이스(veth)까지 보인다.
# 컨테이너 PID 확인docker inspect my-container --format '{{.State.Pid}}'# 출력: 12345
# 컨테이너 네트워크 네임스페이스에서 명령 실행sudo nsenter -t 12345 -n ip addrsudo nsenter -t 12345 -n ss -tlnpsudo nsenter -t 12345 -n tcpdump -i eth0 port 8080
# 컨테이너 안에서 tcpdump 바이너리가 없을 때 유용# 호스트의 도구를 컨테이너 네임스페이스 안에서 실행 가능11-2. kubectl exec: Pod 안에서 직접 디버깅
섹션 제목: “11-2. kubectl exec: Pod 안에서 직접 디버깅”# Pod 안에서 셸 실행kubectl exec -it my-pod -- /bin/sh
# 특정 컨테이너 지정 (멀티 컨테이너 Pod)kubectl exec -it my-pod -c sidecar -- /bin/sh
# 원라이너로 DNS 확인kubectl exec -it my-pod -- nslookup my-service.default.svc.cluster.local
# curl 실행 (이미지에 있다면)kubectl exec -it my-pod -- curl -v http://my-service:8080/health
# 네트워크 정책 확인용 nckubectl exec -it my-pod -- nc -zv my-service 808011-3. Ephemeral Container: 최소 이미지 Pod 디버깅
섹션 제목: “11-3. Ephemeral Container: 최소 이미지 Pod 디버깅”프로덕션 이미지에는 curl, tcpdump, bash가 없는 경우가 많다. Kubernetes 1.23+의 Ephemeral Container를 사용하면 실행 중인 Pod에 임시 디버그 컨테이너를 주입할 수 있다.
# nicolaka/netshoot: 네트워크 디버깅 도구 올인원 이미지kubectl debug -it my-pod --image=nicolaka/netshoot --target=my-container
# 진입 후 사용 가능한 도구들tcpdump -i eth0curl, wget, dig, nslookup, ss, netstat, mtr, traceroute, iperf3, ...
# 종료하면 ephemeral container도 사라짐컨테이너 간 트래픽 tcpdump
# veth 인터페이스 찾기 (호스트에서)# 컨테이너 PID의 네트워크 네임스페이스와 연결된 veth 확인ip link | grep veth
# 또는 컨테이너 IP로 필터sudo tcpdump -i any host <pod-ip>11-4. 컨테이너 DNS 디버깅
섹션 제목: “11-4. 컨테이너 DNS 디버깅”# Pod의 DNS 설정 확인kubectl exec my-pod -- cat /etc/resolv.conf# nameserver 10.96.0.10 ← CoreDNS# search default.svc.cluster.local svc.cluster.local cluster.local# options ndots:5
# ndots:5 의미: 도트가 5개 미만이면 search 도메인을 붙여서 먼저 시도# "my-service" → my-service.default.svc.cluster.local 시도 → 성공하면 바로 사용
# CoreDNS 로그 확인kubectl logs -n kube-system -l k8s-app=kube-dns -f12. 장애 진단 플레이북
섹션 제목: “12. 장애 진단 플레이북”12-1. “사이트가 느려요” 신고 → 원인 파악
섹션 제목: “12-1. “사이트가 느려요” 신고 → 원인 파악”Step 1: 영향 범위 확인 (1분)
# 모든 사용자? 특정 지역? 특정 기능?# 내부에서도 느린가?curl -w "\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \ -o /dev/null -s https://api.example.com/healthStep 2: 인프라 레이어부터 빠르게 순환 (2분)
# DNS 정상?dig api.example.com +short
# ALB/서버 연결 정상?nc -zv api.example.com 443
# 서버 응답 타이밍curl -w "connect:%{time_connect} TLS:%{time_appconnect} TTFB:%{time_starttransfer}\n" \ -o /dev/null -s https://api.example.com/healthStep 3: TTFB가 느리면 → 서버 처리 시간 문제
# 서버 CPU/메모리kubectl top podskubectl top nodes
# 애플리케이션 로그에서 슬로우 쿼리kubectl logs my-api-pod --since=5m | grep -E "slow|timeout|ERROR"
# DB 연결 확인kubectl exec my-api-pod -- ss -tn | grep :5432Step 4: TCP Connect가 느리면 → 네트워크/LB 문제
# ALB 타겟 헬스 확인aws elbv2 describe-target-health \ --target-group-arn arn:aws:elasticloadbalancing:...
# Security Group 변경 이력aws cloudtrail lookup-events \ --lookup-attributes AttributeKey=EventName,AttributeValue=AuthorizeSecurityGroupIngress \ --start-time 2024-01-01T00:00:00ZStep 5: DNS가 느리거나 틀리면 → DNS 문제
# 여러 DNS 서버에서 결과 비교for dns in 8.8.8.8 1.1.1.1 169.254.169.253; do echo -n "$dns: " dig @$dns api.example.com +short +time=2done12-2. “특정 API만 502 에러가 난다”
섹션 제목: “12-2. “특정 API만 502 에러가 난다””# 1. ALB가 502를 주는 건지, 앱이 502를 주는 건지 구분# ALB Access Log에서 elb_status_code vs target_status_code 비교# elb=502, target=- → ALB가 타겟과 연결 실패# elb=502, target=502 → 앱이 502를 응답
# 2. 타겟 헬스 확인aws elbv2 describe-target-health --target-group-arn ...
# 3. 해당 Pod의 응답 직접 확인 (ALB 우회)kubectl exec debug-pod -- curl http://<pod-ip>:8080/api/problematic
# 4. Pod 로그 확인kubectl logs <pod-name> --previous # 이전 크래시 로그kubectl logs <pod-name> --since=5m12-3. “간헐적으로 Connection Reset 된다”
섹션 제목: “12-3. “간헐적으로 Connection Reset 된다””# tcpdump로 RST 패킷 잡기sudo tcpdump -i eth0 'tcp[13] & 4 != 0' -n -w /tmp/rst.pcap
# Wireshark에서 열어 Follow TCP Stream으로 컨텍스트 확인
# 원인 후보:# - keepalive 타임아웃 (ALB 기본 60초, nginx 75초 등 불일치)# - 서버 측 Connection Pool 고갈# - OOM Killer가 프로세스 종료# - iptables 규칙 (conntrack 테이블 가득 참)
# conntrack 테이블 상태 확인sudo conntrack -C # 현재 엔트리 수sudo sysctl net.netfilter.nf_conntrack_max # 최대값12.5 트러블슈팅 (실무 에러 패턴)
섹션 제목: “12.5 트러블슈팅 (실무 에러 패턴)”🔧 VPC Flow Logs에서 REJECT인데 Security Group은 열려 있음 — NACL 차단
섹션 제목: “🔧 VPC Flow Logs에서 REJECT인데 Security Group은 열려 있음 — NACL 차단”증상:
# Flow Logs 분석 (CloudWatch Insights)fields @timestamp, srcAddr, dstAddr, srcPort, dstPort, action| filter action = "REJECT" and dstPort = 8080| sort @timestamp desc# 출력: 10.0.1.5 → 10.0.2.10 포트 8080 REJECT가 계속 기록됨
# Security Group 확인하면 8080 허용되어 있음aws ec2 describe-security-groups --group-ids sg-xxxxx \ --query 'SecurityGroups[0].IpPermissions[?ToPort==`8080`]'# 결과: 허용 규칙 있음 → 그런데 왜 REJECT?원인: Security Group은 Stateful(응답 자동 허용)이지만 NACL(Network ACL)은 Stateless다. NACL 아웃바운드 규칙에서 임시 포트(ephemeral port: 1024-65535)가 차단되어 있으면 응답 패킷이 반환되지 못한다. 또는 소스 서브넷의 NACL 아웃바운드 규칙이 제한되어 있을 수 있다.
해결:
# 1. 해당 서브넷의 NACL 확인aws ec2 describe-network-acls \ --filters "Name=association.subnet-id,Values=subnet-xxxxxx" \ --query 'NetworkAcls[*].Entries' \ --output table
# 2. NACL 임시 포트 아웃바운드 허용 추가 (응답 트래픽)aws ec2 create-network-acl-entry \ --network-acl-id acl-xxxxxx \ --rule-number 100 \ --protocol tcp \ --rule-action allow \ --egress \ --cidr-block 10.0.0.0/16 \ --port-range From=1024,To=65535
# 3. Security Group vs NACL 우선순위 기억:# 패킷 → ENI → Security Group → NACL → 서브넷# 둘 다 통과해야 함🔧 kubectl exec 내부에서는 되는데 클러스터 외부에서 접속 실패
섹션 제목: “🔧 kubectl exec 내부에서는 되는데 클러스터 외부에서 접속 실패”증상:
# 클러스터 내부 (정상):kubectl exec my-pod -- curl -s http://my-service:8080/health# {"status": "ok"}
# 클러스터 외부 (실패):curl https://api.example.com/health# curl: (7) Failed to connect to api.example.com port 443: Connection refused
# nc로 포트 확인:nc -zv api.example.com 443# nc: connect to api.example.com port 443 (tcp) failed: Connection refused원인: 여러 가능성:
- Ingress가 설정되지 않았거나 Ingress Controller가 실행되지 않음
- ALB/NLB Security Group에서 인바운드 443 미허용
- Route 53 레코드가 없거나 잘못된 IP 가리킴
- TLS 인증서 오류 (443은 열려 있지만 핸드셰이크 실패)
해결:
# 1. 레이어별 순차 점검dig api.example.com +short # DNS → IP 반환되는지nc -zv <ip-address> 443 # TCP 연결 가능한지openssl s_client -connect api.example.com:443 # TLS 핸드셰이크
# 2. Ingress 상태 확인kubectl get ingress -n production# NAME CLASS HOSTS ADDRESS PORTS AGE# api-ingress alb api.example.com 54.1.2.3 80,443 2d
# ADDRESS가 없으면 ALB 생성 실패 → Ingress Controller 로그 확인:kubectl logs -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller
# 3. ALB Security Group 인바운드 확인aws elbv2 describe-load-balancers \ --query 'LoadBalancers[?DNSName==`my-alb.elb.amazonaws.com`].SecurityGroups'aws ec2 describe-security-groups --group-ids sg-xxxxxx \ --query 'SecurityGroups[0].IpPermissions[?ToPort==`443`]'🔧 tcpdump로 패킷이 보이는데 앱이 요청을 못 받음 — iptables DROP
섹션 제목: “🔧 tcpdump로 패킷이 보이는데 앱이 요청을 못 받음 — iptables DROP”증상:
# tcpdump에서는 패킷이 서버에 도달:sudo tcpdump -i eth0 port 8080 -n# 10.0.1.5.54321 > 10.0.2.10.8080: Flags [S] seq 0, ...# (그런데 앱 로그에는 아무것도 없음, 응답도 없음)
# ss -tlnp로 포트는 정상적으로 LISTEN 중:ss -tlnp | grep 8080# LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=1234))원인: 패킷이 NIC 레벨(tcpdump가 보는 레벨)에서는 수신되지만, iptables가 커널 레벨에서 DROP하는 경우. Docker, Kubernetes, 또는 직접 iptables 규칙이 트래픽을 차단할 수 있다. conntrack 테이블이 가득 찬 경우에도 동일 증상이 발생한다.
해결:
# 1. iptables 규칙 확인sudo iptables -L -n -v | grep -E "DROP|REJECT" | head -20sudo iptables -t nat -L -n -v | head -30 # NAT 규칙 (Docker/K8s)
# 2. conntrack 테이블 상태 확인sudo conntrack -C # 현재 엔트리 수sudo sysctl net.netfilter.nf_conntrack_max # 최대값# 현재값 ≈ 최대값이면 conntrack 고갈 → 새 연결 DROP
# 3. conntrack 최대값 증가sudo sysctl -w net.netfilter.nf_conntrack_max=1048576# 또는 /etc/sysctl.conf에 영구 적용
# 4. 특정 연결이 conntrack에 있는지 확인sudo conntrack -L | grep "10.0.1.5"
# 5. 패킷 추적 (iptables TRACE target)sudo iptables -t raw -A PREROUTING -p tcp --dport 8080 -j TRACEsudo dmesg | grep TRACE | tail -2013. 프론트엔드 → 플랫폼 브릿지
섹션 제목: “13. 프론트엔드 → 플랫폼 브릿지”13-1. 브라우저 DevTools Network 탭에서 보이는 것
섹션 제목: “13-1. 브라우저 DevTools Network 탭에서 보이는 것”4년차 프론트엔드 개발자라면 이미 이 화면이 익숙하다:
Name Status Type Initiator Size Time─────────────────────────────────────────────────────api/users 200 fetch app.js:42 2.3 KB 243 ms ↓ Request Headers :method: GET :authority: api.example.com Authorization: Bearer eyJ... Content-Type: application/json ↓ Response Headers content-type: application/json x-request-id: abc-123-def cache-control: no-cache ↓ Timing Stalled: 12 ms ← 브라우저 큐잉 (6개 동시 제한) DNS Lookup: 0 ms ← 캐시 히트 Initial connection: 45 ms ← TCP + TLS Request sent: 0 ms Waiting (TTFB): 180 ms ← 서버 처리 시간! Content Download: 6 ms13-2. 서버 사이드에서 같은 요청을 보면
섹션 제목: “13-2. 서버 사이드에서 같은 요청을 보면”# curl로 동일 재현 (브라우저 DevTools의 "Copy as cURL" 사용)curl -v -H "Authorization: Bearer eyJ..." \ https://api.example.com/api/users
# tcpdump로 패킷 레벨 확인sudo tcpdump -i eth0 host api.example.com and port 443 -w /tmp/api.pcap
# ALB Access Log에서# target_processing_time: 서버가 처리한 시간 (DevTools TTFB와 비교)# request_processing_time: ALB → 타겟 연결 시간13-3. 브라우저에서만 보이는 것 vs 서버에서만 보이는 것
섹션 제목: “13-3. 브라우저에서만 보이는 것 vs 서버에서만 보이는 것”| 항목 | 브라우저 DevTools | 서버 사이드 도구 |
|---|---|---|
| CORS 에러 | O (preflight 요청 보임) | X (서버는 모든 요청 수신) |
| 브라우저 캐시 | O (from cache, 0ms) | X |
| 서비스 워커 | O (SW intercept) | X |
| TLS 세부 정보 | 간략히 (Security 탭) | O (openssl, tcpdump) |
| 서버 내부 처리 | X (TTFB로 추정만) | O (앱 로그, 프로파일러) |
| 네트워크 드랍 | X | O (tcpdump, Flow Logs) |
| 방화벽 차단 | Connection 에러로만 | O (Flow Logs REJECT) |
| Pod간 통신 | X | O (nsenter, kubectl) |
13-4. CORS 문제 디버깅
섹션 제목: “13-4. CORS 문제 디버깅”브라우저에서 CORS 에러가 나도 서버에서는 멀쩡해 보이는 이유:
# 브라우저는 먼저 OPTIONS 요청 (preflight) 보냄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" \ -v 2>&1 | grep -i "access-control"
# 올바른 응답이면 이 헤더들이 있어야 함:# Access-Control-Allow-Origin: https://app.example.com# Access-Control-Allow-Methods: GET, POST, PUT, DELETE# Access-Control-Allow-Headers: Authorization13-5. HTTP/2 멀티플렉싱 — 브라우저와 서버 사이드의 차이
섹션 제목: “13-5. HTTP/2 멀티플렉싱 — 브라우저와 서버 사이드의 차이”# 브라우저: 같은 도메인 요청을 HTTP/2 스트림으로 다중화 → Stalled 거의 없음# curl로 HTTP/2 확인curl -v --http2 https://api.example.com/health 2>&1 | grep "HTTP/"
# 서버에서 h2 지원 확인openssl s_client -connect api.example.com:443 -alpn h2 2>&1 | grep "ALPN"# ALPN protocol: h2 → HTTP/2 지원 확인됨14. 실무 치트시트
섹션 제목: “14. 실무 치트시트”14-1. 상황별 도구 선택
섹션 제목: “14-1. 상황별 도구 선택”상황 도구──────────────────────────────────────────────────────DNS 안 된다 dig, nslookup포트 열려 있는지 nc -zv, telnetHTTP 응답 상세 보기 curl -vHTTP 타이밍 분석 curl -w패킷 레벨 디버깅 tcpdump, Wireshark소켓 상태 / 포트 확인 ss -tlnp연결 수 / TIME_WAIT ss -tan | awk 집계경로 추적 traceroute, mtr패킷 로스 모니터링 mtr -rTLS 인증서 확인 openssl s_clientAWS Security Group 차단 VPC Flow LogsALB 레벨 에러 ALB Access Logs컨테이너 네트워크 nsenter, kubectl exec도구 없는 컨테이너 디버깅 kubectl debug (netshoot)14-2. openssl로 TLS 인증서 확인
섹션 제목: “14-2. openssl로 TLS 인증서 확인”# 인증서 만료일 확인echo | openssl s_client -connect api.example.com:443 2>/dev/null \ | openssl x509 -noout -dates
# SAN(Subject Alternative Names) 확인echo | openssl s_client -connect api.example.com:443 2>/dev/null \ | openssl x509 -noout -text | grep -A2 "Subject Alternative"
# 인증서 체인 전체 확인openssl s_client -connect api.example.com:443 -showcerts
# 특정 TLS 버전 지원 여부openssl s_client -connect api.example.com:443 -tls1_2openssl s_client -connect api.example.com:443 -tls1_314-3. iperf3: 네트워크 대역폭 측정
섹션 제목: “14-3. iperf3: 네트워크 대역폭 측정”# 서버에서 (대기)iperf3 -s -p 5201
# 클라이언트에서 (10초간 측정)iperf3 -c server-ip -p 5201 -t 10
# UDP 패킷 로스 측정iperf3 -c server-ip -u -b 100M # 100Mbps로 UDP 전송15. 핵심 요약
섹션 제목: “15. 핵심 요약”- 레이어별로 좁혀라: L3(ping) → DNS(dig) → L4(nc) → L7(curl) 순서로 점검하면 원인 레이어가 좁혀진다.
- tcpdump는 패킷을 거짓말하지 않는다: 앱 로그에 안 보여도 tcpdump에서 RST가 보이면 그게 사실이다.
- 브라우저 DevTools와 서버 도구를 연결하라: TTFB =
target_processing_time(ALB 로그) = 서버 처리 시간이다. - TIME_WAIT vs CLOSE_WAIT 구분: TIME_WAIT는 정상 종료 후 대기, CLOSE_WAIT는 서버가 연결을 안 닫는 코드 버그다.
- 컨테이너에 도구가 없으면 nsenter 또는 kubectl debug: 프로덕션 이미지를 바꾸지 않고도 디버깅할 수 있다.
- VPC Flow Logs는 방화벽 디버깅의 결정판: Security Group이 차단하면 Flow Logs에 REJECT로 남는다.
📚 추천 리소스
섹션 제목: “📚 추천 리소스”- 📖 tcpdump Cheat Sheet — Comparitech — BPF 필터 문법, 옵션 정리, 실무 시나리오별 명령어 모음. 이 문서의 4절 tcpdump 레퍼런스로 직접 활용 (입문)
- 📖 BPF: The Forgotten Bytecode — Cloudflare Blog — tcpdump BPF 필터가 커널 레벨에서 어떻게 동작하는지 내부 구조 설명. 고성능 필터 작성 배경 이해용 (중급)
- 📖 Troubleshooting with VPC Flow Logs — myaws.rocks — ACCEPT/REJECT 레코드 해석, Security Group vs NACL 구분, CloudWatch Insights 쿼리 예시 (중급)
- 📖 Debugging Complex Networking Issues with ELB — ITNEXT — ALB 502 에러, 타임아웃, Security Group 디버깅 실전 사례. 이 문서의 10절 AWS 디버깅 심화 (고급)
- 📖 VPC Traffic Mirroring — AWS Blog — Flow Logs 한계를 넘어 실제 패킷 캡처. tcpdump 없이 AWS에서 Wireshark 분석하는 방법 (고급)
직접 확인해보기
섹션 제목: “직접 확인해보기”# 1. OSI 계층별 순차 점검 스크립트TARGET="api.example.com"echo "=== L3: DNS ==="dig $TARGET +short
echo "=== L4: TCP 포트 ==="nc -zv $TARGET 443 2>&1
echo "=== L6: TLS ==="openssl s_client -connect $TARGET:443 </dev/null 2>/dev/null | grep -E "Protocol|Cipher|Verify"
echo "=== L7: HTTP ==="curl -s -o /dev/null -w "HTTP %{http_code} | TTFB: %{time_starttransfer}s\n" https://$TARGET/health예상 출력:
=== L3: DNS ===54.12.34.56
=== L4: TCP 포트 ===Connection to api.example.com port 443 [tcp/https] succeeded!
=== L6: TLS ===Protocol : TLSv1.3Cipher : TLS_AES_256_GCM_SHA384Verify return code: 0 (ok)
=== L7: HTTP ===HTTP 200 | TTFB: 0.187s# 2. RST 패킷 실시간 감지sudo tcpdump -i eth0 'tcp[13] & 4 != 0' -n -c 10예상 출력 (RST 발생 시):
14:23:05.123456 IP 10.0.1.5.45231 > 10.0.2.10.8080: Flags [R.], seq 0, ack 1, win 0, length 014:23:05.234567 IP 10.0.2.10.8080 > 10.0.1.5.45231: Flags [R], seq 2048, win 0, length 0# 서버가 RST 보내는 경우 → 앱 크래시 or 포트 미청취# 클라이언트가 RST 보내는 경우 → 타임아웃 or keepalive 만료# 3. 소켓 상태 집계ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn예상 출력 (정상):
245 ESTAB 42 TIME-WAIT 5 LISTEN 1 State예상 출력 (비정상 — CLOSE_WAIT 폭증):
150 ESTAB 1240 CLOSE-WAIT ← 서버가 연결을 닫지 않는 코드 버그 12 TIME-WAIT 5 LISTEN# 4. curl 타이밍 종합 분석curl -o /dev/null -s -w \ "DNS: %{time_namelookup}s | TCP: %{time_connect}s | TLS: %{time_appconnect}s | TTFB: %{time_starttransfer}s | Total: %{time_total}s\n" \ https://google.com예상 출력:
DNS: 0.002s | TCP: 0.025s | TLS: 0.071s | TTFB: 0.098s | Total: 0.112s# TLS 핸드셰이크: 0.071 - 0.025 = 46ms# 서버 처리 (TTFB): 0.098 - 0.071 = 27ms# 5. VPC Flow Logs에서 REJECT 트래픽 찾기 (CloudWatch Insights)# aws logs start-query --log-group-name VPCFlowLogs \# --start-time $(date -d "1 hour ago" +%s) \# --end-time $(date +%s) \# --query-string 'fields @timestamp, srcAddr, dstAddr, dstPort, action# | filter action = "REJECT"# | stats count(*) by srcAddr, dstAddr, dstPort# | sort count desc | limit 10'echo "CloudWatch Insights 쿼리를 콘솔에서 실행하면:"echo "srcAddr dstAddr dstPort count"echo "10.0.1.5 10.0.2.10 8080 342 ← Security Group 확인 필요"echo "10.0.1.5 10.0.3.20 5432 156 ← DB 접근 차단"