DNS TTL: 약속한 값 vs 리졸버가 실제로 하는 일

TTL을 300으로 잡고 5분이면 바뀌겠지 했는데, 한 시간이 지나도 트래픽이 옛 IP로 들어온다. TTL은 일정표가 아니다. 캐시들의 사슬에 건네는 힌트일 뿐이고, 그 캐시들은 저마다 당신을 무시할 권리를 갖고 있다.

마이그레이션 전날 운영자라면 한 번쯤 해봤을 그 동작. 레코드 TTL을 60초로 낮춰두면, IP를 바꾸는 순간 1분 안에 세상이 따라온다. 깔끔한 전환.

그런데 막상 바꾸고 한 시간이 지나도, 고집 센 일부 트래픽이 여전히 옛 서버를 두들기고 있다. TTL은 60이라고 했다. 60초를 기다렸다. 인터넷은 따르지 않았다.

문제는 설정에 있지 않다. 그 숫자가 말하는 대로 작동할 거라 믿은 데 있다. TTL은 인터넷이 지키기로 합의한 타이머가 아니다. 길게 늘어선 캐시들의 사슬에 건네는 제안이고, 그 캐시들은 저마다 그 값을 반올림하거나, 위에서 자르거나, 아래에서 떠받치거나, 조용히 무시할 권리를 갖고 있다.

그 숫자의 정체

TTL은 모든 DNS 레코드에 들어 있는 필드로, 멀게는 RFC 1035에 정의돼 있다. 세부는 나중에 RFC 2181이 못 박았다. 부호 없는 값이고 최댓값은 2,147,483,647 — 2³¹−1, 68년을 훌쩍 넘는다. 최상위 비트가 세팅된 TTL을 받으면 리졸버는 전체 값을 0으로 취급하라고 돼 있다. 그러니 합법적 범위는 “아예 캐시하지 마라”부터 “당신이 은퇴한 뒤까지 캐시해라”까지다.

이게 문서상의 계약이다. 권한 서버가 말하는 건 이거다. 이 응답을 최대 이만큼 캐시해도 된다. 핵심 단어는 된다이다. 프로토콜 어디에도 리졸버가 그만큼 응답을 유지하도록 강제하는 조항은 없고 — 여기가 발목을 잡는 지점인데 — 시계가 0이 되는 순간 응답을 버리도록 강제하는 조항도 없다.

위에서 잘리는 천장

레코드를 일주일로 잡고 하류로 뭐가 돌아오는지 보라. 흔히 하루다.

재귀 리졸버는 큰 TTL을 일상적으로 깎는다. 가장 널리 쓰이는 리졸버 중 하나인 Unbound는 cache-max-ttl 기본값이 86,400초 — 정확히 하루다. 레코드를 일주일로 설정해도 Unbound는 “노력은 가상하다”며 24시간만 캐시한다. RFC 8767도 이를 인정한다. 리졸버가 TTL을 며칠에서 몇 주 단위로 상한을 두되 7일을 합리적 천장으로 제안하는데, 실제 리졸버들이 이미 그렇게 하고 있기 때문이다.

그러니 긴 TTL은 당신 의도의 상한일 뿐, 현실의 상한이 아니다. 최댓값의 최종 결정권은 리졸버에 있고, 대개 당신이 요청한 것보다 짧다.

아래에서 떠받치는 바닥

좋다, 반대로 가서 빠른 변경을 위해 아주 작은 TTL을 잡아보자. 이것도 덮어쓰인다.

Unbound에는 짝이 되는 cache-min-ttl 옵션이 있다. 매뉴얼은 그 트레이드오프를 분명히 밝힌다. 이 값을 올리면 리졸버는 응답의 TTL을 무시하고, 출발지가 의도한 것보다 레코드를 더 오래 들고 있는다 — 도메인이 공격적으로 낮은 값을 잡았을 때 끊임없이 재질의하는 걸 막기 위해서다. 당신의 30초 TTL로부터 스스로를 보호하려는 리졸버는 그 응답을 기꺼이 몇 분씩 붙들고 있는다.

브라우저는 한술 더 뜬다. Chromium은 프로세스 안에 자체 DNS 캐시를 두는데, 운영체제 리졸버에 의존할 때는 당신의 TTL을 아예 볼 수 없다 — getaddrinfo()가 그걸 돌려주지 않기 때문이다. 그래서 Chromium은 엔트리를 일률적으로 60초로 묶어둔다. 페이지 하나를 그리는 동안 같은 이름을 두 번 해석하지 않도록, 한 번의 페이지 로드를 넉넉히 버티게 고른 값이다. TTL을 5초로 잡아도 Chrome은 60초를 유지한다. Chromium이 직접 해석해서 TTL을 볼 수 있을 때조차 60초가 바닥이다.

아무도 말해주지 않은 층들

층을 세어보면 그림은 더 나빠진다. 당신의 권한 서버와 사용자 브라우저 사이에는 재귀 리졸버, 운영체제의 스텁 캐시, 종종 사내 리졸버, 때로는 자체 캐싱을 하는 공유기, 그리고 마지막으로 브라우저가 있다. 하나하나가 캐시하고, 하나하나가 자기 시계를 돌린다.

게다가 TTL은 통과하면서 줄어든다. 리졸버가 다음 층에 응답을 넘길 때, 자기가 이미 들고 있던 시간을 빼고 준다. 그러니 dig에서 보이는 TTL은 원래 값이 아니라, 상류의 모든 캐시가 제 몫을 떼고 남은 값이다. 리졸버가 일주일짜리 TTL을 하루로 깎으면, 하류 클라이언트는 그 깎인 하루에서 줄어드는 카운트다운만 볼 뿐, 당신이 설정한 일주일은 영영 보지 못한다. 당신이 설정한 숫자와 사용자 기기가 실제로 따르는 숫자는 좀처럼 같은 숫자가 아니다.

네거티브 캐싱이라는 함정

마지막으로, 존재를 잊은 사람을 노리는 함정이 하나 있다. 이름이 해석되지 않을 때 — NXDOMAIN — 그 실패도 캐시된다. RFC 2308의 규칙에 따라서. 얼마나 오래? 당신이 만들려던 레코드의 TTL이 아니라, 그 존(zone) SOA 레코드의 minimum 필드가 정한다.

그래서 호스트명을 오타 내면, 리졸버는 그 존 SOA minimum에 맞춰 “존재하지 않음” 응답을 캐시하고, 그 다음 당신이 레코드를 고쳐도 — 이미 물어본 모두에게는 그 SOA minimum이 말하는 시간만큼 여전히 깨진 채다. 이미 고친 레코드를 붙잡고 한나절을 태우는 이유가 이것이다. 캐시된 것은 레코드의 부재이고, 그 수명은 확인할 생각조차 못 한 곳에 적혀 있다.

설정값이 아니라 최악의 행위자에 맞춰 설계하라

TTL을 약속으로 읽기를 멈추는 순간, 운영 규칙은 저절로 써진다.

마이그레이션 며칠 전에 TTL을 낮춰라, 몇 시간 전이 아니라 — 짧은 값이 의미를 갖기도 전에, 기존의 더 긴 TTL이 모든 캐시에서 먼저 빠져나가야 하기 때문이다. 전환 중에는 어떤 숫자를 잡았든 한 시간의 겹침을 가정하고, 긴 꼬리가 죽을 때까지 옛 엔드포인트를 계속 서비스하라. TTL을 30~60초 아래로 잡는 건 의미 없다 — 브라우저와 리졸버가 바닥에서 떠받치므로, 추가 질의 부하만 지불하고 응답성은 하나도 못 얻는다. 그리고 “오래된 레코드”를 디버깅하기 전에, 실제로 캐시된 것이 레코드 자체가 아니라 SOA가 지배하는 네거티브 응답은 아닌지 먼저 확인하라.

TTL이 거짓말은 아니다. 정확히는, 힌트다. 저마다 자기 부하와 자기 기본값에 견주어 그 값을 저울질하는, 독립된 캐시들의 사슬에 던지는 힌트. 그 사슬에서 가장 느리고 가장 고집 센 놈에 맞춰 설계하라 — 당신의 사용자가 갇혀 있는 곳이 바로 그놈 뒤이기 때문이다.

토론 참여

← 블로그로 돌아가기