DNS 쿼리의 처음 50밀리초에 무슨 일이 일어나는가

URL을 입력하면 페이지가 뜬다. 그 사이에 컴퓨터는 서버 4개 이상과 대화하고, 1983년산 계층 구조를 탐색하고, 대부분의 쿼리가 목적지에 도달하지도 않는 캐싱에 의존한다.

브라우저에 example.com을 치고 엔터를 누른다. 페이지가 뜬다. 200밀리초 정도 걸렸다. 생각조차 안 했다.

그 200밀리초 동안 컴퓨터는 최소 4개의 서버와 대화하고, 1983년에 설계된 계층 구조를 횡단하고, 대부분의 쿼리가 목적지에 도달하지도 않을 만큼 공격적인 캐싱 시스템에 의존했다. 전부 안 보인다. 키를 누르는 순간과 화면의 첫 픽셀 사이에 일어난다.

쿼리는 생각보다 가까이서 시작된다

네트워크에 뭔가 닿기 전에 운영체제가 싼 답을 먼저 확인한다.

먼저 /etc/hosts — 호스트 이름을 IP 주소에 직접 매핑하는 플랫 파일. 인터넷 전체가 텍스트 파일 하나에 들어가던 시절의 유물이다. 아직도 모든 DNS 쿼리 전에 실행된다. 로컬 개발에 유용하고, 멀웨어가 가끔 수정하는 이유이기도 하다.

그다음 OS 캐시. 이 도메인을 최근에 방문했나? 만료 안 된 캐시 답이 있나? 있으면 끝. 네트워크 요청 없음. 1밀리초 미만.

둘 다 답이 없으면 애플리케이션이 getaddrinfo()를 호출한다 — 호스트 이름을 IP로 변환하는 표준 라이브러리 함수. 여기서 DNS가 시작된다.

내 컴퓨터는 DNS 해석을 직접 안 한다. 위임한다. 스텁 리졸버 — OS에 내장된 가벼운 DNS 클라이언트 — 가 쿼리를 받아서 재귀 리졸버에 전달한다. 어떤 재귀 리졸버? 네트워크 설정에 따라 다르다. 라우터, ISP의 리졸버, 또는 8.8.8.8이나 1.1.1.1 같은 퍼블릭.

스텁이 UDP 패킷 하나를 보낸다. “example.com의 A 레코드가 뭐야?” 그리고 기다린다. 이후 모든 일은 재귀 리졸버 쪽에서 일어난다.

재귀 리졸버의 캐시

재귀 리졸버도 자기 캐시부터 확인한다. 8.8.8.8 같은 바쁜 리졸버는 수조 개의 쿼리를 처리하고 공격적으로 캐싱한다. 누군가 최근에 example.com을 물어봤으면 답이 이미 있다. 응답이 돌아간다. 총 시간: 1-5밀리초 정도.

대부분의 쿼리가 여기서 끝난다. 캐싱 레이어가 DNS 트래픽의 대다수를 흡수한다. 인기 있는 도메인에 접속하면 재귀 리졸버에 캐시된 답이 거의 확실히 있다.

근데 없다고 하자. 캐시가 비었다. 이제 리졸버가 처음부터 답을 찾으러 가야 한다.

트리 걷기

DNS는 계층 구조다. 루트 존이 꼭대기. TLD가 그 아래(.com, .org, .net). 개별 도메인의 권한 있는 네임서버가 그 아래. 재귀 리졸버가 이 트리를 위에서 아래로 걷는다.

첫 번째 정거장: 루트. 리졸버가 루트 서버에 묻는다. “example.com의 A 레코드가 뭐야?” 루트는 모른다 — 개별 도메인 레코드를 저장하지 않는다. 하지만 .com을 누가 관리하는지는 안다. 그래서 리퍼럴을 돌려준다. “이 네임서버들에 물어봐.”

논리적 루트 서버가 13개 있다. A부터 M까지. 실제로는 1,500개 이상의 물리적 인스턴스가 애니캐스트로 전 세계에 분산되어 있다 — 같은 IP 주소가 위치에 따라 다른 머신에서 서빙되는 라우팅 기법. 리졸버가 “루트 서버”와 대화할 때, 가장 가까운 물리적 인스턴스와 대화하는 것이다. 리졸버에 루트 주소는 이미 캐시되어 있어서(거의 안 바뀐다) 이 단계는 빠르다. 왕복 한 번.

두 번째 정거장: TLD. 리졸버가 .com 서버에 묻는다. 같은 패턴. 최종 답은 없지만 example.com의 권한 있는 네임서버를 안다. 리퍼럴을 돌려준다. 왕복 한 번 더.

세 번째 정거장: 권한 있는 네임서버. 리졸버가 직접 묻는다. 이 서버가 실제 레코드를 갖고 있다. IP 주소로 응답한다. 마지막 왕복.

리졸버가 답을 레코드의 TTL(Time To Live)에 따라 캐시하고, 스텁 리졸버에 보내고, 스텁은 애플리케이션에 보내고, 애플리케이션은 그 IP로 TCP 연결을 연다.

완전히 캐시가 비어있으면 왕복 세 번. 실제로는 TLD 리퍼럴이 거의 항상 캐시되어 있어서, 흔한 TLD 아래 새 도메인의 “콜드” 쿼리는 보통 한두 번이면 된다.

50밀리초 예산

각 왕복이 지리적 거리에 따라 대략 5-30밀리초 걸린다. 완전 미캐시: 50-200밀리초. 부분 캐시(일반적): 10-50밀리초. 완전 캐시: 1밀리초 미만.

TTL이 중요한 이유다. 300초 TTL(5분)이면 리졸버가 5분마다 다시 쿼리한다. 86400초 TTL(24시간)이면 하루에 한 번. 짧은 TTL은 더 신선한 데이터지만 만료된 캐시에서 더 느리다. 긴 TTL은 더 빠른 응답이지만 레코드를 바꿨을 때 전파가 느리다.

“DNS 전파에 24-48시간 걸린다”는 말의 실제 의미: 전 세계 캐시가 TTL이 만료될 때까지 오래된 답을 들고 있다. 전파 메커니즘 같은 건 없다. 오래된 캐시가 각기 다른 시간에 만료될 뿐이다.

잘못될 수 있는 것

각 단계에서 실패할 수 있다.

NXDOMAIN. 도메인이 존재하지 않는다. 권한 있는 서버가 그렇다고 말한다. 깔끔한 실패.

SERVFAIL. 리졸버가 답을 얻으려 했는데 못 했다. 권한 있는 서버가 다운이거나, DNSSEC 검증이 실패했거나, 네트워크 문제일 수 있다. SERVFAIL은 “뭔가 깨졌다”의 범용 응답이고, 디버깅하려면 각 홉에서 쿼리를 추적해야 한다.

타임아웃. 리졸버가 쿼리를 보냈는데 아무것도 안 왔다. 2-5초 후 다른 서버로 재시도할 수 있다. 전부 타임아웃이면 쿼리가 실패한다. 사용자는 브라우저 에러 페이지를 본다.

캐시 포이즈닝. 공격자가 리졸버의 캐시에 가짜 응답을 주입한다. 그 도메인을 쿼리하는 모두가 가짜 답을 받는다. DNSSEC가 방지하는 것이다 — 낮은 채택률이 답답한 이유이기도 하다.

보이지 않는 기계

DNS는 인터넷 핵심에서 아직 돌아가는 가장 오래된 프로토콜 중 하나다. 네트워크 전체가 문서 하나에 매핑되던 시절에 설계됐다. 지금은 전 세계에서 하루에 약 2조 개의 쿼리를 처리한다.

대부분의 개발자가 하루에 수백 번 쓰면서 한 번도 의식하지 않는다. URL이 들어가고, 페이지가 뜨고, 그 사이의 50밀리초는 없는 것이나 마찬가지다.

하지만 존재한다. 다음에 페이지가 뚜렷한 이유 없이 느리게 뜨면 — DNS부터 확인해보자. 보이지 않는 기계가 버벅거리고 있을 수 있다.

토론 참여

← 블로그로 돌아가기