Вы вводите в браузере example.com и через долю секунды видите страницу. Но компьютеры не умеют соединяться по имени — им нужен числовой адрес вроде 203.0.113.5. Кто-то должен на лету превратить понятное человеку имя в этот адрес. Этим и занимается DNS (Domain Name System) — служба, которая работает как телефонная книга интернета: вы знаете имя, она возвращает номер.

Для backend-разработчика DNS — это не только «браузер открывает сайт». Ваш сервис ходит к базе по имени db.internal, к соседнему сервису по имени, к внешнему API по домену. Каждый такой вызов начинается с похода в DNS. И когда что-то ломается на этом шаге, симптомы бывают коварные — вплоть до «у меня всё работает, а в проде нет». Разберёмся, как это устроено.

Весь механизм умещается в одну картинку: один холодный поход по цепочке серверов, дальше ответы из кэша — и цена этой скорости видна ровно в момент переезда.

кэш экономит три шага — и на TTL секунд отстаёт от правды ваш код api.example.com рекурсивный резолвер кэш пуст api.example.com203.0.113.10 · TTL 3600 холодный запрос: три шагаответ ложится в кэш ответ из кэша: ноль шаговTTL 3600 ещё не истёк кэш отдаёт старый адресновый увидит после TTL 123 корневой сервер знает, кто держит .com TLD-сервер .com знает, кто держит example.com авторитетный example.com A 203.0.113.10 · TTL 3600 авторитетный example.comA 203.0.113.99 · TTL 3600адрес сменили после переезда старый адрес живёт в кэшах: TTL 3600 60 мин TTL 60 1 мин снизили TTL за сутки до переезда

Первый запрос стоит трёх шагов по цепочке: корень → .com → авторитетный сервер. Дальше резолвер отвечает из кэша за ноль шагов — но ровно на TTL секунд он отстаёт от правды: когда адрес меняют, кэш ещё TTL секунд отдаёт старый. Полосы в одном масштабе: TTL 3600 — окно до 60 минут, заранее сниженный TTL 60 — до одной.

Обязательно

DNS как телефонная книга интернета

Представьте старую бумажную телефонную книгу: слева имена, справа номера. Хотите позвонить — находите имя, читаете номер. DNS делает ровно то же самое, только для сети: на входе имя api.example.com, на выходе IP-адрес 203.0.113.10.

Разница в одном: единой книги не существует. Имён в мире миллиарды, и держать их в одном файле невозможно. Поэтому справочник распределён — разбит на части, за которые отвечают разные серверы по всему миру. Когда вам нужен адрес, система обходит несколько таких серверов и по кусочкам собирает ответ. Со стороны это выглядит мгновенно, но внутри — небольшое путешествие.

Путь запроса: от резолвера до авторитетного сервера

Допустим, вашему коду нужен адрес api.example.com. Вот что происходит по шагам:

  1. Рекурсивный резолвер. Ваша машина спрашивает не весь интернет, а один сервер-посредник — рекурсивный резолвер (обычно это DNS вашего провайдера или публичный вроде 8.8.8.8). Его задача — сбегать за ответом вместо вас и вернуть готовый IP.
  2. Корневые серверы. Резолвер не знает адрес сразу, поэтому идёт по цепочке сверху вниз. Сначала он спрашивает корневой сервер: «Кто отвечает за зону .com?» Корень не знает конкретный адрес api.example.com, но знает, куда обратиться дальше.
  3. TLD-серверы. Корень отправляет резолвер к серверам зоны .com (TLD — top-level domain, домен верхнего уровня). Те тоже не знают финальный адрес, но знают, кто отвечает за example.com.
  4. Авторитетный сервер. Наконец резолвер приходит к авторитетному серверу домена example.com — это тот сервер, который держит настоящие записи этого домена. Он и отдаёт финальный ответ: api.example.com → 203.0.113.10.

Аналогия: вы ищете человека в огромном здании. На входе (корень) вам говорят: «Такие — на 7 этаже». На этаже (TLD) — «Кабинет 712». В кабинете (авторитетный сервер) наконец сидит тот, кто знает точный ответ. Резолвер проходит этот путь за вас и запоминает дорогу, чтобы в следующий раз не бегать заново.

Ездят эти вопросы по UDP на порт 53: ни рукопожатия, ни соединения — один пакет с вопросом, один с ответом. Потому DNS и приводят как главный пример UDP: переспросить дешевле, чем открывать соединение ради двух сотен байт. Но если ответ в один пакет не влезает (исторический потолок — 512 байт) или речь о переносе целой зоны между серверами, тот же вопрос повторяют по TCP на тот же 53-й порт.

Типы записей: A, AAAA, CNAME, MX, TXT

У авторитетного сервера хранится не один адрес, а набор записей разных типов, и каждая нужна в свой момент. Имя в адрес превращают записи A для IPv4 (example.com → 203.0.113.5) и AAAA для IPv6 (2001:db8::5, читается «quad-A»): это то, что заводят при выкате сервиса. Когда несколько имён должны вести на один узел, заводят CNAME, псевдоним: www.example.com указывает не на адрес, а на имя example.com, и адрес меняют в одном месте. Когда для домена настраивают почту, нужна MX (mail exchange): почтовый сервер отправителя смотрит её, чтобы узнать, какой сервер принимает письма для user@example.com. А когда хостинг просит «подтвердить владение доменом» или письма от вас улетают в спам, заводят TXT, произвольный текст: там живут проверочные строки и настройки почты SPF и DKIM, которыми вы доказываете, что письма от вашего имени отправляете именно вы.

Так выглядит фрагмент зоны на авторитетном сервере:

example.com.      3600  IN  A      203.0.113.5
www.example.com.  3600  IN  CNAME  example.com.
example.com.      3600  IN  MX     10 mail.example.com.
example.com.      3600  IN  TXT    "v=spf1 include:_spf.example.com ~all"

Число 3600 в начале — это TTL, о нём дальше.

Посмотреть записи можно самому. Утилита dig показывает ответ детально:

$ dig api.example.com A +short
203.0.113.10

А nslookup — попроще, для быстрой проверки:

$ nslookup example.com
Name:    example.com
Address: 203.0.113.5

Кого спрашивает ваша машина: resolv.conf, домены поиска и ndots

Прежде чем запрос уйдёт в интернет, его обрабатывает локальный разрешатель имён, и настроен он файлом /etc/resolv.conf:

nameserver 10.96.0.10
search my-namespace.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

Три строки, и каждая даёт свой класс сюрпризов.

nameserver — к кому идти с вопросом. Их может быть несколько, и опрашиваются они по порядку с таймаутом; недоступный первый сервер означает, что каждое разрешение имени начинается с ожидания.

search — список доменов, которые дописываются к коротким именам. Запросили payments — система попробует payments.my-namespace.svc.cluster.local, потом payments.svc.cluster.local и так далее. Это удобно внутри кластера и опасно снаружи: короткое имя может случайно совпасть с чужим.

ndots — сколько точек должно быть в имени, чтобы его считали полным и спросили как есть. При ndots:5 имя api.partner.com (две точки) считается неполным, и система сначала переберёт все домены поиска: api.partner.com.my-namespace.svc.cluster.local, потом api.partner.com.svc.cluster.local, и только потом спросит api.partner.com. То есть каждое обращение к внешнему адресу стоит четырёх лишних запросов и четырёх ответов «нет такого имени».

Это и есть главная причина «медленных обращений к чужому API» внутри Kubernetes, и лечится она двумя способами: точкой в конце имени (api.partner.com. — полное имя, поиск не применяется) или явным ndots: 1 в настройках пода. Разбор этой ловушки с командами — в статье про диагностику.

DNS как балансировщик: round-robin и SRV

Простейшая балансировка не требует балансировщика: в зоне заводят несколько A-записей на одно имя, и разрешатель отдаёт их в изменяющемся порядке — клиенты расходятся по адресам. Так же клиенты находят и сам балансировщик: у его имени два-три адреса, и падение одного не оставляет сервис без входа.

Ограничения у этого способа серьёзные, и знать их важно. Кэш на стороне клиента означает, что убранный из зоны адрес продолжают спрашивать, пока не истечёт время жизни записи. Проверок работоспособности нет вовсе: DNS не знает, что один из адресов мёртв, — он честно его отдаёт. И распределение получается «примерно ровным», без учёта нагрузки.

SRV-записи дают больше: кроме адреса в них есть порт, приоритет и вес. То есть клиент узнаёт не только «куда», но и «в каком порядке пробовать» и «в какой пропорции». На них построено обнаружение сервисов в инфраструктурах без Kubernetes, и именно они позволяют менять порт сервиса, не меняя клиента.

_payments._tcp.internal.example.com. 30 IN SRV 10 60 8080 pay-a.internal.example.com.
_payments._tcp.internal.example.com. 30 IN SRV 10 40 8080 pay-b.internal.example.com.

Читается так: приоритет 10 у обоих (одна группа), вес 60 против 40 (распределение), порт 8080. Клиент должен уметь такие записи читать — это не бесплатно, но зато не требует ни балансировщика, ни привязки к порту.

Корень домена и CNAME: как всё-таки сделать

Запрет «CNAME на корне домена нельзя» оставляет читателя в тупике: а как тогда направить example.com (без www) на балансировщик, у которого только имя, а не адрес? Три рабочих пути.

Особая запись у провайдера DNS. Большинство провайдеров придумали нестандартные типы (ALIAS, ANAME, «плоский CNAME»), которые снаружи выглядят как обычная A-запись: провайдер сам разрешает целевое имя и отдаёт его адреса. Это самый распространённый ответ, и он же привязывает вас к провайдеру.

Перенаправление на поддомен. На корне оставляют A-запись на маленький сервер (или на функцию провайдера), задача которого — ответить постоянным перенаправлением на www.example.com, где уже стоит CNAME. Работает всегда, стоит одного лишнего перехода для пользователя.

Свой статический адрес. Если балансировщик умеет выдать постоянный адрес (в облаках это отдельная услуга), на корне ставят обычную A-запись на него. Тогда CNAME не нужен вовсе, но адрес придётся менять руками, если инфраструктура переезжает.

Кому вы доверяете разрешение имён

Ответ разрешателя провайдера можно подменить — и это не теория: провайдеры подменяют ответы для блокировок, для показа своих страниц вместо «нет такого имени», а в открытых сетях этим пользуются и злоумышленники. Обычный DNS-запрос идёт без шифрования и без подписи, и увидеть его может любой на пути.

Отсюда два механизма, которые сегодня встречаются постоянно. Шифрованный DNS (по HTTPS или по TLS) уводит запросы в шифрованный канал к выбранному разрешателю — браузеры включают это сами, и в результате у браузера и у операционной системы могут оказаться разные ответы на одно имя. Это и есть частый ответ на вопрос «почему у меня работает, а у коллеги нет» и «почему dig показывает одно, а браузер идёт в другое место». DNSSEC решает другую задачу — подписывает записи, чтобы подмену можно было обнаружить; шифрования он не даёт.

Практический вывод для разбора инцидентов: сравнивая ответы, надо указывать, кто отвечал (dig @8.8.8.8 имя против dig @10.96.0.10 имя), и помнить, что приложение, браузер и оболочка могут спрашивать разных разрешателей.

TTL и кэширование: почему изменения не мгновенны

Бегать по всей цепочке резолвинга на каждый запрос было бы медленно и накладно. Поэтому ответы кэшируются — запоминаются на время. И вот тут ключевую роль играет TTL (Time To Live) — то самое число 3600 из записи. Оно говорит: «этот ответ можно считать актуальным столько-то секунд». 3600 — значит один час.

Пока не истёк TTL, резолверы и ваша машина отдают запомненный адрес, не переспрашивая авторитетный сервер. Это быстро и снимает нагрузку. Но у этого есть обратная сторона: когда вы меняете адрес домена, старый ответ ещё живёт в кэшах по всему миру ровно столько, сколько разрешает TTL. Отсюда фраза «изменения DNS распространяются не мгновенно» — на самом деле ничего никуда не «распространяется», просто по всей планете постепенно истекают закэшированные ответы.

Практический вывод: если вы планируете переезд сервиса на новый адрес, заранее снизьте TTL записи (например, до 60 секунд) за сутки до переезда. Тогда в момент смены старый адрес протухнет в кэшах за минуту, а не за час. После переезда TTL можно вернуть обратно.

Запоминают, кстати, не только удачные ответы. «Такого имени нет» кэшируется точно так же — это называют отрицательным кэшированием, и срок у него свой: в зоне его задаёт последнее число в SOA-записи, а в JVM — отдельная настройка networkaddress.cache.negative.ttl, по умолчанию 10 секунд. Отсюда растёт классическое «запись создал, а она не резолвится»: пока вы настраивали и проверяли, резолвер успел запомнить, что имени нет, и честно повторяет это, пока срок не выйдет. Лечится не правкой записи ещё раз, а ожиданием.

/etc/hosts: локальное переопределение

До всякого DNS операционная система сначала заглядывает в маленький локальный файл — /etc/hosts (на Windows — C:\Windows\System32\drivers\etc\hosts). Это простой список «имя — адрес»:

127.0.0.1    localhost
127.0.0.1    api.example.com

Если имя нашлось здесь — система берёт адрес отсюда и в DNS вообще не идёт. Это удобно для локальной разработки: можно заставить api.example.com указывать на вашу машину, чтобы тестировать код против локального сервиса под «настоящим» именем.

Но эта же удобная штука — источник классической путаницы. Строчка, добавленная в hosts пару месяцев назад и забытая, продолжает молча перенаправлять имя. Отсюда ситуации «у меня работает, у коллеги нет»: у вас в hosts живёт переопределение, о котором вы не помните. Если резолвинг ведёт себя странно — загляните в hosts в первую очередь.

DNS внутри Kubernetes

В Kubernetes DNS — это основа того, как сервисы находят друг друга. Вместо того чтобы прописывать IP-адреса подов (которые постоянно меняются при перезапусках), сервисы обращаются друг к другу по имени. Внутри кластера работает собственный DNS (обычно CoreDNS), и каждый Service автоматически получает имя.

Например, Service с именем orders в пространстве имён default доступен другим подам просто как orders или полным именем orders.default.svc.cluster.local. Ваш код пишет запрос на http://orders/..., кластерный DNS превращает имя в актуальный адрес, а трафик балансируется между живыми подами. Это и называется service discovery — обнаружение сервисов по имени. Тот же принцип «имя вместо адреса», что и в большом интернете, только внутри кластера.

Переехали, а сервис ходит по старому адресу: DNS-кэш в приложениях

Кэшируют не только резолверы в сети — кэширует и сам процесс. И тут среды ведут себя по-разному: JVM по умолчанию держит результаты резолвинга прямо в памяти приложения, а Go, Node.js и Python в базовой поставке не запоминают ничего и спрашивают заново каждый раз. Кэш у них, конечно, есть, но снаружи процесса: либо в операционной системе (systemd-resolved, nscd), либо во внешней библиотеке, которую подключили специально. Так что «кэш прямо внутри приложения, о котором вы не просили» — особенность именно JVM, и именно она чаще всех и удивляет. Если внешний адрес поменялся, а ваш сервис уже успел его закэшировать, он продолжит стучаться по старому адресу, даже когда DNS давно отдаёт новый. Перезапуск процесса сбрасывает этот кэш — отсюда народное «помогло перезапустить».

У каждой среды свой способ управлять временем жизни этого кэша (в JVM это networkaddress.cache.ttl; по умолчанию адрес живёт в кэше 30 секунд, но если приложение запущено со старым менеджером безопасности, кэш становится вечным — и переезд внешнего API чинится только перезапуском). Похожие внутренние кэши есть и у HTTP-клиентов, connection-пулов и sidecar-прокси. Так что когда после смены адреса «сервис A всё ещё ходит на старый B», подозреваемых несколько: кэш ОС, кэш резолвера, кэш приложения. Тот же механизм даёт эффект «работает у меня»: на вашей машине закэширован свежий адрес, а на другой — ещё старый, или наоборот.

Мораль простая: DNS — не мгновенный переключатель, а слоистая система кэшей с задержками. Планируя смену адресов, закладывайте эти задержки и помните про TTL на каждом уровне.

приложение свой кэш, в JVM 30 с система сначала файл hosts резолвер кэш на TTL записи авторитетный здесь TTL и задают

Один ответ DNS оседает сразу в нескольких кэшах, и у каждого свой срок: смотрите на крайний левый, кэш внутри процесса живёт по своим настройкам и про TTL записи не знает.

Где это применяется

DNS — первый шаг почти любого сетевого взаимодействия, поэтому он всплывает в отладке постоянно. Не открывается сайт, сервис не видит базу, интеграция «отвалилась после переезда» — прежде чем винить код, стоит проверить, правильно ли резолвится имя (dig/nslookup) и не закэширован ли где-то старый адрес. DNS лежит на прикладном слое стека — как это встроено в общую картину, см. в статье про модели OSI и TCP/IP.

Где спотыкаются начинающие:

  • Забывают про TTL при переезде. Меняют A-запись и ждут мгновенного эффекта, а старый адрес живёт в кэшах ещё час. Снижать TTL нужно заранее.
  • Не помнят про строчку в /etc/hosts. Локальное переопределение молча перебивает DNS и даёт «у меня работает, у других нет».
  • Списывают всё на DNS-кэш приложения. Приложения и клиенты держат свой кэш (например, JVM); после смены адреса сервис может стучаться по старому, пока его не перезапустить или не настроить TTL кэша.
  • Путают CNAME и A. CNAME указывает на имя, а не на адрес; нельзя вешать CNAME на корень домена и мешать его с другими записями.
  • Думают, что DNS отдаёт «сайт». DNS отдаёт только адрес. Дальше по этому адресу устанавливается соединение и идёт HTTP — это уже другой шаг.
Дополнительно: при первом чтении можно пропустить

Глубже: обнаружение сервисов без Kubernetes: round-robin, SRV и реестррасширенное

Сервис заказов должен найти сервис оплаты, и адрес нельзя зашить в конфигурацию: экземпляры оплаты появляются и исчезают, а их несколько. В Kubernetes это решает Service из раздела выше. Вне его те же задачи решают четырьмя способами, и у каждого своя связь с балансировкой.

DNS round-robin. У имени payments.internal несколько A-записей, по одной на экземпляр, и резолвер отдаёт их в меняющемся порядке. Просто и работает везде, но балансировка получается по клиентам, а не по запросам: клиент, взявший первый адрес, будет ходить по нему до истечения TTL, и мёртвый экземпляр из ответа никто не уберёт, пока запись не удалят руками или проверкой здоровья на стороне DNS. Отсюда короткий TTL (секунды) и клиент, который перечитывает ответ, а не кэширует адрес навсегда, о чём говорит раздел про кэш в приложениях.

SRV-записи. A-запись знает только адрес, а порт приходится договаривать заранее. Запись SRV хранит и порт, и приоритет, и вес: _payments._tcp.internal. SRV 10 60 8443 pay-1.internal. Так экземпляры на нестандартных портах и разной мощности описываются в самом DNS; это используют Consul и внутренние DNS в облаках, а вот обычные HTTP-клиенты SRV не понимают, и разрешать запись приходится самим.

Реестр сервисов. Consul, etcd, Eureka: каждый экземпляр при старте регистрирует себя с адресом и портом, регулярно подтверждает, что жив, и реестр отдаёт клиентам актуальный список. Балансировка при этом переезжает на клиента: он берёт список и сам выбирает экземпляр (round-robin, наименьшее число соединений), это называют клиентской балансировкой. Плюс в свежести списка и умных стратегиях, минус в том, что логика выбора живёт в каждом клиенте на каждом языке, а реестр становится ещё одной системой, от которой зависит всё.

Sidecar и сетка. Чтобы не тащить логику в клиента, рядом с каждым экземпляром ставят прокси-сосед (Envoy), клиент ходит в localhost, а сосед знает реестр, балансирует, повторяет и шифрует. Это service mesh, и в Kubernetes он стоит поверх Service, а вне его заменяет реестр с клиентской библиотекой. Плата та же, что у любого прокси: ещё один прыжок и ещё одна система.

Связь с балансировщиком из соседней статьи такая: обнаружение отвечает на вопрос «где экземпляры», балансировка на вопрос «кому из них отдать запрос». Балансировщик посередине скрывает обнаружение от клиента (клиент знает один адрес), реестр и sidecar переносят оба вопроса на сторону клиента. Для небольшой системы вне Kubernetes достаточно DNS с коротким TTL и балансировщика перед экземплярами; реестр появляется, когда экземпляров сотни и они живут минуты.

Коротко

  • DNS превращает имя в адрес через цепочку резолверов до авторитетного сервера; ответ кэшируется на TTL, поэтому переезд занимает время.
  • Записи: A и AAAA адрес, CNAME псевдоним, MX почта, TXT подтверждения; SRV хранит ещё порт и вес.
  • Приложения кэшируют адреса по-своему, и после переезда сервис ходит по старому адресу, пока кэш не сброшен.
  • Вне Kubernetes сервисы находят через DNS round-robin с коротким TTL, SRV, реестр с клиентской балансировкой или sidecar; обнаружение отвечает «где», балансировка «кому».
  • Локальное разрешение настраивают /etc/resolv.conf: домены поиска плюс ndots:5 превращают обращение к внешнему имени в четыре лишних запроса — лечится точкой в конце имени или ndots: 1.
  • Несколько A-записей дают примитивную балансировку без проверок работоспособности, а SRV добавляет порт, приоритет и вес — на них строят обнаружение сервисов без Kubernetes.
  • CNAME на корне домена запрещён, но есть три обхода: особая запись провайдера (ALIAS), перенаправление на поддомен или постоянный адрес балансировщика.
  • Обычный DNS не шифрован и не подписан: провайдер может подменить ответ, а шифрованный DNS в браузере даёт другие ответы, чем у системы — в разборе всегда указывают, кого спрашивали.

Что почитать дальше

DNS вернул адрес — дальше начинается собственно соединение. Логично разобрать IP-адреса, порты и NAT: как по полученному адресу пакет находит машину и конкретный сервис. Затем — прикладной слой поверх: HTTP и его защищённый вариант HTTPS и TLS, где, кстати, имя из DNS ещё раз проверяется в сертификате. А чтобы понять, как сервис остаётся доступным, когда часть узлов отваливается, посмотрите надёжность и отказоустойчивость — там DNS и кэши тоже играют роль.