sy/dev
Concept
12 min read

네트워크 장애를 읽는 법 — DNS, TCP, TLS, HTTP를 나눠서 보기

백엔드 지식 지도 7편. 연결 실패를 DNS, TCP, TLS, HTTP, 라우팅, 컨테이너 네트워크 문제로 나누어 진단하는 관점을 정리한다.

네트워크 장애는 처음에는 전부 같은 말로 들어온다.

“연결이 안 됩니다.”

하지만 이 말 안에는 여러 실패가 섞여 있다. DNS가 안 풀리는 것과 TCP 포트가 닫힌 것은 다르다. TCP는 붙었지만 TLS 인증서 검증이 실패한 것도 다르다. HTTP 502는 앱이 아니라 프록시가 말하는 실패일 수 있다.

네트워크를 공부하는 목표는 모든 패킷을 외우는 것이 아니다.

“연결이 안 된다”를 서로 다른 실패로 나눌 수 있게 되는 것.

이게 핵심이다.

연결 한 번은 네 단계로 나눠진다

일반적인 HTTPS 요청은 대략 이렇게 진행된다.

DNS → TCP → TLS → HTTP

DNS는 이름을 IP 주소로 바꾼다. TCP는 목적지 IP와 포트까지 연결을 만든다. TLS는 인증서 검증, 키 교환, SNI 같은 과정을 거쳐 안전한 통신 채널을 만든다. 그 위에서 HTTP 요청과 응답이 오간다.

진단도 이 순서로 나누면 된다.

  • 이름이 풀리는가?
  • 목적지 IP와 포트에 TCP 연결이 되는가?
  • TLS 핸드셰이크가 성공하는가?
  • HTTP 응답이 의미 있는가?

TCP만 열렸다고 전체 성공이 아니다. TLS에서 SNI가 안 맞거나 인증서 체인이 깨지면 HTTP 요청은 시작도 못 한다. 반대로 HTTP 502는 TCP/TLS는 됐지만 프록시가 upstream과 통신하지 못했다는 뜻일 수 있다.

실패 신호는 원인을 가른다

클라이언트 입장에서 보이는 실패 신호는 꽤 많은 정보를 준다.

ECONNREFUSED는 보통 목적지까지 갔지만 해당 포트에서 듣는 프로세스가 없다는 뜻이다. TCP RST가 돌아온다.

EHOSTUNREACH나 network unreachable 계열은 길이 없다는 뜻이다. 라우팅, on-link 판정, 게이트웨이 문제를 봐야 한다.

timeout은 응답이 없다는 뜻이다. 방화벽이 DROP하고 있을 수도 있고, 중간 라우팅이 막혔을 수도 있다.

RST → 즉시 거절 → 포트에 아무도 없음
ICMP unreachable → 길 없음
무응답 → timeout → DROP 또는 블랙홀

이 셋을 구분하는 것만으로도 티켓의 방향이 갈린다.

방화벽도 REJECT와 DROP이 다르다. REJECT는 RST나 ICMP unreachable을 돌려주므로 즉시 실패한다. DROP은 아무 응답이 없어 클라이언트가 SYN을 재전송하다가 timeout 된다.

“붙는 데 한참 걸리다 죽는다”는 DROP의 전형적 신호다.

이름과 IP는 어긋날 수 있다

운영에서 자주 생기는 문제가 IP allowlist와 DNS의 어긋남이다.

방화벽은 IP로 열린다. 하지만 애플리케이션은 이름으로 접속한다. DNS가 여러 IP를 돌려주거나, 운영자가 문서에 적힌 IP만 allowlist에 넣었거나, 프록시가 오래된 DNS 결과를 들고 있으면 실제 연결은 열리지 않은 주소로 나갈 수 있다.

증상은 timeout 하나로 보인다.

그래서 중요한 것은 실측이다.

문서에 적힌 IP
현재 DNS가 돌려주는 IP
실제 프로세스가 접속하는 IP
상대가 보는 source IP

이 넷이 같은 이야기를 하고 있는지 봐야 한다.

특히 source IP allowlist는 내 노트북에서 된다고 배포 서버에서도 된다는 뜻이 아니다. 상대는 “어느 IP에서 왔는가”를 본다. 점검은 실제 배포 호스트에서 해야 한다.

라우팅은 longest prefix match다

IP 패킷을 어디로 보낼지는 라우팅 테이블이 정한다. 목적지를 포함하는 경로가 여러 개면 더 구체적인 경로가 이긴다. 이것을 longest prefix match라고 한다.

/32는 특정 IP 하나를 뜻하고, /16보다 언제나 구체적이다. 그래서 응급으로 특정 목적지에 대한 경로를 추가하면 기존 넓은 경로보다 우선할 수 있다.

또 중요한 것이 on-link 판정이다.

목적지가 내 인터페이스 프리픽스 안에 있으면 커널은 게이트웨이를 쓰지 않고 같은 링크에 있다고 판단한다. 그리고 ARP를 뿌린다. 응답이 없어도 “아, 게이트웨이로 보내야겠다”로 자동 전환하지 않는다.

잘못된 netmask 하나가 EHOSTUNREACH를 만들 수 있다.

컨테이너의 네트워크는 호스트와 다르다

호스트에서 curl이 된다고 컨테이너에서도 된다는 보장은 없다.

컨테이너는 자기 네트워크 네임스페이스를 가진다. 자기 인터페이스, 자기 라우팅 테이블, 자기 ARP 테이블, 자기 DNS 설정이 있다.

Docker bridge 네트워크에서는 veth 쌍으로 컨테이너와 브리지가 연결되고, 포트 매핑은 iptables DNAT로 처리된다. 컨테이너 안의 localhost는 호스트가 아니라 컨테이너 자기 자신이다.

DNS도 다르다. Docker 안에서는 127.0.0.11 내장 DNS가 컨테이너 이름을 풀어 준다.

그래서 네트워크 문제를 볼 때는 대조군을 호스트에서만 잡으면 안 된다. 실제 프로세스가 도는 네임스페이스 안에서 확인해야 한다.

Docker 대역은 사내망과 겹칠 수 있다

사설 IP 대역은 익숙하다.

10.0.0.0/8
172.16.0.0/12
192.168.0.0/16

여기에 CGNAT 대역 100.64.0.0/10, 루프백 127.0.0.0/8, link-local 169.254.0.0/16도 자주 본다.

실무에서 은근히 자주 터지는 것은 Docker 기본 주소 풀이다. Docker는 기본적으로 172.17부터 172.31 근처 대역을 쓰는 일이 많다. 그런데 사내망이나 VPN도 172.16/12 안의 주소를 쓸 수 있다.

겹치면 이상한 일이 생긴다. 컨테이너는 목적지가 자기 Docker 네트워크 안에 있다고 믿고 엉뚱한 곳으로 보내거나, VPN으로 가야 할 트래픽이 로컬 브리지로 빠진다.

이 문제는 앱 로그에 안 나온다. 라우팅 테이블을 봐야 한다.

idle 연결은 조용히 죽는다

TCP keepalive의 커널 기본값은 보통 매우 길다. Linux 기본은 2시간 수준이다. 실무 서비스의 idle timeout 방어로는 거의 의미가 없다.

그 사이 NAT, 방화벽, 로드밸런서가 유휴 연결을 조용히 버릴 수 있다. 애플리케이션은 연결이 살아 있다고 생각하다가 다음 전송에서야 죽었다는 것을 알게 된다.

그래서 애플리케이션 레벨 heartbeat나 connection pool의 max idle time 설정이 필요하다.

DB 커넥션 풀, HTTP keep-alive pool, gRPC channel 모두 이 문제를 가진다. “오래 놀던 연결을 재사용했더니 첫 요청만 실패한다”면 idle timeout을 의심해 볼 만하다.

작은 요청은 되고 큰 요청만 죽으면 MTU를 보자

MTU 문제는 흔하진 않지만 한 번 걸리면 고통스럽다.

핸드셰이크는 되고 작은 요청도 된다. 그런데 큰 응답이나 업로드만 멈춘다. 이때 경로 MTU 발견, PMTUD 블랙홀을 의심할 수 있다.

큰 패킷이 중간 링크에서 분할이 필요한데, “fragmentation needed” ICMP가 차단되면 송신 측은 적절한 크기를 알 수 없다. 계속 재전송하다 멈춘다.

VPN, 터널, 오버레이 네트워크가 끼면 더 자주 나온다.

핵심 증상은 이렇다.

작은 요청은 성공
큰 요청은 멈춤
TCP 연결 자체는 살아 있는 것처럼 보임

프록시는 DNS를 언제 푸는가

프록시와 로드밸런서는 L4와 L7로 나눠 볼 수 있다.

L4는 TCP 수준에서 넘긴다. L7은 HTTP를 읽고 라우팅, 재시도, 헤더 조작을 한다.

운영에서 자주 문제 되는 것은 upstream 이름을 언제 푸느냐다.

기동 시점에 한 번만 DNS를 풀고 IP를 오래 들고 있으면, 대상 컨테이너나 서비스가 재생성된 뒤에도 옛 IP로 붙을 수 있다. 반대로 요청마다 동적으로 풀면 DNS TTL과 캐시 정책이 중요해진다.

무중단 배포에서 “새 컨테이너는 떴는데 프록시는 옛 주소를 본다”는 사고가 여기서 나온다.

마무리

네트워크 지식은 패킷 덕후가 되기 위한 것이 아니다.

앱 로그에 남지 않는 실패를 계층별로 분리하기 위한 것이다.

HTTPS 요청 하나는 DNS, TCP, TLS, HTTP로 나눠서 봐야 한다. RST, unreachable, timeout은 서로 다른 문제다. 이름과 IP, source IP allowlist는 어긋날 수 있다. 컨테이너는 호스트와 다른 네트워크 네임스페이스에서 돌고, Docker 대역은 사내망과 겹칠 수 있다. idle 연결과 MTU, 프록시의 DNS 해석 시점도 운영 장애의 단골 원인이다.

다음 편에서는 시리즈를 묶어 장애를 읽는 방법을 정리한다.

핵심 질문은 이것이다.

로그, 메트릭, 트레이스, 상태코드, errno를 어떻게 연결해서 볼 것인가.

Comments