Node.js Internals

DNS Module: Резолвинг и кеширование

Деплой приложения в Kubernetes. Всё работает локально, но в проде начинается ад: timeout'ы, CoreDNS падает под нагрузкой, логи полны ошибок `ENOTFOUND`. Виновник - DNS. Node.js делает в 10 раз больше DNS-запросов, чем кажется, и совершенно не кеширует результаты.

  • **Shopify** потратили недели на отладку медленных запросов, пока не обнаружили, что каждый HTTP-запрос генерирует 5 DNS-запросов из-за `ndots:5` в K8s
  • **Netflix** написали свой DNS-кеш для Node.js микросервисов, чтобы снизить нагрузку на CoreDNS с 100k qps до 5k qps
  • **Stripe** используют FQDN с точкой (`api.stripe.com.`) во всех HTTP-клиентах, чтобы избежать лишних DNS-запросов

Как работает DNS

DNS (Domain Name System) - это распределённая база данных, которая превращает человекочитаемые домены (`api.example.com`) в IP-адреса (`192.168.1.100`). На каждом HTTP-запросе или открытии сокета под капотом происходит DNS-резолвинг.

**Рекурсивный резолвинг** - это процесс, когда DNS-сервер опрашивает цепочку серверов (root → TLD → authoritative), пока не найдёт IP-адрес. Результаты кешируются с учётом **TTL** (Time To Live) - времени, в течение которого запись считается актуальной.

**Важно:** В Node.js нет встроенного DNS-кеша на уровне приложения! Каждый запрос обращается либо к системному резолверу (который кеширует), либо напрямую к DNS-серверу.

Что происходит при рекурсивном DNS-резолвинге?

dns.lookup vs dns.resolve

В Node.js есть **два принципиально разных способа** резолвинга DNS: `dns.lookup()` (использует системный резолвер через `getaddrinfo`) и `dns.resolve*()`/`dns.Resolver` (использует c-ares - асинхронную DNS-библиотеку).

**dns.lookup()** вызывает POSIX-функцию `getaddrinfo()`, которая блокируется и выполняется в thread pool (по умолчанию 4 потока). Она учитывает `/etc/hosts`, NSS (Name Service Switch), системные настройки DNS. **dns.resolve*()** работает напрямую с DNS-серверами через UDP/TCP, полностью асинхронно, без блокировок. Но игнорирует `/etc/hosts` и системные резолверы.

**Ключевая разница:** `dns.lookup()` блокирует один из thread pool потоков, `dns.resolve()` - нет. В высоконагруженных приложениях это критично!

Почему dns.lookup() может стать bottleneck в высоконагруженном приложении?

DNS кеширование: проблема Node.js

**Шокирующий факт:** Node.js **не кеширует DNS на уровне приложения**. Каждый вызов `dns.resolve()` идёт в сеть, даже если тот же домен резолвили секунду назад. `dns.lookup()` зависит от системного кеша (например, systemd-resolved на Linux кеширует на 30 секунд), но это вне контроля приложения.

В production-приложениях это приводит к: - Тысячам лишних DNS-запросов в секунду - DNS rate limiting от провайдера (например, CoreDNS в K8s ограничивает ~1000 qps) - Задержкам при DNS-флуде (все запросы идут последовательно) - Игнорированию TTL (можете запросить домен с TTL=3600, но Node.js снова пойдёт в сеть)

**Решение:** Используйте библиотеку `cacheable-lookup` - она добавляет in-memory кеш с соблюдением TTL и работает как drop-in replacement для dns.lookup().

Почему Node.js не имеет встроенного DNS-кеша?

Thread pool блокировка: dns.lookup

**Самая частая ошибка:** использовать `dns.lookup()` в высоконагруженных приложениях. Эта функция вызывает POSIX `getaddrinfo()`, которая **блокирует один из потоков libuv thread pool**. По умолчанию их всего **4**.

Ситуация: приложение получает 1000 запросов в секунду, каждый требует DNS-резолвинга. При использовании `dns.lookup()` только 4 резолвинга будут идти параллельно. Остальные 996 ждут в очереди!

**Fix:** Используйте `dns.resolve*()` (неблокирующий) или увеличьте thread pool через `UV_THREADPOOL_SIZE=128`. Но лучше вообще избегать блокирующих операций.

Что произойдёт, если 100 одновременных запросов вызовут dns.lookup() при дефолтном thread pool?

Production: Kubernetes и CoreDNS

В production-окружении DNS становится критической точкой отказа. Особенно в **Kubernetes**, где CoreDNS часто становится bottleneck из-за неправильной конфигурации `ndots` и `search domains`.

**Проблема ndots:** По умолчанию в K8s `ndots:5` - это означает, что домен `api.example.com` (2 точки) будет искаться в **search domains** перед тем, как попробовать абсолютное имя: ``` api.example.com.default.svc.cluster.local (NXDOMAIN) api.example.com.svc.cluster.local (NXDOMAIN) api.example.com.cluster.local (NXDOMAIN) api.example.com. (SUCCESS!) ``` Итого **4 DNS-запроса** вместо одного! Умножьте на 1000 rps - и CoreDNS падает.

**Fix:** Используйте FQDN с точкой в конце (`api.example.com.`) или настройте `dnsPolicy: Default` в Pod spec, чтобы использовать DNS хоста, а не кластерный.

Node.js автоматически кеширует DNS на TTL секунд, как браузеры

Node.js НЕ кеширует DNS на уровне приложения. Каждый запрос идёт либо в системный резолвер (который может кешировать), либо напрямую в сеть

Это осознанное решение: Node.js делегирует кеширование ОС. Но в production (особенно в K8s) это приводит к тысячам лишних запросов. Решение - библиотека cacheable-lookup или увеличение системного TTL

Почему в Kubernetes домен api.example.com может генерировать 4 DNS-запроса?

Ключевые идеи

  • **Node.js НЕ кеширует DNS** - каждый запрос идёт в сеть или системный резолвер. Используйте `cacheable-lookup` для production
  • **dns.lookup() блокирует thread pool** (4 потока по умолчанию). Для высоконагруженных приложений используйте `dns.resolve*()` или увеличьте `UV_THREADPOOL_SIZE`
  • **В Kubernetes ndots:5 генерирует 4+ DNS-запроса** на каждый домен. Fix: FQDN с точкой (`example.com.`) или `dnsPolicy: Default`
  • **dns.lookup() vs dns.resolve()**: первый использует `getaddrinfo()` (блокирующий, учитывает /etc/hosts), второй - c-ares (асинхронный, прямой UDP к DNS)
  • **CoreDNS в K8s - частая точка отказа**. Кеширование DNS на уровне приложения снижает нагрузку в 10-100 раз

Связанные темы

DNS тесно связан с другими аспектами Node.js и инфраструктуры:

  • HTTP Clients (fetch, axios) — Все HTTP-клиенты используют DNS под капотом. Без кеширования каждый запрос может тормозить на DNS-резолвинге
  • Libuv и Event Loop — dns.lookup() использует thread pool из libuv, что влияет на производительность всех блокирующих операций
  • Kubernetes Networking — CoreDNS, ndots, search domains и dnsPolicy - критичны для DNS в K8s. Неправильная конфигурация убивает производительность

Вопросы для размышления

  • Как проверить, сколько DNS-запросов генерирует приложение? (Hint: tcpdump, DNS query logging в CoreDNS)
  • Почему браузеры не страдают от проблемы отсутствия DNS-кеша, а Node.js страдает?
  • В каких случаях dns.lookup() предпочтительнее dns.resolve()? (Hint: когда нужно учитывать /etc/hosts или NSS)

Связанные уроки

  • net-21-http-basics
DNS Module: Резолвинг и кеширование

0

1

Войти