DNS-кэширование и TTL — слои кэшей и негативное кэширование
DNS — это самая популярная сеть в мире. По данным Cloudflare и Google, в день делается ~10 триллионов DNS-запросов глобально. Если бы каждый запрос проходил полный путь через root -> TLD -> authoritative, root-серверы развалились бы за минуты.
DNS работает в таком масштабе только благодаря кэшированию. На каждом уровне (от твоего браузера до recursive resolver’а) данные кэшируются на сроки, указанные в TTL (Time-To-Live). Это уменьшает реальную нагрузку на authoritative-серверы в тысячи раз.
В этом уроке разберём:
- Слои кэшей на пути от приложения до интернета.
- Как TTL влияет на propagation изменений.
- Negative caching — кэширование «не существует».
- Что такое stale cache и как с этим жить.
- Cache poisoning и защиты.
Слои кэшей
Между твоим приложением и authoritative-сервером есть от 3 до 6 слоёв кэша:
Реальное распределение нагрузки: на authoritative-сервер приходит ~1% от того, что генерируют клиенты. Всё остальное обслуживается из кэшей.
TTL в деталях
TTL (Time-To-Live) — это unsigned 32-битное число в секундах, указанное в каждой DNS-записи. Когда resolver кэширует запись, он записывает её с этим TTL. По истечении — запись удаляется из кэша.
Типичные значения TTL:
- Short (30-300 сек): для часто меняющихся сервисов (CDN, A/B-тестирование, blue-green deployments).
- Medium (1-24 часа): для большинства веб-сайтов. Хороший баланс между гибкостью и нагрузкой.
- Long (1-7 дней): для NS-записей родительских зон (TLD), почти не меняются.
# Посмотри TTL на разных типах записей:
dig example.com SOA +noall +answer
# 86400 -- TTL SOA
dig example.com NS +noall +answer
# 86400 -- TTL NS (длинный)
dig www.github.com A +noall +answer
# 60 -- TTL A (короткий, для гибкости)
dig 8.8.8.8 -x +noall +answer
# Различные TTL -- зависит от провайдера
Чем короче TTL:
- Быстрая propagation при изменениях.
- Больше нагрузка на authoritative-серверы.
- Дольше резолв при cold cache.
Чем длиннее TTL:
- Меньше нагрузка на сеть.
- Быстрее warm cache hits.
- Долгая propagation изменений.
Стандартная стратегия:
- Обычные веб-сайты: 3600 (1 час) или 14400 (4 часа).
- Перед миграцией IP: понизить до 60-300 за день-два.
- После миграции: вернуть к нормальному уровню.
Как TTL «декрементируется» по дороге
Важный нюанс: когда recursive resolver кэширует запись с TTL=3600, и спустя 1000 секунд клиент его запрашивает, resolver отдаёт запись с TTL=2600. Это и есть остаток времени, в течение которого запись валидна.
Каждый последующий слой кэша делает то же самое — отвечает остаток TTL. Это значит, что если у тебя серия кэшей по пути, реальное «время жизни» записи определяется самым первым и долгим кэшем.
# Демонстрация декремента TTL:
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
dig +noall +answer www.example.com A
# 3600
# Подожди 30 секунд и запроси снова:
sleep 30
dig +noall +answer www.example.com A
# 3570 -- TTL уменьшился
# Подожди ещё:
sleep 30
dig +noall +answer www.example.com A
# 3540
Negative caching — кэш «не существует»
Что происходит, если ты запрашиваешь несуществующий домен? Resolver сходит до authoritative-сервера, получит NXDOMAIN, и закэширует этот факт тоже. Иначе при следующем запросе того же имени он опять пойдёт через всю иерархию.
Длительность негативного кэша определяется Minimum TTL в SOA-записи. По RFC 2308 это поле использовано именно для этого: «как долго кэшировать NXDOMAIN и NODATA для этой зоны».
# Посмотри SOA и Minimum TTL:
dig example.com SOA
# В SOA: NS hostmaster serial refresh retry expire MINIMUM
# Последнее число -- minimum TTL
# Запроси несуществующее имя -- оно тоже закэшируется:
dig nonexistent.example.com
# NXDOMAIN
dig +stats nonexistent.example.com
# Второй запрос -- мгновенный, из negative cache
Negative caching экономит много трафика: если кто-то делает typo в адресе сайта, не нужно гонять весь интернет, чтобы повторно сказать «нет».
Подвох: если ты только что зарегистрировал домен, а до этого кто-то его запрашивал и получил NXDOMAIN, resolver его кэшировал. Теперь, когда домен существует, эти resolver’ы какое-то время будут отдавать NXDOMAIN из кэша. Подожди или используй другие resolver’ы.
Browser DNS-кэш
Большинство современных браузеров поддерживают свой DNS-кэш, поверх OS-кэша. Это:
- Сокращает latency. Не нужно даже до OS resolver доходить.
- Защищает от плохих ISP resolvers. В некоторых случаях браузер делает запросы напрямую через DoH (DNS over HTTPS), минуя ISP.
- Создаёт путаницу. При смене IP сервера ты можешь продолжать видеть старый IP до перезапуска браузера или очистки кэша.
# Chrome -- посмотри и очисти DNS-кэш:
# В адресной строке:
chrome://net-internals/#dns
# Кнопка 'Clear host cache'
# Firefox -- about:networking#dns
# Safari -- менее доступно через UI, через Develop > Empty Caches
Это иногда экзотический источник проблем: ты обновил DNS, очистил OS кэш, но браузер всё ещё работает по старому. Перезапусти браузер или очисти DNS-кэш в нём.
Stale cache — проблема и решение
Stale cache — это запись в кэше, которая «устарела» (TTL истёк), но кэш всё ещё может её отдавать. Это происходит, когда:
- TTL expired, и нужно повторно запросить authoritative.
- Authoritative-сервер не отвечает (упал, сеть проблема).
- Resolver вынужден либо отдать ошибку, либо отдать stale запись.
Современные resolvers (Cloudflare, Google) реализуют serve stale: если authoritative недоступен, отдают старую запись — лучше «возможно устаревшая» страница, чем «не открылась вообще». В RFC 8767 это формализовано: запись может оставаться в «serve stale» состоянии до 7 дней при недоступности authoritative.
Это хороший паттерн для resilience: даже если у тебя падает DNS-инфраструктура, мир может ещё работать. Но это значит, что записи могут жить «дольше TTL» при определённых обстоятельствах.
Cache poisoning — атака на кэш
Идея атаки: подделать DNS-ответ так, чтобы resolver закэшировал неправильное значение. Если получится, все клиенты этого resolver’а в течение TTL будут идти на IP атакующего.
Классическая атака — Kaminsky attack (2008). Атакующий шлёт огромное количество DNS-запросов от имени resolver’а на authoritative-сервер, и параллельно шлёт поддельные ответы с предсказуемыми transaction ID. Если угадает — закэширует фейковую запись.
Защита:
- Random transaction ID + source port. Resolver выбирает случайный 16-битный transaction ID и случайный source port. Атакующему нужно угадать оба (~32 бита энтропии).
- DNSSEC. Криптографическая подпись делает поддельные ответы невалидными.
- DoH/DoT. Шифрование канала между клиентом и resolver’ом — атакующий не видит запросы.
- 0x20 encoding. Resolver рандомизирует регистр букв в имени (
gOOgLE.coM), authoritative-сервер должен ответить тем же регистром — это даёт дополнительную энтропию.
В современном интернете cache poisoning сложен, но не невозможен. Для критических сервисов используется DNSSEC.
Propagation — реальное время распространения
Если ты меняешь запись с TTL=60, через сколько все клиенты увидят новое значение?
Идеальный случай: 60 секунд. Худший случай: дни, недели.
Почему худший:
- Long initial TTL. Если до изменения TTL был 3600, клиенты с кэшем должны его доиграть. Только потом подхватят новое TTL=60.
- ISP resolvers ignore TTL. Многие провайдеры (особенно мобильные) кэшируют дольше TTL для экономии трафика.
- Stale cache. Resolvers могут отдавать старое значение, если authoritative недоступен.
- Application caches. Java по дефолту кэширует forever. Многие приложения не очищают DNS-кэш при долгой работе.
- CDN caches. Если ты используешь CDN-провайдер с anycast, синхронизация между точками может быть медленной.
Поэтому при смене IP всегда:
- За день-два до миграции: снизь TTL до 60. Тогда долгие кэши истекут к моменту смены.
- В момент смены: обновляешь A-запись. Большинство клиентов переключатся за минуты.
- Период миграции (1-7 дней): держи старый сервер работающим и отдающим тот же контент или редирект на новый.
- Long tail (недели-месяцы): некоторые клиенты могут долго ходить на старый IP. Это «нормально», нужно ли это лечить — зависит от твоего use case.
# Понизь TTL до 60 у DNS-провайдера за день до смены:
# (Это делается в admin UI Cloudflare/Route53/etc.)
# В день миграции проверяй, что весь мир видит низкий TTL:
for resolver in 8.8.8.8 1.1.1.1 9.9.9.9 208.67.222.222; do
echo "$resolver:"
dig @$resolver +short www.example.com A
dig @$resolver +noall +answer www.example.com A | head -1
done
# Если у всех TTL <= 60, можно безопасно менять
Что приложение должно делать
Несколько практических правил:
1. Не кэшируй DNS сам без хорошей причины
Многие фреймворки и языки кэшируют DNS-результат на уровне приложения. Это часто проблемнее, чем полезно. Если cluster IP меняется (балансировщик, k8s service), приложение продолжает использовать старый IP.
Особенно плохо — Java. По дефолту JVM кэширует DNS forever. Это бесконечный источник production проблем. Правильно:
// В -Dnetworkaddress.cache.ttl=60 (или меньше)
// В коде:
Security.setProperty("networkaddress.cache.ttl", "60");
Security.setProperty("networkaddress.cache.negative.ttl", "10");
2. Используй имена, не IP
В конфигурации сервиса — всегда имя. IP может смениться, имя — стабильнее. Если используешь свой DNS-сервер для service discovery (Consul, etcd, k8s) — имя позволяет ему динамически отдавать актуальный IP.
3. Делай retry на DNS ошибки
DNS-резолв может фейлиться даже на работающей сети. Retry с маленьким backoff обычно достаточно.
4. Помни о long-tail
Никогда не отключай старый сервер сразу после смены DNS. Минимум — неделя «переходного периода», лучше две.
Попробуй сам
# 1. Найди записи с разными TTL у популярных доменов:
for domain in github.com google.com www.example.com facebook.com; do
echo "=== $domain ==="
dig +noall +answer $domain A | head -1
done
# Видишь различия в TTL -- это политика владельца
# 2. Посмотри, как TTL декрементируется в кэше:
sudo dscacheutil -flushcache # macOS
# (или твоё ОС-специфичное)
for i in 1 2 3; do
dig +noall +answer +stats www.github.com A | head -1
sleep 5
done
# TTL уменьшается на 5 при каждом повторе
# 3. Negative caching демонстрация:
sudo dscacheutil -flushcache
time dig +short totally-nonexistent-xyz123.com # cold, ~50 мс
time dig +short totally-nonexistent-xyz123.com # warm, <1 мс
# 4. Очисти OS-кэш и сравни:
# macOS:
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
# Linux systemd-resolved:
sudo systemd-resolve --flush-caches
# 5. Сравни TTL у разных resolver'ов:
for r in 8.8.8.8 1.1.1.1 9.9.9.9; do
echo "Resolver $r:"
dig @$r +noall +answer www.example.com A | head -1
done
# 6. Browser-cache test (если на macOS с Chrome):
# Открой chrome://net-internals/#dns
# Видишь все домены, которые сидят в кэше Chrome
# 7. Force flush и сравни время:
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
time curl -s -o /dev/null https://example.com # cold include DNS
sleep 1
time curl -s -o /dev/null https://example.com # warm
CoreDNS TTL в Kubernetes: почему pod DNS обновляется быстро