Почему DNS в Kubernetes тормозит и что с этим делать

Первый раз — когда поднимают кластер и ставят CoreDNS, потому что без него ничего не работает.
Второй — когда что‑то начинает падать и в логах видно
i/o timeoutна резолве.
Между этими двумя моментами может пройти года три, и всё это время механизм работает, никого не беспокоит и настройки свои не показывает.
А настройки там интересные.
Они были выбраны под очень узкий сценарий: приложение в кластере ходит к соседнему сервису по короткому имени вроде payment-service, и хочется, чтобы это просто работало без всяких доменов и суффиксов. Задачу решили, короткие имена действительно работают из коробки.
Проблема в том, что современный сервис редко ограничивается соседями по кластеру.
Он ходит в платёжный шлюз, в объектное хранилище, в чужой API за курсами валют, в почтовый сервис.
И вот на этих вызовах умолчания разворачиваются к вам неудобной стороной — сначала лишним трафиком, а потом и задержками.
Что лежит в resolv.conf
Начнём с того, что видно.
kubectl exec -it deploy/api -- cat /etc/resolv.confnameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5Этот файл пишет kubelet при создании пода, приложение к нему отношения не имеет.
nameserverуказывает на ClusterIP сервиса kube‑dns, за которым стоят поды CoreDNS.searchперечисляет суффиксы, которые резолвер будет подставлять к неполным именам.А
ndotsзадаёт границу, по которой имя считается полным или неполным.
Логика такая:
Если точек в имени меньше, чем указано в
ndots, резолвер считает имя коротким и сначала пробует дописать к нему суффиксы из списка по очереди.Если точек столько же или больше, имя уходит в сеть как есть.
Пятёрка выбрана не случайно.
Полное имя сервиса в кластере выглядит как payment-service.billing.svc.cluster.local, в нём четыре точки, и чтобы такое имя тоже проходило через search‑список при обращении из другого неймспейса, порог подняли до пяти.
Решение аккуратное и для внутренних имён работает отлично: короткое payment-service находится с первой же попытки, потому что первый суффикс в списке как раз тот, что нужен.
Для всего остального оно работает ровно наоборот.
Во что обходится внешний вызов
Возьмём обычное обращение к внешнему API, скажем к api.stripe.com.
Две точки, до пяти далеко, значит имя неполное и его нужно достраивать. Резолвер честно идёт по списку:
api.stripe.com.default.svc.cluster.local → NXDOMAIN
api.stripe.com.svc.cluster.local → NXDOMAIN
api.stripe.com.cluster.local → NXDOMAIN
api.stripe.com → наконец-то ответЧетыре имени вместо одного. Три из них заведомо мусорные.
Теперь умножаем на два.
Начиная с версии 2.9 glibc спрашивает адреса A и AAAA параллельно, двумя отдельными запросами, и делает это независимо от того, включён у вас IPv6 или выключен.
Итого восемь UDP‑пакетов, чтобы узнать адрес одного внешнего хоста.
Представим, что он на каждый входящий запрос ходит в три внешних API, а HTTP‑клиент создаётся заново и соединения не переиспользует.
Это двадцать четыре DNS‑пакета на один запрос пользователя.
При тысяче запросов в секунду получается двадцать четыре тысячи пакетов, из которых восемнадцать тысяч гарантированно вернут NXDOMAIN.
Само по себе это ещё не далеко не проблема. CoreDNS отвечает быстро, кэш у него есть, миллисекунды на фоне сетевого вызова к чужому API теряются.
Неприятности начинаются, когда к этому объёму добавляется вторая деталь.
Откуда берутся пять секунд
Когда glibc отправляет A и AAAA одновременно, оба пакета уходят из одного сокета, с одинаковым исходным портом, и различаются только содержимым.
По дороге к CoreDNS они проходят через DNAT, потому что обращение идёт к ClusterIP, а доставить надо в конкретный под. Каждый пакет регистрируется в таблице conntrack.
Вставка в эту таблицу устроена с гонкой.
Два пакета, пришедшие практически одновременно и претендующие на один и тот же набор адресов и портов, могут столкнуться при регистрации, и тогда второй получает NF_DROP.
Просто исчезает.
Резолвер про это ничего не знает. Он отправил запрос и ждёт ответа.
Таймаут по умолчанию — пять секунд, после чего он повторяет попытку, и та обычно проходит нормально.
Здесь две части истории и складываются.
Гонка сама по себе — событие редкое, на десятки тысяч запросов. Но ndots:5 умножает количество попыток на восемь, и то, что должно было случаться раз в час, начинает происходить каждую минуту.
Есть ещё похожий по симптомам, но совершенно другой случай: CoreDNS просто не справляется.
Путать их дорого, потому что исправляются они разными вещами.
Что смотрим | Гонка в conntrack | Не хватает CoreDNS |
|---|---|---|
Тайминги неудачных запросов | кучкуются на 5,00 и 10,00 с | разбросаны произвольно |
Что видит приложение | i/o timeout | SERVFAIL |
Логи CoreDNS | чисто | отказы, всплески времени ответа |
| растёт | без изменений |
Добавление реплик CoreDNS от гонки не спасает вообще никак, и это первое, что обычно пробуют.
Самое дешёвое: точка в конце
Прежде чем трогать кластер, имеет смысл посмотреть на то, что можно исправить в конфигурации приложения.
Имя с точкой на конце считается абсолютным, и search‑список к нему не применяется:
api.stripe.com.Один запрос вместо четырёх.
Когда внешние адреса лежат в переменных окружения или в конфиге сервиса, правка занимает десять минут и не требует согласований с теми, кто отвечает за кластер.
Нюанс: не все библиотеки это переваривают.
Понизить ndots
Более общее решение — сказать резолверу, что пять точек это слишком высокий порог.
Делается это через dnsConfig в спецификации пода:
spec:
dnsPolicy: ClusterFirst
dnsConfig:
options:
- name: ndots
value: "2"
- name: single-request-reopenС ndots:2 имя api.stripe.com уходит в сеть сразу, одним запросом: две точки, порог не меньше двух.
Короткое payment-service с нулём точек по‑прежнему идёт через search‑список и находится с первой попытки.
Внутренние обращения не страдают.
Страдает одна форма — обращение к сервису в чужом неймспейсе по имени вида payment.billing.
Там одна точка, до порога не хватает. Резолвер снова пойдёт по списку, и первый суффикс default.svc.cluster.local выдаст NXDOMAIN.
Работать это будет, просто с лишними запросами, как раньше.
Короче, понижать до двух безопасно, если внутри кластера сервисы ходят друг к другу либо по коротким именам, либо по полным FQDN.
Ставить единицу разумно только там, где все внутренние адреса записаны полностью, в форме payment-service.billing.svc.cluster.local.
Вторая опция в том же примере, single-request-reopen, разводит A и AAAA по разным сокетам и убирает гонку.
Но у неё есть ограничение, из‑за которого половина попыток заканчивается ничем:
это опция glibc. Musl её не поддерживает и игнорирует молча.
Значит, на Alpine‑образах строчка не делает ничего.
Конфиг применился, опция в файле лежит, поведение не изменилось, пятисекундные задержки остались на месте.
NodeLocal DNSCache
Этот способ работает независимо от того, какая в образе libc, и вообще независимо от приложения.
На каждый узел ставится DaemonSet с кэширующим агентом, а поды получают в nameserver не ClusterIP сервиса, а локальный адрес:
# /var/lib/kubelet/config.yaml
clusterDNS:
- 169.254.20.10Адрес link‑local, трафик до него не покидает узел и не проходит через DNAT.
Нет DNAT, нет вставки в conntrack, нет гонки. Пятисекундные ступеньки исчезают целиком.
Дальше идут приятные побочные эффекты.
Агент держит соединение с CoreDNS по TCP, а записи conntrack для TCP закрываются вместе с соединением, вместо того чтобы висеть тридцать секунд, как у UDP.
Таблица перестаёт заполняться на пустом месте.
Плюс сам кэш: заметная доля повторяющихся запросов отдаётся локально и до CoreDNS вообще не доезжает, что снимает нагрузку с центральных подов.
Минус в том, что агент на узле станет единой точкой отказа для резолва: если он упал, DNS на этом узле не работает ни у кого.
Обновление демонсета даёт несколько секунд таймаутов на каждом узле по очереди.
И если у вас настроены сетевые политики, их придётся дополнить, потому что агенту нужен свой доступ к CoreDNS.
Autopath: починить на стороне сервера
А еще есть подход, при котором манифесты приложений не трогаются совсем.
Плагин autopath в CoreDNS определяет, из какого неймспейса пришёл запрос, сам проходит по search‑списку за клиента и возвращает готовый ответ на первое же обращение.
.:53 {
errors
health { lameduck 5s }
ready
autopath @kubernetes
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods verified
fallthrough in-addr.arpa ip6.arpa
}
forward . /etc/resolv.conf
cache 30
loop
reload
}Четыре запроса схлопываются в один.
Для крупного кластера, где сотни чартов катятся независимо друг от друга, это часто единственный реалистичный вариант: обойти все манифесты и добавить в каждый dnsConfig физически некому.
Цена у плагина в том, что во‑первых, ему нужен режим pods verified, при котором CoreDNS держит в памяти список подов и следит за его изменениями.
Во‑вторых: autopath определяет неймспейс по исходному адресу запроса.
Если перед CoreDNS стоит нодовый кэш, который не сохраняет IP пода‑клиента, плагин увидит адрес агента вместо адреса приложения и начнёт достраивать имена не тем суффиксом.
Пару проблем
Первая касается подов с hostNetwork: true.
Без явно указанной политики такой под получает dnsPolicy: Default, и слово «Default» здесь означает не «как принято в кластере», а «взять resolv.conf узла».
Кластерных суффиксов там нет, CoreDNS в nameserver тоже нет.
Дальше агент мониторинга или ingress‑контроллер пытается сходить в сервис по имени и получает NXDOMAIN.
В логе появляется что‑то про недоступность, и разбирательство уходит в сторону сети.
Фиксится одной строкой:
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNetА найти все такие места в кластере можно так:
kubectl get pods -A -o json | jq -r '
.items[]
| select(.spec.hostNetwork == true)
| select(.spec.dnsPolicy != "ClusterFirstWithHostNet")
| "\(.metadata.namespace)/\(.metadata.name) → \(.spec.dnsPolicy)"'Вторая проблема скорее про возможность.
Сервису кластерные имена могут быть не нужны в принципе — шлюз, выгрузчик, что‑то, что смотрит только наружу, search‑список можно выключить целиком, и тогда значение ndots перестаёт иметь значение:
spec:
dnsPolicy: None
dnsConfig:
nameservers:
- 10.96.0.10
searches: []
options:
- name: ndots
value: "1"dnsPolicy: None отменяет всё, что kubelet подставил бы сам.
Nameserver приходится указывать руками.
Обычному сервису так делать не надо: соседей по кластеру он после этого не найдёт.
метрика coredns_dns_requests_total и время ответа покажут это точнее любых рекомендаций.
Под конец
DNS в кластере устроен так, что о нём невозможно вспомнить вовремя.
Он не отдаёт ошибок, не занимает заметных ресурсов, не попадает в дашборды.
Пока запросы резолвятся, весь этот слой существует для команды примерно как электропроводка в стене: она есть, она работает, заглядывать туда незачем.
Заглянуть всё‑таки приходится, и обычно в неудачный момент.
Причём выясняется, что настройки в порядке — они просто выбраны под сценарий, в котором приложение общается с соседями по кластеру и почти не ходит наружу.
Такие приложения были нормой, когда ndots:5 попал в умолчания, и почти исчезли с тех пор, как каждый второй сервис дёргает чужие API.
Отсюда и общая логика.
Дешевле всего разбираться не с кластером, а со своими клиентами: общий HTTP‑клиент с живым пулом соединений убирает изрядную часть DNS‑трафика вообще без согласований.
Следом идёт ndots на самых разговорчивых сервисах — правка в один манифест, которая откатывается мгновенно.
И только если после этого остались ровные пятисекундные ступеньки, стоит поднимать нодовый кэш: он единственный лечит гонку в conntrack независимо от того, что за libc внутри образа.

DNS‑таймауты, лишние запросы и пятисекундные задержки могут возникать даже в исправно работающем кластере. Чтобы быстрее находить причины таких проблем и не тратить время на случайные изменения конфигурации, важно понимать, как устроено сетевое взаимодействие и чем диагностировать сбои.
Разобраться в этом на практике помогут бесплатные открытые уроки OTUS:
15 октября, 20:00. «Поднимаем кластер Kubernetes с помощью Terraform и Ansible». Записаться
5 ноября, 20:00. «Стек сетевых протоколов и с чем его едят. На примере TCP/IP». Записаться
17 ноября, 20:00. «Почему тормозит приложение: диагностика в Wireshark». Записаться
Ещё больше тем для развития технических навыков — в октябрьском дайджесте OTUS.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.