Кэширование DNS в NodeJS
- среда, 10 сентября 2025 г. в 00:00:05
Команда 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) позволяет нам вводить удобочитаемые доменные имена (например, 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, блокируя рекламу и трекеры.
Популярные библиотеки для получения данных в 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.lookup
полагается на системный резолвер, который уже кэширует DNS-записи на ограниченное время (TTL). Это означает, что если вы делаете несколько запросов к одному и тому же домену в пределах TTL, ОС вернёт результат из кэша без нового запроса.
Однако EMAC обрабатывает так много DNS-запросов, что пул потоков libuv может быть перегружен, замедляя весь процесс NodeJS. В итоге ответы API от EMAC становятся медленнее, и это может нарастать как ком снежный. Прекрасный «эффект снежного кома».
Добавить ещё один уровень кэширования на уровне приложения — логичный шаг. Это можно сделать быстро, потому что не требуется изменений инфраструктуры, и может дать отличный эффект. К тому же многие сервисы, к которым обращается EMAC, живут под одним и тем же доменом, так что можно ожидать высокий процент попаданий в кэш.
В NodeJS существует несколько библиотек для кэширования DNS:
Мы выбрали cacheable-lookup
. Она учитывает TTL, работает с высокоуровневыми HTTP-библиотеками (например, с Axios) и широко используется.
Важно: если у ваших DNS-записей слишком длинный TTL, а IP-адреса серверов меняются, могут возникнуть серьёзные проблемы. Убедитесь, что записи настроены корректно.
Работать с 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 for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Frontend. Подписывайтесь, чтобы быть в курсе и ничего не упустить!
Для backend-for-frontend вроде EMAC, который каждую минуту делает тысячи запросов к одним и тем же доменам, кэширование DNS — очевидное решение.
Оно не устранит все ошибки таймаута, но точно поможет сделать EMAC более устойчивым и отзывчивым.
Как мы увидели, реализовать это в NodeJS просто. Главное помнить: кэширование на уровне приложения — лишь один из возможных вариантов. Если нужна более широкая поддержка, можно рассмотреть кэширование на уровне инфраструктуры. В любом случае важно обеспечить качественную наблюдаемость как на уровне приложения, так и на уровне инфраструктуры, чтобы можно было точно выявлять, понимать и устранять проблемы с задержками и сбоями.