Модуль: Сети и протоколы · Уровень: Middle+/Senior
TL;DR#
- DNS — распределённая иерархическая система имён; резолвер рекурсивно идёт от root → TLD → authoritative, отдавая записи с TTL.
- Типы записей: A/AAAA (IP), CNAME (алиас), MX (почта), TXT (метаданные/SPF), NS, SOA, SRV (сервис+порт), PTR (обратный), CAA.
- Кэширование на каждом уровне по TTL — отсюда главные продовые боли: устаревшие записи, “залипший” резолв при деплое/failover.
- В Go два резолвера: cgo (системный) и чистый Go; выбор влияет на поведение
/etc/hosts,/etc/resolv.conf, кэширование (которого в Go-нет по умолчанию). - Прод-грабли: отсутствие кэша в Go → DNS на каждый запрос, негативное кэширование, TTL vs балансировка, DNS-таймауты как причина “медленных” сервисов.
Теория#
Иерархия и резолвинг#
root (.)
|
TLD (.com, .org, .ru)
|
authoritative (example.com NS)- Recursive resolver (обычно у провайдера / 8.8.8.8 / внутренний) делает работу за клиента: спрашивает root → получает NS для TLD → спрашивает TLD → получает authoritative NS → получает финальный ответ.
- Stub resolver — это сам клиент (
getaddrinfo/Go), который шлёт запрос рекурсивному резолверу. - Транспорт: UDP/53 (дефолт), TCP/53 при truncation (флаг TC) или zone transfer. Современное: DoT (DNS over TLS, 853), DoH (DNS over HTTPS, 443).
Типы записей#
| Тип | Назначение |
|---|---|
| A | hostname → IPv4 |
| AAAA | hostname → IPv6 |
| CNAME | алиас на другое имя (нельзя на apex-домене вместе с другими записями) |
| MX | почтовые серверы (+приоритет) |
| TXT | произвольный текст: SPF, DKIM, верификации |
| NS | делегирование зоны на name servers |
| SOA | параметры зоны (serial, refresh, TTL негатива) |
| SRV | _service._proto → host+port (service discovery) |
| PTR | обратный резолв IP → имя |
| CAA | какие CA могут выдавать сертификаты для домена |
TTL и кэширование#
- Каждая запись несёт TTL — сколько секунд её можно кэшировать.
- Кэш на всех уровнях: stub, рекурсивный резолвер, ОС, иногда приложение.
- Negative caching: NXDOMAIN тоже кэшируется (TTL берётся из SOA minimum) — поэтому только что созданная запись может “не находиться” какое-то время.
- Trade-off TTL: низкий TTL → быстрый failover/смена IP, но больше нагрузки и латентности; высокий TTL → меньше запросов, но медленная пропагация изменений. Перед миграцией TTL заранее снижают.
CNAME-нюансы#
- CNAME — алиас, требует доп. резолва на целевое имя.
- На apex (корне зоны,
example.com) CNAME запрещён стандартом (конфликт с SOA/NS) → провайдеры предлагают ALIAS/ANAME/CNAME flattening. - Цепочки CNAME увеличивают латентность резолва.
DNS в Go#
Go имеет два резолвера:
- cgo-based (системный) — вызывает
getaddrinfo, уважает nsswitch, mDNS, сложные конфиги. Используется по умолчанию на некоторых платформах. - pure Go resolver — сам парсит
/etc/resolv.confи шлёт DNS-запросы; кроссплатформенный, без cgo.
Выбор:
// Принудительно чистый Go
net.DefaultResolver.PreferGo = true
// или через env: GODEBUG=netdns=go (или netdns=cgo)- Go не кэширует DNS в приложении (полагается на ОС/резолвер). Без локального кэширующего резолвера каждый
net.Dialпо имени = DNS-запрос. - Кастомный резолвер:
r := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 2 * time.Second}
return d.DialContext(ctx, network, "1.1.1.1:53") // свой DNS
},
}
ips, err := r.LookupHost(ctx, "example.com")net.LookupHost/LookupIP/LookupSRV/LookupTXT/LookupCNAME/LookupMX— типизированные запросы.- Контроль через
Dialer.ResolverиResolver.LookupIPAddr(контекст-aware, с таймаутом).
Подводные камни / gotchas#
- Нет DNS-кэша в Go: высоконагруженный сервис, дёргающий хост по имени, генерит DNS-запрос на каждое соединение. Решение: локальный кэширующий резолвер (systemd-resolved, dnsmasq, CoreDNS/NodeLocal в k8s) или кэш в приложении.
- Залипший резолв при failover: приложение/пул держит старый IP (закэшированный или удерживаемый keep-alive соединением) после смены DNS. HTTP-пул переиспользует соединение со старым IP, игнорируя новый DNS. Нужны ограничение времени жизни соединений и/или повторный резолв.
- TTL=0 / низкий TTL не помогает, если приложение само кэширует или держит соединения. DNS-балансировка слаба именно из-за кэшей.
- NXDOMAIN кэшируется (negative caching) — новая запись “не видна” до истечения negative TTL.
- CNAME на apex не работает — типичная ошибка конфигурации.
search-домены в/etc/resolv.conf(особенно в Kubernetesndots:5) → каждый внешний запрос порождает несколько лишних lookup’ов (перебор search-суффиксов) → задержки и нагрузка. Классическая k8s-проблема, лечитсяndotsили FQDN с точкой на конце.- cgo vs pure-Go расхождения: разный парсинг
/etc/hosts, поведение при нескольких A-записях, поддержка mDNS — поведение может отличаться между средами. - DNS-таймауты маскируются под “медленный сервис”: блокирующий резолв с дефолтным таймауту добавляет секунды к запросу. Всегда контекст с таймаутом на резолв.
- IPv4/IPv6 (AAAA): лишние AAAA-запросы и неудачные IPv6-подключения добавляют латентность; Happy Eyeballs (Go умеет) смягчает.
Вопросы на собеседовании#
В: Опиши путь рекурсивного резолва example.com. О: Stub-резолвер шлёт запрос рекурсивному резолверу. Тот (если нет в кэше) спрашивает root-сервер → получает NS для .com → спрашивает TLD-сервер → получает authoritative NS example.com → спрашивает его → получает A-запись. Результат кэшируется по TTL на каждом уровне.
В: Разница между A, CNAME и ALIAS? О: A — прямое отображение имя→IP. CNAME — алиас на другое имя (требует доп. резолва), запрещён на apex. ALIAS/ANAME — провайдерское решение для apex: ведёт себя как CNAME, но резолвится на стороне DNS-провайдера и отдаётся как A.
В: Почему изменение DNS “не применяется сразу”? О: Кэширование по TTL на всех уровнях (резолверы, ОС, приложение) + negative caching для несуществовавших записей. Старые значения живут до истечения TTL. Перед миграцией TTL снижают заранее.
В: Кэширует ли Go DNS? Чем это грозит? О: Нет, стандартная библиотека не кэширует — полагается на системный/рекурсивный резолвер. Под нагрузкой без локального кэша это DNS-запрос на каждое новое соединение → нагрузка и латентность. Решение: кэширующий резолвер на хосте или кэш в приложении.
В: Сервис не переключился на новый IP после failover, хотя DNS обновлён. Почему?
О: Либо закэшированный старый IP, либо (чаще) HTTP-пул держит keep-alive соединение со старым IP и не делает новый резолв. Лечится IdleConnTimeout, ограничением жизни соединений, форсированным реконнектом или резолвом перед каждым соединением.
В: Что такое проблема ndots в Kubernetes?
О: ndots:5 в /etc/resolv.conf пода заставляет резолвер сначала перебирать search-домены для имён с <5 точками → много лишних DNS-запросов на каждый внешний хост. Лечится снижением ndots, использованием FQDN с завершающей точкой, NodeLocal DNS-кэшем.
В: Когда DNS использует TCP вместо UDP? О: Когда ответ не влезает в лимит UDP (флаг TC — truncated), при zone transfer (AXFR/IXFR), при DNSSEC с большими ответами. Клиент видит TC и переотправляет запрос по TCP/53.
В: Чем отличаются cgo и pure-Go резолверы и как выбрать?
О: cgo (getaddrinfo) уважает nsswitch/mDNS/сложные конфиги, но требует cgo и блокирует поток в системном вызове. Pure-Go сам парсит resolv.conf, кроссплатформенный, контекст-aware. Выбор: GODEBUG=netdns=go|cgo или Resolver.PreferGo. Для контейнеров/статической линковки обычно pure-Go.
На что копают на senior+#
- Взаимодействие DNS-TTL и connection pooling: почему DNS-балансировка ненадёжна и как делать клиентскую балансировку/повторный резолв.
- NodeLocal DNSCache, CoreDNS в k8s, autopath, кэш negative-ответов и их влияние на latency tail.
- DoT/DoH: приватность, обход цензуры, как это меняет операционную видимость и debugging.
- DNSSEC: цепочка подписей (RRSIG/DNSKEY/DS), что он защищает (целостность) и не защищает (конфиденциальность).
- Happy Eyeballs (RFC 8305) в Go: параллельные IPv4/IPv6 попытки, тюнинг fallback delay.
- Service discovery через SRV-записи vs Consul/etcd; почему DNS-based discovery ограничен (нет весов/health в стандартном DNS).
- Диагностика: dig +trace, проверка авторитетных серверов, TTL, расхождения между резолверами; почему
nslookupврёт про кэш приложения.