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)