Оптимизация DNS в Linux: ускорили резолвинг на 80% для 50 серверов

Проблема клиента: медленный DNS убивает производительность

Компания «КлаудСофт» — разработчик SaaS-платформы для автоматизации бизнес-процессов. Инфраструктура: 50 серверов Ubuntu 22.04 в двух дата-центрах, 12 микросервисов на Go и Python, трафик — 15 000 HTTP-запросов в секунду.

Клиент обратился к нам с проблемой: P99-латентность API выросла с 200 мс до 800 мс за последние два месяца без видимых изменений в коде. Внутренняя команда добавляла серверы, но это не помогало. Мы провели аудит и обнаружили, что 65% времени ответа уходит на DNS-резолвинг внутренних и внешних доменов.

Корень проблемы оказался в непонимании того, как именно работает DNS в Linux — это не «просто запрос к серверу», а многоуровневый процесс с множеством подводных камней. Подробнее о нашей экспертизе — на itfresh.ru.

Архитектура DNS-резолвинга в Linux: теория

Прежде чем оптимизировать, нужно понять, как приложение преобразует доменное имя в IP-адрес. Вопреки распространённому заблуждению, это не просто «отправить UDP-пакет на DNS-сервер». Цепочка вызовов:

Приложение (curl, Go-сервис, Python-скрипт)
    ↓
getaddrinfo()  — функция стандартной C-библиотеки
    ↓
NSS (Name Service Switch)  — определяет источники резолвинга
    ↓
libnss_dns.so / libnss_files.so  — загружаемые модули
    ↓
libresolv  — формирует DNS-пакеты
    ↓
DNS-сервер  — отвечает на запрос

Практически все современные Linux-приложения — от curl до systemd — используют функцию getaddrinfo() для преобразования доменных имён. Эта функция возвращает список структур addrinfo с IP-адресами, типами сокетов и протоколами.

NSS и nsswitch.conf: кто решает, где искать

NSS (Name Service Switch) — механизм, который определяет порядок и источники резолвинга. Конфигурация задаётся в /etc/nsswitch.conf:

# /etc/nsswitch.conf
hosts: files dns myhostname
passwd: files systemd
services: db files

Строка hosts: files dns myhostname означает:

  1. files — сначала проверяется /etc/hosts (модуль libnss_files.so)
  2. dns — затем DNS-сервер из /etc/resolv.conf (модуль libnss_dns.so)
  3. myhostname — резолвинг имени текущего хоста через systemd (libnss_myhostname.so)

Критический момент: если модуль dns не указан в строке hosts, настройки resolv.conf полностью игнорируются. У клиента на 3 серверах из 50 этот модуль отсутствовал из-за ошибки в Ansible-playbook при провижининге.

Диагностика: где именно теряется время

Мы начали с инструментальной диагностики DNS-резолвинга на проблемных серверах. Первый инструмент — strace для анализа системных вызовов при запросе curl:

# Трассировка DNS-резолвинга curl
$ strace -e trace=network,write curl -o /dev/null -s https://api.external-service.com

# Вывод (ключевые строки):
connect(7, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = -1 ENOENT
socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 7
connect(7, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("10.0.0.2")}, 16) = 0
sendmmsg(7, ...) = 2    # Отправляем A и AAAA запросы
poll([{fd=7, events=POLLIN}], 1, 5000) = 1
recvfrom(7, ..., 2048, 0, ...) = 56    # Получаем ответ
recvfrom(7, ..., 65536, 0, ...) = 114  # Дополнительные записи
connect(8, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("203.0.113.45")}, 16) = 0

Здесь видно полный процесс: попытка подключения к NSCD (кэширующий демон, отсутствует), создание UDP-сокета, отправка запросов, получение ответов. Задержка на этапе poll() показывала время ожидания ответа от DNS-сервера.

Ключевое открытие: dig и curl используют разные механизмы

Важнейший момент, который не понимала команда клиента: утилиты dig и nslookup используют функцию res_query() напрямую, минуя NSS. А curl, ping и большинство приложений используют getaddrinfo(), который проходит через NSS.

# dig использует res_query() — отправляет запрос напрямую DNS-серверу
$ dig api.internal.cloudsoft.local
;; Query time: 2 msec  # Быстро!

# curl использует getaddrinfo() → NSS → dns → resolv.conf
$ time curl -o /dev/null -s https://api.internal.cloudsoft.local
real    0m0.487s  # Медленно!

# Разница в 240 раз!

Команда клиента тестировала DNS через dig и получала ответы за 2 мс. Поэтому DNS не рассматривался как причина проблемы. Но реальные приложения шли другим путём, где накапливались задержки.

Различия glibc и musl: ловушка для Alpine-контейнеров

Часть сервисов «КлаудСофт» работала в Alpine-контейнерах, которые используют библиотеку musl вместо glibc. Поведение DNS-резолвинга кардинально отличается:

Характеристикаglibc (Ubuntu, Debian)musl (Alpine)
Использование NSSДа, через загружаемые модулиНет, прямой резолвинг
Чтение /etc/hostsЧерез libnss_files.soНапрямую
Поддержка options rotateДаНет
Поддержка search domainПолнаяОграниченная
КэшированиеМожет кэшироватьБез кэша
Внешние зависимостиlibresolv.soНет

Мы обнаружили, что Alpine-контейнеры с musl отправляли DNS-запросы последовательно (A, затем AAAA), не поддерживали options rotate для балансировки между DNS-серверами и не кэшировали результаты. Это удваивало количество DNS-запросов по сравнению с glibc-системами.

Решение для Alpine-контейнеров

Для контейнеров на Alpine мы добавили локальный DNS-кэш через dnsmasq в sidecar-контейнере и отключили IPv6 lookups, чтобы устранить ненужные AAAA-запросы:

# resolv.conf для Alpine-контейнеров
nameserver 127.0.0.1
options single-request-reopen ndots:2 timeout:1 attempts:2

# dnsmasq sidecar конфигурация
no-resolv
server=10.0.0.2
server=10.0.0.3
cache-size=10000
neg-ttl=60
min-cache-ttl=30
log-queries

Оптимизация resolv.conf: тонкая настройка

Конфигурация /etc/resolv.conf на серверах клиента была дефолтной — без каких-либо оптимизаций:

# До оптимизации — /etc/resolv.conf
nameserver 10.0.0.2
search cloudsoft.local

Мы оптимизировали каждый параметр:

# После оптимизации — /etc/resolv.conf
nameserver 10.0.0.2
nameserver 10.0.0.3
nameserver 8.8.8.8
search cloudsoft.local
options timeout:1 attempts:2 rotate ndots:2 single-request-reopen

Разбор каждого параметра

Детальное объяснение каждого параметра и его влияния на производительность:

  • nameserver — добавили второй внутренний DNS и публичный 8.8.8.8 как fallback. При недоступности первого сервера система автоматически переключается на следующий
  • timeout:1 — таймаут ожидания ответа сокращён с 5 секунд (по умолчанию) до 1 секунды. При наличии трёх серверов общий worst-case — 6 секунд вместо 30
  • attempts:2 — количество попыток к каждому серверу сокращено с 2 до 2 (уже оптимально)
  • rotate — round-robin между DNS-серверами вместо использования только первого. Распределяет нагрузку и повышает устойчивость
  • ndots:2 — количество точек в имени, после которого оно считается FQDN. По умолчанию 1, что вызывает лишние запросы с search-доменом для имён вида api.internal
  • single-request-reopen — переоткрывает сокет между A и AAAA запросами. Решает проблему с некоторыми файрволами, которые теряют второй запрос на одном сокете

Важный нюанс: на серверах с DHCP или NetworkManager файл resolv.conf может быть перезаписан при обновлении аренды. Мы зафиксировали настройки через systemd-resolved:

# /etc/systemd/resolved.conf
[Resolve]
DNS=10.0.0.2 10.0.0.3
FallbackDNS=8.8.8.8 1.1.1.1
Domains=cloudsoft.local
DNSSEC=no
Cache=yes
CacheFromLocalhost=yes

Локальный DNS-кэш: главное ускорение

Самый значительный прирост дало внедрение локального DNS-кэширования. Без кэша каждый HTTP-запрос к внешнему API требовал полный цикл DNS-резолвинга (2-50 мс). При 15 000 запросов в секунду это генерировало огромную нагрузку на DNS-серверы и добавляло задержку к каждому запросу.

Развёртывание systemd-resolved с кэшированием

На всех 50 серверах мы активировали systemd-resolved как локальный кэширующий резолвер:

# Включение systemd-resolved
systemctl enable --now systemd-resolved

# Создание символической ссылки
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

# Проверка статуса кэша
$ resolvectl statistics
Current Cache Size: 847
          Hits: 142856
        Misses: 3241
     Hit Rate: 97.8%

# Просмотр кэшированных записей
$ resolvectl query api.external-service.com
api.external-service.com: 203.0.113.45 -- link: eth0
                          (cached)

Для серверов, где systemd-resolved не подходил (Alpine-контейнеры, специфичные конфигурации), мы развернули unbound как локальный кэширующий резолвер:

# /etc/unbound/unbound.conf
server:
    interface: 127.0.0.1
    port: 53
    access-control: 127.0.0.0/8 allow

    # Оптимизация кэша
    cache-min-ttl: 30
    cache-max-ttl: 86400
    msg-cache-size: 64m
    rrset-cache-size: 128m
    key-cache-size: 32m

    # Предзагрузка популярных доменов
    prefetch: yes
    prefetch-key: yes

    # Параллельные запросы
    num-threads: 4
    outgoing-range: 8192
    num-queries-per-thread: 4096

    # Приватные зоны
    private-domain: "cloudsoft.local"
    domain-insecure: "cloudsoft.local"

forward-zone:
    name: "cloudsoft.local"
    forward-addr: 10.0.0.2
    forward-addr: 10.0.0.3

forward-zone:
    name: "."
    forward-addr: 8.8.8.8
    forward-addr: 1.1.1.1

Особенности DNS в Go и Python

При аудите мы обнаружили, что некоторые языки программирования имеют собственные DNS-резолверы, которые полностью игнорируют системные настройки.

Go по умолчанию использует встроенный Pure Go DNS-резолвер, который читает /etc/resolv.conf напрямую, минуя NSS и getaddrinfo(). Это означает, что настройки NSS, NSCD-кэш и /etc/nsswitch.conf не действуют:

# Принудительное использование CGO-резолвера в Go
# (задействует getaddrinfo() и системный кэш)
export GODEBUG=netdns=cgo

# Или в коде
import _ "unsafe" // для CGO
import "net"

func init() {
    // Принудительно используем cgo resolver
    net.DefaultResolver.PreferGo = false
}

Python модуль dns.resolver из dnspython также имеет собственный резолвер, минуя системный. Стандартный socket.getaddrinfo() использует системный резолвер:

# Python: системный резолвер (правильно)
import socket
result = socket.getaddrinfo('api.example.com', 443)

# Python: прямой DNS (минует кэш!)
import dns.resolver
answers = dns.resolver.resolve('api.example.com', 'A')

На сервисах «КлаудСофт» мы обнаружили, что 3 Go-сервиса использовали Pure Go резолвер и не попадали в системный кэш, генерируя тысячи лишних DNS-запросов.

Результаты оптимизации

После комплексной оптимизации DNS на всех 50 серверах мы провели замеры:

МетрикаДо оптимизацииПосле оптимизацииУлучшение
Среднее время DNS-резолвинга45 мс0.3 мс (кэш)-99%
P99 DNS-резолвинга350 мс12 мс-97%
P99 API-латентность800 мс160 мс-80%
DNS-запросов к серверам28 000/сек1 200/сек-96%
Cache hit rate0% (нет кэша)97.8%-

Основной выигрыш дало локальное кэширование (снижение DNS-запросов на 96%) и исправление конфигурации Go-сервисов (переход на CGO-резолвер с использованием системного кэша). Настройка resolv.conf обеспечила корректный failover и сократила worst-case с 30 до 6 секунд.

Проект был выполнен за 1 неделю командой из 2 инженеров ITFresh без даунтайма для сервисов клиента.

Часто задаваемые вопросы

dig и nslookup используют функцию res_query(), которая отправляет DNS-запрос напрямую серверу, минуя NSS и системные механизмы. Приложения же используют getaddrinfo(), который проходит через NSS, проверяет /etc/hosts, и может иметь другие задержки. Для корректной диагностики используйте strace на реальном приложении.
glibc использует NSS с загружаемыми модулями, поддерживает кэширование и все опции resolv.conf. musl (Alpine Linux) резолвит напрямую без NSS, не поддерживает options rotate и не кэширует результаты. Для Alpine-контейнеров рекомендуется sidecar DNS-кэш.
На серверах с systemd — systemd-resolved, он уже установлен и хорошо интегрирован. Для контейнеров или серверов без systemd — unbound с настройкой prefetch и оптимизированным размером кэша. dnsmasq подходит для простых сценариев.
Go по умолчанию использует собственный Pure Go DNS-резолвер, который читает resolv.conf напрямую, минуя getaddrinfo() и NSS. Чтобы задействовать системный кэш, установите переменную окружения GODEBUG=netdns=cgo или настройте net.DefaultResolver.PreferGo = false.
Используйте systemd-resolved с конфигурацией в /etc/systemd/resolved.conf — он создаёт управляемый resolv.conf в /run/systemd/resolve/. Альтернатива: установите атрибут immutable через chattr +i /etc/resolv.conf, но это может помешать обновлению при смене инфраструктуры.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#dns linux#resolv.conf настройка#dns оптимизация#systemd-resolved#nsswitch.conf#dns кэширование#getaddrinfo#dns troubleshooting
Комментарии 0

Оставить комментарий

загрузка...