네거티브 캐싱: DNS는 '없음'도 기억한다

레코드를 추가했는데 세상은 여전히 '존재하지 않는다'고 답한다. 레코드는 멀쩡하다. DNS가 그 부재를 캐싱했을 뿐이다 — 당신이 설정한 적 없는 TTL, 아무도 건드리지 않는 SOA 필드가 지배하는 수명으로.

DNS 레코드를 추가한다. 존(zone)은 정확하고, 권한 서버는 깔끔하게 응답하며, dig @your-ns는 답을 돌려준다. 그런데 노트북에서 확인해 보면 나머지 인터넷은 여전히 그 이름이 존재하지 않는다고 우긴다. 오타를 낸 적도 없다. 레코드는 거기 있다. 그런데 앞으로 한 시간 동안, 세상의 일부는 눈앞에서 멀쩡히 풀리는 이름에 대해 NXDOMAIN을 받는다.

레코드에는 아무 문제가 없다. 당신이 싸우고 있는 건 그 부재의 캐싱된 기억이다.

DNS가 응답을 캐싱한다는 건 누구나 안다. TTL의 존재 이유가 그것이니까. 그런데 DNS가 응답의 부재를 똑같은 확신으로, 그것도 별도의 시계 위에서 캐싱한다는 사실을 체득한 사람은 훨씬 적다. 이건 버그가 아니다. RFC까지 있다.

애초에 ‘없음’을 왜 캐싱하는가

RFC 2308 — Negative Caching of DNS Queries (DNS NCACHE), M. Andrews, 1998년 3월 — 이 존재하는 이유는, DNS 트래픽의 놀라울 만큼 큰 비중이 영영 풀리지 않을 이름들을 향하기 때문이다. 오타. 철자 틀린 도메인을 계속 조회하는 메일 서버. 싱크홀에 박힌 이름으로 비콘을 쏘는 멀웨어. 캡티브 포털을 탐지하려는 브라우저. 이 모든 질의가 매번 트리를 끝까지 걸어 권한 서버에 도달해야 한다면, 루트와 TLD 운영자는 순전히 실수로만 이루어진 영구적 DoS를 떠안게 된다.

그래서 리졸버는 실패를 캐싱한다. 존재하지 않는 이름을 물으면 리졸버는 “없음”을 기억해 두고, 같은 실수를 하는 다음 사람에게 로컬에서 답한다. 이 최적화 자체는 옳고 또 필요하다. 함정은 얼마나 오래 기억하느냐의 디테일에 있다.

‘없음’에는 두 종류가 있다

사람들이 헷갈리는 지점이다. 서로 다른 두 실패를 한 단어로 뭉뚱그리기 때문이다.

NXDOMAIN은 RCODE 3, 이름 오류(name error)다. 이름 자체가 존에 없다. widget.example.com은 어떤 레코드 타입으로도 존재하지 않는다. 끝.

NODATA는 더 교묘하다. 이름은 존재하지만, 당신이 물은 레코드 타입이 없다. MX만 있는 호스트에 A 레코드를 질의하는 경우 — 이름은 실재하고, 그저 그 타입의 데이터가 없을 뿐이다. 여기엔 별도의 wire 코드가 없다. RFC의 표현으로 NODATA는 “pseudo RCODE”다. 응답은 NOERROR에 빈 answer 섹션으로 돌아오고, 당신은 그 부재로부터 NODATA 상황을 추론해야 한다. 둘 다 네거티브 응답으로 캐싱되고, 둘 다 같은 방식으로 발목을 잡는다.

당신이 설정한 적 없는 TTL

오후를 통째로 날리는 부분이 여기다. 리졸버가 네거티브 응답을 캐싱할 때, 그 수명을 결정하는 건 무엇일까? 당신이 만들려던 레코드의 TTL은 아니다 — 그 레코드는 아직 존재하지도 않을 수 있으니, 내놓을 TTL도 없다.

수명은 존의 SOA 레코드에서 온다. 정확히는 RFC 2308이 그 값을 SOA의 MINIMUM 필드와 SOA 레코드 자신의 TTL 중 더 작은 쪽으로 정한다. 이를 강제하기 위해, 권한 서버는 모든 NXDOMAIN과 NODATA 응답의 authority 섹션에 SOA를 반드시 포함해야 한다. 리졸버는 그 SOA를 캐싱하고, 캐싱된 “없음”을 내줄 때마다 그동안 보관한 시간만큼 SOA의 TTL을 깎아 네거티브 응답이 정확히 만료되게 한다.

즉 “내 실수는 얼마나 오래 살아남는가”의 답은, 거의 아무도 들여다보지 않는 SOA 레코드의 마지막 필드에 들어 있다.

그리고 그 필드에는 헷갈리는 역사가 있다. SOA MINIMUM 값은 오랫동안 세 가지 일을 떠맡았다. 존 내 모든 레코드의 최소 TTL, TTL이 없는 레코드의 기본 TTL, 그리고 네거티브 응답의 TTL. RFC 2308은 앞의 두 의미를 버렸다. 살아남은 유일한 의미가 네거티브 캐싱이다. 그러니까 2014년에 템플릿에서 복사한 뒤로 다시 생각해 본 적 없는 그 숫자가, 오늘날 세상이 당신의 장애를 얼마나 오래 기억할지를 결정하는 단 하나의 설정이다.

리졸버도 당신을 다 믿지는 않는다

SOA minimum을 터무니없이 크게 잡았다면 일주일짜리 장애를 각오해야 할 것 같지만, 실제로 리졸버는 이 값을 캡(cap)한다. BIND는 max-ncache-ttl로 네거티브 응답을 제한하는데 기본값이 3시간, 상한은 7일로 고정돼 있다 — 더 높게 잡으면 BIND가 조용히 잘라낸다. Unbound에는 cache-max-negative-ttl이 있고 기본값은 1시간이다.

좋은 소식이자 나쁜 소식이다. 거대한 SOA minimum이 당신을 일주일씩 묶어 두진 않는다. 하지만 기본값 — 한 시간에서 세 시간 — 만으로도, 갓 만든 레코드가 배포 윈도우 내내 고장 난 것처럼 보이게 하기엔 충분하다. 설정은 완벽히 맞는데도 말이다.

아무도 대비하지 않는 비대칭

이게 진짜 교훈이고, 설정 버그라기보다 사고방식의 버그다.

사람들은 자신이 만드는 레코드의 TTL에는 안달한다. 마이그레이션 전에 60초로 낮추고, 전파를 신중히 따진다. 그런데 아직 존재하지 않는 레코드의 TTL은 거의 아무도 생각하지 않는다 — 거기엔 TTL을 가질 것이 없는 듯 느껴지니까. 하지만 부재에도 TTL이 있고, 그건 당신이 고른 게 아니다. SOA minimum을 DNS 제공업체 기본값 그대로 두는 건 “결정하지 않음”이 아니다. 모든 성급한 조회와 방금 삭제한 레코드가 얼마나 오래 당신을 따라다닐지를, 기본값으로 결정해 버린 것이다.

그래서 실무 습관 두 가지. 무언가가 일찍 조회할 수 있는 이름을 띄우기 전에 — 헬스 체크, 모니터링 프로브, 아직 살아 있지도 않은 호스트명에 자기 캐시를 데우는 CDN — 레코드를 먼저 만들어 이전 네거티브 응답이 만료되게 하거나, 몇 시간 앞서 SOA minimum을 낮춰 윈도우를 줄여라. 그리고 “추가했는데 안 풀린다”를 디버깅할 때는, 캐싱된 것이 당신의 레코드인지 아니면 그 부재인지부터 따져라. 둘은 완전히 다른 시계로 만료되고, 엉뚱한 쪽을 무한정 기다릴 수도 있다.

DNS는 어디에 무엇이 있는지만 기억하지 않는다. 어디에 무엇이 없는지도, 똑같이 집요하게, 당신이 설정하지 않은 시계 위에서 기억한다. 이미 고친 버그는 어딘가에서 여전히 참(true)이고 — 거기엔 당신이 고른 적 없는 TTL이 붙어 있다.

토론 참여

← 블로그로 돌아가기