간헐적 503 또는 502
관측 없이 재현과 로그만 보면 4~8시간이 걸릴 수 있다. ALB access log에서 elb_status_code와 target_status_code를 나누고 `ss`로 타겟 소켓을 보면 30분~1시간 안에 LB-타겟 경계로 좁힐 수 있다.
반증: 앱이 502를 냈는가, ALB가 타겟 연결에 실패했는가네트워크 디버깅은 “요청이 실패했다”는 겉 증상을 OSI(Open Systems Interconnection) 계층 모델의 관측 지점으로 나누고, 각 지점에서 반증 가능한 가설을 하나씩 제거하는 과정이다.
OSI 모델은 네트워크를 물리 링크, 데이터 링크, IP 라우팅, TCP/UDP 전송, 세션, 표현, 애플리케이션 계층으로 나눠 보는 학습 모델이다. 실제 시스템은 이 7개 층이 항상 깔끔히 분리되지는 않지만, 장애를 좁힐 때는 “어느 층의 약속이 먼저 깨졌는가”를 묻는 지도 역할을 한다.
MTTR(Mean Time To Recovery 또는 Mean Time To Repair)은 장애가 발생한 뒤 정상 상태로 복구되기까지 걸린 평균 시간이다. 네트워크 디버깅의 목적은 명령어를 많이 외우는 것이 아니라, 잘못된 재시작과 무작정 롤백을 줄여 MTTR을 낮추는 것이다.
이 문서의 중심 질문은 하나다.
어느 관측 지점에서무슨 증거를 얻었고그 증거가 어떤 가설을 반증했는가?“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 버전, ALPN | openssl s_client, curl -v |
| TLS는 되지만 첫 바이트가 늦다 | 앱 처리, upstream 호출, DB, target queue | curl -w, ALB access log, app trace |
| 브라우저에서만 실패한다 | CORS, mixed content, service worker, cache | DevTools, preflight 재현, 응답 헤더 |
TTL(Time To Live)은 DNS 레코드나 IP 패킷에서 “얼마나 오래 유효한가”를 제한하는 값이다. DNS에서 TTL 300은 resolver가 해당 응답을 최대 300초 동안 캐시할 수 있다는 뜻이다. 그래서 dig로 새 IP가 보이더라도, 일부 클라이언트는 TTL이 끝날 때까지 이전 IP를 계속 사용할 수 있다.
DNS propagation은 DNS 변경이 여러 resolver와 캐시를 거치며 관찰자에게 점차 다르게 보이는 기간을 말한다. 실제로는 전 세계에 무언가가 동시에 “전파”되는 사건이라기보다, 기존 캐시가 각자 만료되는 과정에 가깝다.
퀴즈
힌트: 증상은 L7이어도 원인은 캐시된 이름 해석일 수 있다.
먼저 TTL과 resolver별 응답을 확인해야 한다. TTL이 아직 남아 있으면 일부 사용자가 이전 IP를 보는 것은 정상 후보다. 앱 롤백보다 DNS 캐시 만료, 이전 서버 유지, dual serving 같은 완충책을 먼저 검토한다.
애플리케이션 로그만 보면 서버 코드가 실행된 뒤의 세계만 보인다. 그러나 많은 네트워크 장애는 앱 로그에 도달하기 전에 끝난다.
클라이언트 -> DNS resolver -> 인터넷/VPC 라우팅 -> 방화벽 또는 Security Group -> 로드밸런서 -> TLS 종료 지점 -> Pod 또는 VM 커널 -> 애플리케이션 프로세스이 중 어느 지점에서 끊겨도 사용자에게는 “API 실패”로 보인다. 앱 로그가 비어 있을 때 “로그가 없으니 요청이 없었다”고 결론 내리면 위험하다. 요청이 방화벽에서 드롭됐거나, 커널에서 RST(reset)로 끊겼거나, 로드밸런서가 타겟을 unhealthy로 보고 전달하지 않았을 수 있다.
계층형 디버깅은 이 한계를 피하기 위해 등장한다.
예를 들어 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
네트워크 디버깅의 단위는 도구가 아니라 루프다.
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()했다고 말하지 않는다.원문이 보존하던 운영 시나리오의 숫자는 여전히 중요하다. 단, 숫자는 “이 도구를 외우라”가 아니라 “어떤 가설을 빨리 버릴 수 있는가”를 보여줘야 한다.
관측 없이 재현과 로그만 보면 4~8시간이 걸릴 수 있다. ALB access log에서 elb_status_code와 target_status_code를 나누고 `ss`로 타겟 소켓을 보면 30분~1시간 안에 LB-타겟 경계로 좁힐 수 있다.
반증: 앱이 502를 냈는가, ALB가 타겟 연결에 실패했는가사용자 제보만 모으면 1~3일이 걸릴 수 있다. `openssl s_client`와 짧은 packet capture로 SNI, SAN, TLS 버전, alert를 확인하면 2~4시간 안에 재현 가능한 실패로 바꿀 수 있다.
반증: 서버 인증서 문제인가, 클라이언트 정책 문제인가무작정 24~48시간 기다리는 대신 resolver별 `dig`와 TTL을 보면 즉시 '아직 캐시가 남은 정상 혼재'인지 '권한 DNS 설정 오류'인지 나눌 수 있다.
반증: 변경이 안 됐는가, 캐시가 남았는가사후 로그만으로는 방향을 놓치기 쉽다. RST를 누가 보냈는지 잡으면 5분 안에 앱 종료, idle timeout 불일치, 방화벽 정책 후보로 좁힐 수 있다.
반증: 클라이언트가 끊는가, 서버나 중간 장비가 끊는가| 계층 | 디버깅 질문 | 대표 관측 지점 |
|---|---|---|
| L7 Application | HTTP 요청/응답의 의미가 맞는가 | curl -v, DevTools, ALB access log |
| L6 Presentation | TLS 인증서, 암호화, ALPN이 맞는가 | openssl s_client, curl -v |
| L5 Session | 연결 재사용, 쿠키, idle timeout이 맞는가 | ALB logs, app logs, ss, DevTools |
| L4 Transport | TCP/UDP 연결과 포트 상태가 맞는가 | nc, ss, tcpdump |
| L3 Network | IP 라우팅과 방화벽 경계가 맞는가 | 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 -> 앱”처럼 프로토콜 단계로 읽는 편이 더 직접적이다.
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의 누적 타이밍 차이를 계산해야 어느 구간이 느린지 분리된다.
다음은 계층형 디버깅의 반례다. 항상 L3부터 끝까지 순서대로 돌 필요는 없다.
| 상황 | 바로 볼 지점 | 이유 |
|---|---|---|
| HTTP 500이 응답 body와 함께 온다 | 앱 로그, trace, upstream metric | DNS/TCP/TLS는 이미 통과했다 |
| 특정 브라우저에서 mixed content만 실패 | DevTools Security/Console | 서버 packet capture보다 브라우저 정책 문제 |
모든 요청이 Could not resolve host | DNS resolver와 권한 DNS | TCP 관측 전에 이름 해석이 깨졌다 |
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를 별도로 본다.curl: 사용자 공간에서 HTTP 단계 시간을 나눈다curl은 사용자 공간에서 DNS, TCP, TLS, HTTP 응답을 한 번에 관찰한다. 좋은 점은 가볍고 어디서나 실행하기 쉽다는 것이다. 한계는 커널이나 방화벽에서 사라진 패킷의 위치를 직접 보여주지 못한다는 점이다.
핵심은 curl -w 값이 대부분 “시작부터 해당 단계까지의 누적 시간”이라는 점이다. 구간 시간은 서로 빼서 계산한다.
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.002connect=0.020appconnect=0.120starttransfer=1.800total=1.830code=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.10 | DNS가 완전히 실패했다 |
Connected | TCP 443이 전혀 닫혀 있다 |
SSL connection using TLSv1.3 | TLS 핸드셰이크가 실패했다 |
ALPN: server accepted h2 | 서버가 HTTP/2를 전혀 협상하지 않았다 |
< HTTP/2 200 | 이 health 경로가 HTTP 에러를 낸다 |
--resolve는 DNS를 임시로 우회해 같은 호스트명으로 특정 IP에 접속한다. 인증서 검증에는 호스트명이 필요하므로 /etc/hosts를 바꾸지 않고도 새 타겟, 특정 Pod, 특정 ALB를 비교할 수 있다.
curl --resolve api.example.com:443:10.0.1.15 https://api.example.com/health이 명령이 성공하고 일반 요청만 실패하면 DNS, 로드밸런서, 라우팅 경로가 후보로 남는다. 반대로 이것도 실패하면 타겟 자체, TLS 인증서, 앱 경로를 본다.
dig: DNS 응답을 resolver별로 비교한다dig는 이름 해석 결과와 TTL을 관찰한다. DNS 문제의 핵심은 “누가 어떤 답을 캐시하고 있는가”다.
dig api.example.com +shortdig @8.8.8.8 api.example.com +shortdig @1.1.1.1 api.example.com +shortdig 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 이상 유지하는 방식으로 설계한다.
openssl s_client: TLS 협상과 인증서를 본다openssl s_client는 TLS 핸드셰이크를 관찰한다. 여기서 보는 것은 HTTP body가 아니라 서버가 어떤 인증서를 제시했고, 어떤 TLS 버전과 cipher가 선택됐으며, ALPN이 무엇으로 합의됐는가다.
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: h2 | HTTP/2로 넘기기로 협상됨 |
no peer certificate | TLS 종료 지점이 없거나 잘못된 포트일 수 있음 |
handshake failure | 버전, cipher, SNI, 클라이언트 인증 요구 등을 의심 |
TLS 디버깅의 흔한 반례는 “브라우저는 실패하는데 curl -k는 성공한다”다. -k는 인증서 검증을 끄므로, 인증서 만료나 SAN 불일치 문제를 숨긴다. 원인을 찾을 때는 -k 성공을 정상으로 보지 말고 “TCP와 TLS record 교환은 가능하지만 신뢰 검증이 실패한다”로 읽어야 한다.
ss: 로컬 커널의 소켓 상태를 본다ss(Socket Statistics)는 로컬 머신의 소켓 상태를 보여준다. 포트가 LISTEN 중인지, 연결이 ESTAB(established, 수립됨) 상태인지, CLOSE-WAIT가 쌓이는지, TIME-WAIT가 많은지 확인할 수 있다.
ss -tlnpss -tanss -tn state established소켓 상태는 원격 경로 전체를 보여주지 않는다. 대신 “로컬 프로세스와 커널이 어떤 상태로 연결을 보고 있는가”를 보여준다.
| 상태 | 의미 | 디버깅 질문 |
|---|---|---|
| LISTEN | 프로세스가 해당 포트에서 연결을 기다림 | 올바른 IP와 포트에 바인딩했는가 |
| ESTAB | TCP 연결이 수립됨 | 연결 수가 평소보다 많은가, 특정 peer인가 |
| SYN-SENT | 로컬이 SYN을 보냈고 응답을 기다림 | 원격 SYN-ACK가 돌아오는가 |
| SYN-RECV | SYN을 받았고 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 같은 커널 파라미터로 도망가면 원인과 멀어진다.
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를 보내는 쪽은 클라이언트인가 서버인가?짧은 관찰 스니펫:
sudo tcpdump -i eth0 -n 'host 10.0.1.5 and port 8080' -c 20sudo tcpdump -i eth0 -n 'tcp[13] & 4 != 0' -c 10sudo tcpdump -i eth0 -n 'port 8080 and (tcp[13] == 2 or tcp[13] == 18)' -c 20tcp[13]은 TCP 헤더의 flags 바이트를 보는 BPF 표현식이다. 2는 SYN, 18은 SYN+ACK, 4는 RST 비트다. 모든 값을 외우는 것이 목적은 아니다. “연결 시작 신호와 강제 종료 신호를 패킷에서 직접 구분할 수 있다”가 학습 포인트다.
pcap 파일로 저장할 때는 화면에 payload를 길게 출력하지 않는다.
sudo tcpdump -i eth0 -n -w /tmp/api-443.pcap 'host 10.0.1.5 and port 443'pcap은 Wireshark나 tcpdump -r로 나중에 읽을 수 있다. 민감한 쿠키나 토큰이 들어갈 수 있으므로 공유 전에 캡처 범위와 payload 포함 여부를 확인한다.
Wireshark는 packet capture를 GUI로 분석하는 도구다. 프로덕션 서버에서 직접 GUI를 띄우는 것이 아니라, 보통 서버에서 좁게 pcap을 저장한 뒤 로컬에서 연다.
캡처 필터와 디스플레이 필터를 구분해야 한다.
| 구분 | 적용 시점 | 문법 | 예시 |
|---|---|---|---|
| 캡처 필터 | 수집 전 | BPF | host 10.0.1.5 and port 443 |
| 디스플레이 필터 | 수집 후 | Wireshark 문법 | tcp.flags.reset == 1 |
Wireshark의 좋은 쓰임:
tcp.analysis.retransmission으로 재전송을 찾는다.tls.alert로 TLS alert 위치를 찾는다.TLS payload는 보통 암호화되어 HTTP body가 보이지 않는다. 개발/스테이징에서 SSLKEYLOGFILE이라는 세션 키 로그 파일을 남기면 Wireshark가 해당 TLS 세션을 복호화할 수 있지만, 프로덕션에서는 키와 개인정보 취급 위험이 커진다. 첫 회독에서는 “Wireshark는 패킷 순서와 프로토콜 메타데이터를 잘 보여준다” 정도로 잡으면 충분하다.
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해석:
REJECT만으로 Security Group과 NACL 중 어느 쪽인지 바로 확정하지 않는다.ACCEPT는 네트워크 정책 통과이지 앱 처리 성공이 아니다.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를 봐야 한다.
컨테이너는 별도 네트워크 네임스페이스를 갖는다. 네트워크 네임스페이스는 인터페이스, 라우팅 테이블, iptables 관점을 분리하는 커널 기능이다. 호스트에서 보는 ss와 Pod 안에서 보는 ss가 다를 수 있다.
관측 지점은 셋으로 나눈다.
| 관측 지점 | 무엇을 보는가 |
|---|---|
Pod 내부 kubectl exec | 컨테이너 입장에서 DNS, Service, loopback |
| debug/ephemeral container | 도구가 없는 이미지에 임시 관측 도구 추가 |
호스트 nsenter | 컨테이너 네트워크 네임스페이스를 호스트 도구로 관찰 |
Ephemeral Container는 실행 중인 Pod에 임시 디버그 컨테이너를 붙이는 Kubernetes 기능이다. 네트워크 네임스페이스를 공유할 수 있지만, 파일시스템과 프로세스 권한은 기존 컨테이너와 다를 수 있다. 따라서 “debug container에서 된다”를 “앱 컨테이너에서도 된다”로 바로 일반화하지 않는다.
conntrack(connection tracking)은 Linux netfilter가 연결 흐름을 추적하기 위해 유지하는 상태 테이블이다. NAT, 방화벽, Kubernetes Service 구현은 conntrack에 의존하는 경우가 많다.
conntrack이 가득 차면 새 연결이 드롭되거나 NAT 상태가 만들어지지 않는다. 이때 앱 로그에는 요청이 없고, tcpdump에는 SYN이 보일 수 있으며, 사용자는 timeout을 본다.
관찰 포인트:
sudo conntrack -Csysctl 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을 함께 본다.
브라우저 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는 전체 네트워크 진단 루프의 한 관측 지점으로만 사용한다.
| 도구/로그 | 관측 지점 | 반증할 수 있는 가설 | 조심할 오해 |
|---|---|---|---|
| DevTools | 브라우저 | CORS, cache, mixed content, protocol 문제 | 서버 내부 처리 원인을 확정할 수 없음 |
curl -v | 사용자 공간 HTTP/TLS | DNS/TCP/TLS/HTTP 단계 일부 성공 여부 | 브라우저 정책과 다를 수 있음 |
curl -w | 단계별 누적 시간 | 어느 구간이 느린지 | 값을 구간 시간으로 착각하면 안 됨 |
dig | DNS resolver | 이름 해석, TTL, resolver 차이 | TCP 연결 가능성을 말하지 않음 |
openssl s_client | TLS 핸드셰이크 | SNI, 인증서, TLS 버전, ALPN | HTTP 앱 응답 성공을 말하지 않음 |
nc | TCP 포트 연결 | 포트가 완전히 닫혔는지 | TLS/HTTP 정상 여부는 말하지 않음 |
ss | 로컬 커널 소켓 | LISTEN, ESTAB, CLOSE-WAIT, TIME-WAIT 상태 | 원격 방화벽에서 사라진 패킷은 안 보임 |
tcpdump | 커널 패킷 캡처 지점 | SYN/SYN-ACK/RST, 재전송, 캡처 지점 도달 여부 | 앱이 처리했다는 뜻은 아님 |
| Wireshark | pcap 오프라인 분석 | 프로토콜 흐름, 재전송, TLS alert | 암호화된 payload는 기본적으로 보이지 않음 |
traceroute | 1회 경로 추정 | 어느 홉까지 ICMP 응답이 오는지 | 중간 홉 무응답을 장애로 단정하면 안 됨 |
mtr | 반복 경로 통계 | 간헐적 손실과 jitter | ICMP rate limit에 속을 수 있음 |
| VPC Flow Logs | AWS VPC 정책 경계 | SG/NACL/라우팅 정책 거부 후보 | 앱 처리 성공을 말하지 않음 |
| ALB access log | 로드밸런서-타겟 경계 | ALB 5xx와 앱 5xx 분리 | 타겟 내부 원인은 별도 로그가 필요 |
kubectl exec/debug | Pod 네트워크 관점 | 클러스터 내부 DNS/Service/포트 | 외부 Ingress 경로와 다를 수 있음 |
conntrack | 커널 연결 추적 테이블 | NAT/방화벽 상태 고갈 | 설정값 조정 전 연결 생성 원인을 봐야 함 |
사용자 신고:
사이트가 느려요.디버깅 문장:
HTTP 요청의 전체 시간이 증가했다.DNS, TCP, TLS, TTFB, 다운로드 중 어느 구간이 증가했는지 아직 모른다.처음 가설을 하나로 고정하지 않는다. “DB가 느리다”는 아직 이른 결론이다.
curl -w짧은 스니펫:
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반증:
다음 계층:
ALB target_processing_time앱 로그의 request durationupstream/DB traceconnection pool 대기결과 2:
dns=0.001 tcp=3.007 tls=0.000 ttfb=0.000 total=3.010 code=000반증:
다음 계층:
nc -zv api.example.com 443VPC Flow Logstcpdump로 SYN/SYN-ACK 확인ALB target health결과 3:
dns=1.200 tcp=1.220 tls=1.280 ttfb=1.360 total=1.390 code=200반증:
다음 계층:
resolver별 dig 비교OS DNS cacheCoreDNS 또는 VPC resolver 상태DNS TTL과 query volume“정상 TCP 연결은 몇 ms인가”는 리전, 사용자 위치, 네트워크에 따라 다르다. 그러나 같은 환경에서 갑자기 20ms가 3초로 바뀌는 것은 강한 신호다.
좋은 기준은 절대값 하나가 아니라 비교다.
| 비교 | 의미 |
|---|---|
| 같은 리전 내부 health check | 인프라 내부 기준선 |
| 사용자 지역별 RUM/DevTools | 지역 네트워크 또는 CDN 경계 |
| 배포 전후 같은 경로 | 변경 영향 |
| health 경로와 실제 API 경로 | 앱 처리와 네트워크 공통 비용 분리 |
| HTTP/1.1과 HTTP/2 비교 | ALPN/프로토콜 선택 또는 multiplexing |
502 Bad Gateway는 중간 프록시나 게이트웨이가 upstream과 정상 대화하지 못했다는 뜻으로 자주 등장한다. 그러나 “502”라는 숫자만으로 ALB, Nginx, 앱, upstream 중 누가 만들었는지 알 수 없다.
관측 지점:
클라이언트 HTTP statusALB access logtarget app logPod restart/readinesstarget group healthALB 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 확인ALB를 우회해 Pod IP 또는 Service를 직접 호출하는 것은 “로드밸런서와 타겟 사이” 가설을 줄이기 위한 관측이다.
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 |
| 직접 호출은 200 | ALB routing, target group, Ingress, SG/NACL |
| 직접 호출이 timeout | Service/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:
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가 더 자세히 다룬다.
사용자 신고:
배포 후 일부 사용자는 접속되고 일부 사용자는 예전 서버로 갑니다.디버깅 문장:
resolver별 DNS 응답이 일시적으로 다르거나, 일부 클라이언트가 이전 TTL 캐시를 사용 중일 수 있다.먼저 앱 인스턴스 문제로 들어가지 않는다. DNS가 어떤 IP를 주는지부터 비교한다.
dig @8.8.8.8 api.example.com +shortdig @1.1.1.1 api.example.com +shortdig @169.254.169.253 api.example.com +shortdig api.example.com | grep -A2 'ANSWER SECTION'예:
8.8.8.8: 203.0.113.101.1.1.1: 203.0.113.20VPC resolver: 203.0.113.20TTL: 240해석:
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을 네트워크 디버깅의 첫 관측 지점으로 사용한다.
dig +trace는 맞는데 앱은 틀리다dig +trace는 권한 DNS까지의 위임 경로와 최종 응답을 보여준다. 그러나 실제 앱이 사용하는 resolver가 같은 답을 쓰는지는 별도 문제다.
dig +trace: 권한 DNS에는 새 IP가 있음.
앱 컨테이너: /etc/resolv.conf가 CoreDNS를 가리킴. CoreDNS 또는 node-local DNS cache가 이전 응답을 캐시 중.그래서 Kubernetes 안에서는 Pod 내부에서 직접 dig를 실행해야 한다.
kubectl exec my-pod -- dig api.example.com +shortkubectl exec my-pod -- cat /etc/resolv.confPod 내부와 외부 resolver가 다르면 CoreDNS, node-local-dns, search domain, ndots 옵션을 본다. 이때도 긴 CoreDNS 로그 런북보다 “앱이 실제로 묻는 resolver는 누구인가”가 먼저다.
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 pathNACL을 수정하기 전에는 “어느 subnet의 어느 방향에서 어떤 port가 막혔는가”를 먼저 적는다. 그렇지 않으면 양방향으로 과하게 열어 문제를 숨길 수 있다.
tcpdump에서 SYN이 보이는데 앱 로그가 비어 있다면, 요청은 적어도 캡처 지점까지 왔다. 하지만 앱이 받았다는 뜻은 아니다.
가능한 지점: NIC에서 수신됨 커널 네트워크 스택 진입 iptables 또는 nftables에서 DROP conntrack 상태 생성 실패 소켓 backlog 초과 프로세스가 0.0.0.0이 아니라 127.0.0.1에만 LISTEN Service/Pod 네임스페이스가 다름먼저 LISTEN을 확인한다.
ss -tlnp해석:
0.0.0.0:8080 LISTEN: 모든 인터페이스에서 받을 수 있다.
127.0.0.1:8080 LISTEN: 같은 네임스페이스의 loopback에서만 받을 수 있다. 외부 Pod나 ALB에서는 접근할 수 없다.
LISTEN 없음: 앱이 포트를 열지 않았거나 다른 포트에 떠 있다.그다음 conntrack과 firewall을 본다.
sudo conntrack -Csysctl net.netfilter.nf_conntrack_max현재 conntrack 수가 max에 근접하면 새 연결이 상태를 만들지 못할 수 있다. 이때 증상은 “간헐적 timeout”, “Pod 재시작 때만 실패”, “대량 배포 직후 연결 실패”로 나타날 수 있다.
호스트의 -i any에서 보인 패킷이 컨테이너 내부 eth0까지 들어갔다는 뜻은 아니다. bridge, veth, DNAT, network policy 경계를 지날 수 있다.
관측 지점을 명시한다.
host eth0: 노드의 외부 인터페이스까지 도달했는가
cni0 또는 bridge: Pod 네트워크로 라우팅됐는가
Pod namespace eth0: Pod 입장에서 수신됐는가
localhost: 앱이 loopback에만 묶여 있는가이 구분이 없으면 “패킷이 보이는데 앱이 왜 못 받지”에서 멈춘다.
CORS(Cross-Origin Resource Sharing)는 브라우저가 다른 origin으로 요청할 때 서버가 허용한 origin인지 확인하는 정책이다. CORS 에러가 났다고 해서 서버가 요청을 못 받았다는 뜻은 아니다.
브라우저는 실제 요청 전에 preflight OPTIONS 요청을 보낼 수 있다.
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-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Allow-Credentials반증:
| 관찰 | 의미 |
|---|---|
| OPTIONS가 404/405 | preflight 경로가 서버에서 처리되지 않음 |
| 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은 여전히 계단식으로 보일 수 있다.
| 증상 | 먼저 의심할 계층/개념 | 관측 지점 |
|---|---|---|
Could not resolve host | DNS resolver, 레코드, search domain | dig, /etc/resolv.conf |
| 일부 사용자만 이전 IP로 접속 | DNS TTL, resolver cache | resolver별 dig, TTL |
Connection refused | 포트 미청취, 즉시 RST | nc, ss -tlnp, tcpdump |
Connection timeout | DROP, 라우팅, SG/NACL, SYN 재전송 | Flow Logs, packet capture, route |
| TLS certificate verify failed | 인증서 체인, SAN, SNI | openssl s_client -servername |
ALPN이 기대와 다름 | HTTP/2 설정, TLS 종료 지점 | curl -v, DevTools Protocol |
| TTFB만 느림 | 앱 처리, upstream, DB, target queue | curl -w, ALB log, trace |
| 다운로드만 느림 | payload 크기, bandwidth, client path | curl -w, CDN log, mtr |
| CLOSE-WAIT 증가 | 앱 close 누락 | ss, app connection handling |
| TIME-WAIT 증가와 포트 고갈 | 단기 연결 폭증, keep-alive 미사용 | ss, ephemeral port range |
| Flow Logs REJECT | SG/NACL/network policy | Flow Logs, SG/NACL |
| tcpdump SYN 보임, 앱 로그 없음 | local firewall, conntrack, bind address | ss, conntrack, iptables/nftables |
| Pod 내부는 성공, 외부는 실패 | Ingress, ALB, external SG, DNS | kubectl, ALB log, external curl |
| 브라우저만 실패 | CORS, mixed content, cache, service worker | DevTools, 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 하나”가 아니라 경계별 관측을 맞추는 것이다.
도구 옵션을 모두 외우는 것은 목표가 아니다. 처음에는 아래 질문만 잡으면 된다.
curl: 사용자 공간에서 HTTP 요청 단계가 어디까지 갔는가?
dig: 어떤 resolver가 어떤 IP와 TTL을 주는가?
openssl: TLS 신뢰와 ALPN이 어떻게 협상됐는가?
ss: 로컬 커널과 프로세스가 연결을 어떤 상태로 보는가?
tcpdump: 캡처 지점까지 실제 패킷이 왔는가?
Flow Logs: VPC 정책 경계에서 ACCEPT인가 REJECT인가?
ALB logs: 로드밸런서가 만든 에러인가, 타겟이 만든 에러인가?L7에서 L3으로 내려갔다가 다시 L7로 올라오는 일이 정상이다.
브라우저 502 -> ALB log 확인 -> target_status_code가 비어 있음 -> L4 연결 실패 의심 -> Flow Logs ACCEPT -> tcpdump에서 SYN/SYN-ACK/RST 확인 -> 서버가 RST -> 다시 앱 프로세스와 keep-alive timeout 확인계층형이라는 말은 항상 아래로만 간다는 뜻이 아니다. 관측 지점을 바꿔 가며 가설의 공간을 줄인다는 뜻이다.
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추상화 이름을 먼저 외우지 말고, 그 추상화가 어느 관측 지점을 추가하는지 본다.
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프로덕션 이미지는 curl, dig, tcpdump를 넣지 않는 경우가 많다. 이때 이미지를 바꾸는 것은 마지막 선택이다.
우선순위:
kubectl exec로 이미 있는 최소 도구를 쓴다.nsenter로 해당 network namespace에 들어간다.각 방법의 한계:
| 방법 | 장점 | 한계 |
|---|---|---|
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를 가리킴”일 수도 있다.
이 부록은 본문 이해에 필수는 아니다. 실제 장애에서 관측 지점을 빠르게 잡기 위한 최소 스니펫만 남긴다. 긴 복구 절차나 서비스별 설정 변경은 각 운영 문서로 분리하는 편이 낫다.
dig api.example.com +shortdig @8.8.8.8 api.example.com +shortdig @1.1.1.1 api.example.com +shortdig api.example.com | grep -A2 'ANSWER SECTION'dig api.example.com +trace관찰:
resolver별 응답이 같은가?TTL이 얼마나 남았는가?NXDOMAIN, SERVFAIL, NOERROR/NODATA를 구분했는가?Pod 내부 resolver와 외부 resolver가 같은 답을 주는가?nc -zv api.example.com 443ss -tlnpss -tanss -tn state establishedss -tn state time-wait관찰:
Connection refused: 원격이 즉시 거부하거나 포트가 닫힘.
Connection timeout: DROP, 라우팅, SG/NACL, SYN 응답 없음 후보.
LISTEN 127.0.0.1:8080: 외부 네임스페이스에서 접근 불가.
CLOSE-WAIT 증가: 애플리케이션 close 누락 후보.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_namelookupTCP = time_connect - time_namelookupTLS = time_appconnect - time_connectTTFB wait = time_starttransfer - time_appconnectDownload = time_total - time_starttransfer관찰:
connect만 느림 -> L4/L3appconnect만 느림 -> TLSstarttransfer만 느림 -> 앱/업스트림/LB targettotal만 느림 -> payload 또는 다운로드 경로openssl s_client -connect api.example.com:443 -servername api.example.com -alpn h2 </dev/nullcurl -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 요구 확인.sudo tcpdump -i eth0 -n 'host 10.0.1.5 and port 8080' -c 20sudo tcpdump -i eth0 -n 'tcp[13] & 4 != 0' -c 10sudo 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 api.example.comtraceroute -T -p 443 api.example.commtr -r -c 100 api.example.commtr -T -P 443 -r -c 100 api.example.com관찰:
중간 홉 * * *: ICMP 차단일 수 있음. 이후 홉이 정상인지 본다.
목적지 Loss%: 사용자 트래픽 손실과 더 가까운 신호.
StDev 증가: jitter 후보. 실시간 서비스 영향 확인.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로 바로 수정하지 않는다.먼저 어느 경계가 어떤 방향으로 막혔는지 기록한다.kubectl exec my-pod -- cat /etc/resolv.confkubectl exec my-pod -- nslookup my-service.default.svc.cluster.localkubectl exec my-pod -- curl -s -o /dev/null -w "code=%{http_code}\n" http://my-service:8080/healthkubectl 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 권한 차이 확인.sudo conntrack -Csysctl net.netfilter.nf_conntrack_maxsudo conntrack -L | grep '10.0.1.5'관찰:
current ~= max: 새 연결 상태 생성 실패 후보.
특정 목적지 흐름 폭증: connection pool, retry storm, NAT, idle timeout 확인.주의:
max만 키우면 증상이 늦게 다시 온다.연결 생성률과 retry 원인을 함께 본다.curl -w는 DNS, TCP, TLS, TTFB를 누적 시간으로 보여준다. 구간 시간은 빼서 계산한다.dig는 resolver별 답과 TTL을 비교한다. DNS propagation은 대개 캐시 만료의 문제다.openssl s_client는 SNI, 인증서, TLS 버전, ALPN을 확인한다. curl -k 성공은 TLS 정상의 증거가 아니다.ss는 로컬 소켓 상태를 보여준다. TIME-WAIT와 CLOSE-WAIT는 의미가 다르다.tcpdump는 캡처 지점까지 온 실제 패킷을 보여준다. BPF로 좁게 필터링해야 안전하다.time_namelookup, time_connect, time_appconnect, time_starttransfer의 정확한 의미.목표는 성공/실패를 한 번에 맞히는 것이 아니라, 각 출력이 어떤 가설을 줄이는지 말해보는 것이다.
TARGET="api.example.com"
dig "$TARGET" +shortnc -zv "$TARGET" 443openssl s_client -connect "$TARGET:443" -servername "$TARGET" -alpn h2 </dev/nullcurl -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는 무엇을 말하는가?