Классическая ситуация из практики любого администратора: на ноутбуке инженера домен api.example.io резолвится мгновенно, приложение работает, а на продакшен-сервере тот же самый хост отвечает "Temporary failure in name resolution" или, что коварнее, выдаёт старый IP-адрес и молча стучится не туда. Паниковать рано, гадать поздно. DNS - это иерархическая, строго документированная система, и почти любая загадка раскладывается на атомы, если задать ей правильные вопросы правильным инструментом. Главный инструмент здесь - утилита dig из пакета bind-utils или dnsutils, а главный приём - сравнение ответов разных DNS-серверов и пошаговый проход по цепочке делегирования через dig +trace. В этой статье разобран реальный сценарий диагностики: от проверки локального резолвера до охоты за устаревшим кэшем на авторитативном уровне, с выводами команд, разбором каждой строки и планом действий, который работает в продакшене на Linux.
Почему локальный результат и серверный результат расходятся у одного и того же домена
Прежде чем запускать команды, полезно понимать, сколько рук касается каждого DNS-ответа. Запрос от процесса на сервере проходит несколько слоёв: библиотечный резолвер glibc читает /etc/nsswitch.conf и /etc/resolv.conf, дальше запрос уходит либо напрямую на внешний рекурсор, либо в локальный кэширующий стаб systemd-resolved на 127.0.0.53, либо в nscd, который молча держит собственный кэш. Каждый слой способен подменить, закэшировать или потерять ответ. На домашней машине цепочка другая, часто роль рекурсора выполняет роутер, который в свою очередь форвардит запросы провайдеру. Поэтому фраза "у меня работает, а на сервере нет" на самом деле означает "две разные цепочки резолва дали разные ответы", и работа диагноста сводится к поиску того слоя, где ответы расходятся.
Вторая большая группа причин - время жизни записей. Параметр TTL у записи A, скажем 300 секунд, приказывает каждому кэширующему резолверу помнить ответ пять минут. Если адрес сменили десять минут назад, один резолвер уже запросил свежие данные, а другой ещё четыре минуты будет отдавать старьё с чистой совестью. Третья группа - split-horizon DNS, когда внутренние резолверы компании осознанно отвечают на одни имена иначе, чем публичные 8.8.8.8 или 1.1.1.1, и различие это не баг, а архитектура, о которой кто-то забыл рассказать коллегам. Наконец, бывают просто ошибки: умерший форвардер в resolv.conf, зависший nscd, сломанная зона, отдающая NXDOMAIN. Все эти сценарии ниже разобраны на живых командах.
Проверка локальной цепочки резолва через nsswitch.conf и resolv.conf
Начинать стоит не с dig, а с того, что реально использует приложение. Диагностическая ловушка номер один состоит в том, что dig ходит мимо большинства локальных механизмов: он читает только список серверов из /etc/resolv.conf и общается с ними напрямую, а библиотечный вызов getaddrinfo, которым пользуются curl, ping и почти все фоновые службы, идёт через подсистему NSS. Смотрим, что задумано:
cat /etc/nsswitch.conf | grep hosts
# hosts: files mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] dns myhostname
Эта строка читается слева направо: сначала /etc/hosts (files), затем multicast DNS с обязательным выходом при NOTFOUND, затем плагин resolve, то есть systemd-resolved, и лишь в конце классический DNS. Если где-то между files и dns застрял устаревший или непредвиденный плагин, приложение получит ответ, которого dig никогда не увидит. Проверка через getent показывает именно тот результат, который видит приложение:
getent hosts api.example.io
# 10.24.6.18 api.example.io
А теперь сравним с честным сетевым запросом:
dig +short api.example.io A
# 203.0.113.44
Расхождение 10.24.6.18 против 203.0.113.44 означает, что между приложением и сетью сидит локальный слой: либо запись в /etc/hosts (проверьте grep example.io /etc/hosts), либо systemd-resolved с собственным кэшем и отдельным представлением о мире, либо nscd. Дальше смотрим, куда фактически отправляются запросы:
cat /etc/resolv.conf
# nameserver 127.0.0.53
# options edns0 trust-ad
# search corp.example.local
Адрес 127.0.0.53 - это стаб systemd-resolved. Весь трафик идёт в него, а он уже дальше решает, спрашивать ли upstream, отдать из кэша или применить правила split-DNS из resolvectl. Команда resolvectl status покажет, какие DNS-серверы назначены каждому интерфейсу и есть ли доменные маршруты вида "~corp.example.local", которые направляют часть имён на внутренние серверы. Именно такие маршруты нередко объясняют феномен "на сервере резолвится внутренний адрес, а извне внешний".
Осторожность требует nscd: его кэш живёт отдельно от всех перечисленных механизмов. Когда приложение получает старый адрес даже после исправления зоны, а getent упрямо повторяет неправильный ответ, полезно выполнить "nscd -i hosts" для инвалидации кэша или "systemctl restart nscd". На системах, где nscd не нужен осознанно, его кэш - частый источник мистики, и многие администраторы после пары таких ночей просто отключают кэширование hosts в /etc/nscd.conf, задав "enable-cache hosts no".
Сравнение ответов публичного резолвера и локального через dig с уточнённым сервером
Рабочая лошадка диагностики - явное указание сервера через символ @. Команда "dig @8.8.8.8 api.example.io A" обходит всех локальных посредников, resolv.conf и systemd-resolved, и задаёт вопрос напрямую рекурсору Google. Сравним оба мира:
dig @8.8.8.8 api.example.io A +noall +answer
# api.example.io. 287 IN A 203.0.113.44
dig api.example.io A +noall +answer
# api.example.io. 512 IN A 10.24.6.18
Опция +noall убирает весь служебный мусор, +answer оставляет только секцию ответа. Число 287 и 512 - это не исходный TTL записи, а остаток времени жизни в кэше каждого резолвера. Это тонкий и полезный момент: если исходный TTL записи 300, а 8.8.8.8 показывает 287, значит, рекурсор закэшировал ответ 13 секунд назад. Повторный запрос через минуту покажет 227, и по скорости убывания счётчика можно прикинуть, когда кэш истечёт и когда есть шанс увидеть свежие данные без вмешательства.
Полный вывод dig без косметики читается так:
dig @8.8.8.8 api.example.io A
# ;; Query time: 14 msec
# ;; SERVER: 8.8.8.8#53(8.8.8.8)
# ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
Флаги рассказывают историю запроса: qr - это ответ, rd - клиент просил рекурсию, ra - сервер рекурсию поддерживает, ad - данные прошли валидацию DNSSEC. Строка SERVER подтверждает, с кем именно говорили; если там неожиданно окажется 127.0.0.53, значит, забыли указать @ или локальный резолвер перехватил запрос. Query time на регулярно опрашиваемом домене должен укладываться в единицы-десятки миллисекунд; внезапные 3000 msec намекают на медленный форвардер или таймауты по пути.
Не менее информативна вторая проверка - запрос SOA или NS у зоны, чтобы убедиться, что оба резолвера видят одну и ту же редакцию зоны. У SOA есть серийный номер, и он честен:
dig @8.8.8.8 example.io SOA +short
# ns1.example-dns.com. hostmaster.example.io. 2026100703 7200 3600 1209600 300
dig @ns1.example-dns.com example.io SOA +short
# ns1.example-dns.com. hostmaster.example.io. 2026100704 7200 3600 1209600 300
Серийники 2026100703 у кэширующего и 2026100704 у авторитативного сервера означают, что зону уже обновили, а рекурсор ещё держит предыдущую редакцию. Проще всего зафиксировать истину запросом напрямую к авторитативному серверу: он отвечает из файла зоны, а флаг +norecurse гарантирует, что ответ не будет получен через рекурсию. Если @8.8.8.8 и авторитативный расходятся, виноваты не серверы администратора, а кэш чужого рекурсора, и остаётся ждать истечения TTL либо временно переключить клиентов на резолвер, где кэша нет.
Пошаговый проход по цепочке делегирования с dig +trace
Когда домен не резолвится вообще, а природа ошибки неясна, dig +trace превращает догадки в протокол. Утилита отключает рекурсию и проходит путь от корневых серверов до авторитативных так, как это делал бы встроенный рекурсор, печатая каждый шаг:
dig +trace api.example.io A
# ; <<>> DiG 9.18.24 <<>> +trace api.example.io A
# . 518400 IN NS a.root-servers.net.
# . 518400 IN NS b.root-servers.net.
# ;; Received 1097 bytes from 127.0.0.53#53 in 2 ms
#
# io. 172800 IN NS a0.nic.io.
# io. 172800 IN NS b0.nic.io.
# ;; Received 731 bytes from 192.5.5.241#53(f.root-servers.net) in 41 ms
#
# example.io. 86400 IN NS ns1.example-dns.com.
# example.io. 86400 IN NS ns2.example-dns.com.
# ;; Received 96 bytes from 163.121.176.40#53(a0.nic.io) in 38 ms
#
# api.example.io. 300 IN A 203.0.113.44
# ;; Received 64 bytes from 185.22.44.11#53(ns1.example-dns.com) in 22 ms
Читаем сверху вниз. Первая секция - корневая зона: локальный резолвер 127.0.0.53 отдал список корневых серверов "a." через "m.", это фундамент всей иерархии. Вторая секция - ответ корневого сервера f.root-servers.net: он не знает про api.example.io, но знает, кто отвечает за зону io., и возвращает referral - список серверов a0.nic.io и соседей. Число 172800 - TTL этого указания, двое суток. Третья секция - ответ сервера зоны io.: вот он уже делегирует example.io конкретным ns1 и ns2.example-dns.com. Финальная секция - авторитативный ответ с записью A и TTL 300.
Где обрывается цепочка, там и проблема. Если после запроса к ns1.example-dns.com вместо ответа видно "connection timed out; no servers could be reached" - авторитативный сервер недоступен, и приложение честно получает SERVFAIL. Если авторитативный отвечает "NXDOMAIN", а запись только что добавили - вероятны две вещи: либо запись создана не в той зоне (частая ошибка - создать запись в example.com вместо example.io), либо ответ NXDOMAIN уже закэширован. Отрицательные ответы кэшируются тоже, на время из поля minimum в SOA зоны, в нашем случае 300 секунд, и новая запись может невидимо существовать до пяти минут после создания. Другой типовой штрих: если +trace показывает корректный путь, а обычный dig через резолвер возвращает SERVFAIL, проблема между клиентом и рекурсором - зависший форвардер, сломанная валидация DNSSEC у промежуточного сервера или EDNS-несовместимость.
Статусы ответа стоит знать наизусть: NOERROR означает, что домен существует (даже если ANSWER: 0 - например, запросили AAAA, а есть только A); NXDOMAIN - имя не существует в принципе; SERVFAIL - сервер сам не смог получить ответ выше по цепочке; REFUSED - сервер отказался обслуживать запрос, типично для авторитативных при попытке рекурсии. Проверить код помогает "dig +comments" или "+noall +comments +answer", где статус печатается строкой ";; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 44731".
Типичные причины расхождения ответов и порядок действий администратора
Собрав воедино опыт подобных расследований, удобно держать фиксированный порядок проверок, который закрывает девять из десяти случаев за десять минут:
- Сравнить "getent hosts имя" с "dig +short имя A", чтобы отделить локальную цепочку NSS от сетевого DNS;
- Проверить содержимое /etc/hosts и статус nscd, инвалидировав его кэш командой "nscd -i hosts";
- Посмотреть /etc/resolv.conf и "resolvectl status", убедившись, какие серверы и доменные маршруты реально активны;
- Спросить публичный рекурсор напрямую: dig @8.8.8.8 имя A, и сравнить с ответом локального резолвера по адресу и по остаточному TTL;
- Запросить авторитативный сервер зоны из вывода +trace с флагом +norecurse и сверить серийный номер SOA;
- Прогнать dig +trace полностью, найдя шаг, на котором цепочка делегирования ломается или возвращает NXDOMAIN;
- При подозрении на split-horizon уточнить у сетевой команды, не настроен ли для домена отдельный ответ на внутренних резолверах.
Дальше по списку наиболее частых виновников. Первый - устаревший кэш при смене IP: решается ожиданием TTL, а чтобы не ждать в следующий раз, TTL на боевой записи за сутки до миграции снижают до 60-120 секунд, а после возвращают 300-3600. Второй - split-horizon DNS, настроенный через views в BIND9 или через доменные маршруты в systemd-resolved: здесь важно не чинить то, что не сломано, а зафиксировать в документации, какой ответ какой сети положен. Третий - мёртвый первый nameserver в resolv.conf: glibc ждёт таймаут около 5 секунд (параметр timeout по умолчанию, при two попытках attempts:2 это до 10 секунд на каждый запрос), и симптом выглядит как "резолвится, но медленно". Четвёртый - отрицательный кэш NXDOMAIN: запись добавили, а клиенты до пяти минут уверены, что её нет, потому что minimum в SOA равен 300. Пятый - расходящиеся NS-записи у регистратора и в самой зоне, когда делегирование указывает на старые серверы, а зону правят на новых; +trace такое показывает безжалостно: referral от родителя ведёт на серверы, где записи просто нет.
Профилактика сводится к нескольким твёрдым привычкам. Любая миграция адресов начинается со снижения TTL за 24 часа и быстрой проверки "dig @авторитативный" после правки. На серверах держат единообразный resolv.conf через configuration management, чтобы у двух машин одной фермы не оказалось разных резолверов. Мониторинг DNS делают не по факту "резолвится ли домен изнутри", а сравнением ответа публичного рекурсора с эталоном - расхождение ловится до того, как его заметит пользователь. И наконец, знание своего ландшафта: сколько слоёв между приложением и сетью, кто кэширует, какие TTL у ключевых записей. Тогда вопрос "почему локально работает, а на сервере нет" перестаёт быть загадкой и становится короткой процедурой из семи команд, после которой виновник найден, кэш сброшен, а внятное объяснение с цифрами уже лежит в отчёте об инциденте.