Команда JavaScript for Devs подготовила перевод статьи о том, как кэширование DNS в NodeJS помогает ускорить работу приложений. На примере инфраструктуры крупного онлайн-сервиса автор показывает, как незаметные на первый взгляд DNS-запросы могут превратиться в узкое место и как простое решение на уровне кода способно повысить стабильность и отклик системы.
Работая над инфраструктурой Arte.tv, мы обнаружили, что DNS-запросы могут быть скрытым узким местом, замедляющим критически важный слой. Хорошая новость в том, что это легко исправить. Об этом и пойдёт речь в этой статье.
Предыстория
Последние несколько недель мы наблюдали всплески ошибок 504 Gateway Timeout
, которые исходили от нашего backend-for-frontend (BFF). Этот ключевой компонент инфраструктуры Arte.tv называется EMAC. Он выступает в роли прокси для шести сервисов и может делать десятки API-запросов на один вызов с фронтенда.

Хотя пока эти всплески не оказывают серьёзного влияния, EMAC является единой точкой отказа, поэтому игнорировать проблему мы не могли. Мы обратились к инструментам мониторинга, чтобы разобраться в причине и устранить её.
Благодаря NewRelic (инструменту, который мы используем для мониторинга и отладки приложений), мы выяснили, что DNS-разрешение может значительно замедлять время отклика и в некоторых случаях превращается в системное узкое место.
Вот пример особенно медленного запроса, вызванного DNS-разрешением:

А вот график, сравнивающий количество DNS-запросов от EMAC и других сервисов:

В часы пиковых нагрузок EMAC может выполнять до 3000 DNS-запросов в минуту. Этого оказалось достаточно, чтобы мы внимательно посмотрели, как работает DNS в NodeJS — и что можно улучшить.
DNS в двух словах
Система доменных имён (DNS) позволяет нам вводить удобочитаемые доменные имена (например, https://lapoulequimue.fr/
) и получать в ответ IP-адреса (например, 46.105.57.169
).
Обычно DNS-запросы проходят через сервер DNS вашего провайдера, который дальше обращается к другим серверам в иерархии, пока не найдёт правильный IP.

DNS-серверы возвращают записи с именем, временем жизни (TTL, time to live), типом и данными. Для нас важнее всего записи типа A:
Доменное имя |
Время жизни |
Тип записи |
IPv4-адрес |
---|---|---|---|
arte.tv. |
300 |
A |
212.95.74.37 |
Эта запись говорит о том, что домен arte.tv
указывает на 212.95.74.37
, а любой клиент должен кэшировать её 300 секунд.
Когда ваше приложение делает HTTP-запрос, разрешение имени происходит «за кулисами» с использованием встроенных системных функций.
Проверить это можно с помощью команды dig
в Linux:
$ dig arte.tv
; <<>> DiG 9.16.1-Ubuntu <<>> arte.tv
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 29724
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;arte.tv. IN A
;; ANSWER SECTION:
arte.tv. 8739 IN A 212.95.74.37
;; Query time: 9 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Fri Apr 04 10:49:32 CEST 2025
;; MSG SIZE rcvd: 52
Чтобы получить более короткий вывод, используйте параметр +short
:
dig +short arte.tv
212.95.74.37
Теперь можно впечатлить друзей, притворившись, будто вы управляете Матрицей.
По умолчанию dig
использует DNS-сервер, указанный в /etc/resolv.conf
, но можно выбрать другой:
dig arte.tv @8.8.8.8 # Google Public DNS
Также можно переопределить разрешение имён локально через /etc/hosts
:
127.0.0.1 localhost
Совет: вы можете поднять собственный DNS-сервер с помощью Unbound, чтобы повысить приватность и ускорить работу интернет соединения. Именно так работает Pi-hole, блокируя рекламу и трекеры.
DNS в NodeJS
Популярные библиотеки для получения данных в NodeJS — такие как Axios или node-fetch — опираются на модули HTTP и HTTPS. В них доменные имена разрешаются через функцию NodeJS dns.lookup
, которая в свою очередь использует системный DNS-резолвер.
const dns = require('node:dns');
const options = {
family: 6,
hints: dns.ADDRCONFIG | dns.V4MAPPED,
all: true,
};
dns.lookup('example.org', options, (err, addresses) =>
console.log('addresses: %j', addresses));
// addresses: [{"address":"2606:2800:21f:cb07:6820:80da:af6b:8b2c","family":6}]
Проблема в том, что dns.lookup
работает синхронно (хотя и принимает колбэк) и блокирует поток в пуле потоков libuv. По умолчанию там всего четыре потока, поэтому несколько медленных DNS-запросов могут негативно повлиять на другие части приложения.
В NodeJS есть альтернатива — dns.resolve
, которая получает DNS-записи, не блокируя пул потоков. Однако большинство библиотек по умолчанию её не используют, и я, на момент написания статьи, не знаю почему.
Теперь, когда мы понимаем, что именно может просаживать производительность EMAC, посмотрим на возможное решение.
Кэширование DNS
dns.lookup
полагается на системный резолвер, который уже кэширует DNS-записи на ограниченное время (TTL). Это означает, что если вы делаете несколько запросов к одному и тому же домену в пределах TTL, ОС вернёт результат из кэша без нового запроса.
Однако EMAC обрабатывает так много DNS-запросов, что пул потоков libuv может быть перегружен, замедляя весь процесс NodeJS. В итоге ответы API от EMAC становятся медленнее, и это может нарастать как ком снежный. Прекрасный «эффект снежного кома».
Добавить ещё один уровень кэширования на уровне приложения — логичный шаг. Это можно сделать быстро, потому что не требуется изменений инфраструктуры, и может дать отличный эффект. К тому же многие сервисы, к которым обращается EMAC, живут под одним и тем же доменом, так что можно ожидать высокий процент попаданий в кэш.
В NodeJS существует несколько библиотек для кэширования DNS:
Мы выбрали cacheable-lookup
. Она учитывает TTL, работает с высокоуровневыми HTTP-библиотеками (например, с Axios) и широко используется.
Важно: если у ваших DNS-записей слишком длинный TTL, а IP-адреса серверов меняются, могут возникнуть серьёзные проблемы. Убедитесь, что записи настроены корректно.
Использование cacheable-lookup для кэширования DNS-запросов
Работать с cacheable-lookup
просто. Сначала установите её:
yarn add cacheable-lookup
Затем интегрируйте с HTTP- и HTTPS-агентами. Мы добавили feature flag, чтобы можно было легко включать и отключать кэширование DNS:
import CacheableLookup from 'cacheable-lookup';
if (config.getProperties().cacheDNSLookups) {
const cachedDns = new CacheableLookup();
cachedDns.install(http.globalAgent);
cachedDns.install(https.globalAgent);
}
Этот код переопределяет стандартный dns.lookup
на версию с кэшем. Если запись уже есть в кэше — она будет использована. Если нет — выполняется запрос, и результат сохраняется в кэше на время, указанное в TTL. Если ничего не найдено, вызывается оригинальный dns.lookup
.
Совет: кэширование DNS на уровне приложения — не единственный вариант. Можно зашить IP-адреса в код, использовать виртуальные IP, править
/etc/hosts
, запускать общий DNS-кэш вне приложения и многое другое.
Неожиданный результат
Сразу после релиза метрики производительности начали выглядеть просто потрясающе:

Возможно, даже слишком потрясающе. Количество DNS-запросов упало до нуля. Но даже при длинных TTL каждый pod EMAC (а их у нас много) должен был сделать хотя бы несколько запросов, прежде чем кэширование вступит в силу.
Причина оказалась в том, что метрика, которую мы использовали (вызовы dns.lookup
), больше не срабатывала. Наш кастомный резолвер полностью обходил её. Значит, теперь нам нужны более точные метрики, чтобы измерять реальную DNS-активность.
На данный момент мы не можем отслеживать работу заменённой функции dns.lookup
из пакета cacheable-lookup
через NewRelic. Поэтому ждём, пока команда инфраструктуры не предоставит метрики на уровне самого DNS-резолвера, которые дадут нам больше информации о влиянии внесённых изменений.
Русскоязычное JavaScript сообщество

Друзья! Эту статью перевела команда «JavaScript for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Frontend. Подписывайтесь, чтобы быть в курсе и ничего не упустить!
Заключение
Для backend-for-frontend вроде EMAC, который каждую минуту делает тысячи запросов к одним и тем же доменам, кэширование DNS — очевидное решение.
Оно не устранит все ошибки таймаута, но точно поможет сделать EMAC более устойчивым и отзывчивым.
Как мы увидели, реализовать это в NodeJS просто. Главное помнить: кэширование на уровне приложения — лишь один из возможных вариантов. Если нужна более широкая поддержка, можно рассмотреть кэширование на уровне инфраструктуры. В любом случае важно обеспечить качественную наблюдаемость как на уровне приложения, так и на уровне инфраструктуры, чтобы можно было точно выявлять, понимать и устранять проблемы с задержками и сбоями.
Комментарии (2)
SuperOleg39ru
09.09.2025 15:44В первую очередь рекомендую добавить параметр keepAlive для http.Agent и пробросить его в ваш http клиент.
Это радикально уменьшить количество новых TCP соединений и резолвов DNS.
Кэш DNS может выстрелить в ногу в динамических средах (читай k8s), где IP сервисов часто меняются. Обработать такие ошибки и сделать повторный резолв возможности насколько я помню нет.
Sirion
В голос