Коротко о DNS в NGINX

Как DNS вообще работает в Linux (и почему это важно для NGINX)

Когда мы говорим про proxy_pass в NGINX, важно понимать, через какой именно механизм резолвятся доменные имена, потому что от этого зависят кеши, поведение при сбоях, реакция на смену IP и вообще предсказуемость всего маршрута запроса.

Первое, с чего надо начать: NGINX — не магический прокси. Он не абстрагирован от системы. Он работает в рамках ОС, и поведение его DNS‑запросов напрямую зависит от того, используете ли вы переменные в proxy_pass и указываете ли resolver.

Два режима резолвинга

СценарийЧто делает NGINXЧерез что резолвит
proxy_pass http://example.com; без resolver и без переменныхОднократный резолвинг при старте (или reload)системный стек, чаще всего gethostbyname()
proxy_pass http://$backend; + resolverРезолвинг при каждом запросе или по valid=собственный асинхронный DNS‑клиент NGINX

В первом случае NGINX использует системный резолвер, а значит:

  • читает /etc/resolv.conf
  • следует nsswitch.conf и может обращаться к кастомным провайдерам (libnss_mdnslibnss_resolve)
  • может получать IP из кеша systemd-resolvednscd или dnsmasq, если так настроено
  • поведение варьируется от дистрибутива к дистрибутиву

В этом режиме, если IP у домена example.com поменялся — NGINX об этом не узнает. Он будет работать с IP, который получил при старте.

Как только вы пишете:

resolver 8.8.8.8 valid=10s;

— всё меняется.

Теперь NGINX:

  • не использует glibc;
  • не обращается к системному кешу;
  • делает DNS‑запросы сам, напрямую через UDP на указанный resolver;
  • полученные IP кешируются ровно на valid=5s, затем резолвятся заново.

Это в особенности нужно, если ваши backend‑сервисы живут за балансировщиком, в динамическом окружении (Kubernetes, Nomad, Docker Swarm и т. п.), или вообще регулярно получают новые IP по SRV‑записям (которые, кстати, NGINX не умеет).

Главное, что нужно запомнить

Если вы хотите, чтобы NGINX на лету подхватывал изменения IP у домена — вам нужно:

  1. Указать resolver;
  2. Использовать переменные в proxy_pass.

Без переменных — только однократный резолвинг при старте.

Если хотите, могу дополнительно добавить секцию про то, как проверить, какой именно механизм сейчас работает в вашей конфигурации — с stracelsoftcpdump.

DNS-запросы глазами NGINX

Когда вы прописываете:

resolver 1.1.1.1 valid=5s;

server {
listen 80;
location / {
set $up backend.local;
proxy_pass http://$up;
}
}

NGINX при первом обращении к переменной в proxy_pass отправляет обычный UDP‑запрос типа A (или AAAA, если IPv6 не отключён).

Пример DNS‑запроса, который делает NGINX:

sudo tcpdump -n port 53 -i any and udp

Ответ кешируется ровно на valid=X секунд. В течение этого времени никаких повторных запросов не будет. По истечении срока — резолвится заново.

Пример отладки резолвинга

Допустим, есть вот такой конфиг:

resolver 1.1.1.1 valid=5s;

server {
listen 80;
location / {
set $up backend.local;
proxy_pass http://$up;
}
}

Чтобы понять, работает ли резолвинг как надо, запускаем tcpdump:

sudo tcpdump -n port 53 -i any and udp

Теперь с другого терминала:

curl http://localhost/

Мы увидим DNS‑запрос от NGINX напрямую к 1.1.1.1. Это и есть тот самый прямой резолвинг.

Если valid=5s, и вы повторите curl в течение 5 секунд — запроса не будет. Если позже — будет новый.

Какие типы DNS-записей NGINX поддерживает?

Список минимален:

  • A (IPv4)
  • AAAA (IPv6, если не отключено)

CNAME — только если он разворачивается до A/AAAA (что обычно делает DNS‑сервер). SRV, TXT, MX и прочее — не поддерживаются напрямую. Если вы используете service discovery, где backend отдаётся как SRV‑запись (например, в Consul или Kubernetes), вам нужен внешний резолвер или промежуточный прокси.

Что происходит при ошибке DNS?

Если DNS не отвечает, или NGINX не может его разрешить — то получим 502 Bad Gateway. Пример:

http {
resolver 8.8.8.8 1.1.1.1 valid=5s ipv6=off;
resolver_timeout 2s;

map $upstream_http_x_healthcheck $backend_host {
default "api-primary.example.com";
"fail" "api-fallback.example.com";
}

server {
listen 80;

location /api/ {
proxy_pass http://$backend_host;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
}

Чтобы смягчить последствия, используем:

resolver_timeout 2s;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;

Конфигурация с DNS и fallback

http {
resolver 8.8.8.8 1.1.1.1 valid=5s ipv6=off;
resolver_timeout 2s;

map $upstream_http_x_healthcheck $backend_host {
default "api-primary.example.com";
"fail" "api-fallback.example.com";
}

server {
listen 80;

location /api/ {
proxy_pass http://$backend_host;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
}

Интеграция с Kubernetes через headless service

Kubernetes не поддерживает балансировку через обычный DNS, если вы используете headless‑сервис:

apiVersion: v1
kind: Service
metadata:
name: myapp-headless
spec:
clusterIP: None
selector:
app: myapp
ports:
- port: 80

Каждый DNS‑запрос к myapp-headless.default.svc.cluster.local будет возвращать все IP подов.

NGINX умеет использовать только один из них. Поэтому при каждом новом резолвинге (по valid=5s) он может выбрать другой под, но не все сразу.

Если нужно распределение между всеми — используем внешний sidecar или nginx+lua с round‑robin логикой вручную.

Как NGINX выбирает IP, если DNS отдаёт несколько?

Допустим, DNS отвечает:

A 10.0.0.1
A 10.0.0.2
A 10.0.0.3

NGINX возьмёт первый из списка. Если соединение не удалось, переключится на следующий. При следующем резолвинге — снова первый. Это не балансировка, это fallback.

Если нужна настоящая балансировка — используем upstream с IP‑шардингом заранее:

upstream dynamic_backend {
server 10.0.0.1;
server 10.0.0.2;
server 10.0.0.3;
}

proxy_pass http://dynamic_backend;

Но тут нужен внешний скрипт/сервис, который обновляет этот список.

Вот три плотных и технически насыщенных блока, каждый по заявленной теме. Написаны в едином стиле статьи — лаконично, глубоко и по существу, с комментариями «от старшего разраба».

Edge-кейсы с DNS-over-TCP

По дефолту NGINX делает DNS‑запросы через UDP. Это быстро и дешево. Но есть ситуации, когда UDP — ненадёжен: слишком длинные ответы (больше 512 байт без EDNS), или отказ со стороны DNS‑сервера с флагом TC. В таком случае по спецификации RFC 1035 клиент должен повторить запрос через TCP.

NGINX этого не делает.

Встроенный DNS‑резолвер NGINX не поддерживает fallback на TCP, и даже не поддерживает EDNS (расширения DNS). Это значит, что если DNS‑сервер отдаёт слишком большой ответ, или обрезает его — вы получите ошибку 502 Bad Gateway в NGINX, и всё.

Пример, где это может случиться:

  • У вас example.com, а в ответе — 20 A‑записей (например, от SRV‑провайдера).
  • Ответ > 512 байт.
  • DNS‑сервер обрезает и говорит перезапроси по TCP.
  • А NGINX не может.

Если ваш DNS может отдавать большие ответы — поставьте перед ним dnsmasqCoreDNS или Unbound, который будет агрегировать и резать ответы как надо.

Lua-расширения и dns.resolver

Если встроенный резолвер вас не устраивает, есть выход — использовать Lua через ngx_lua (OpenResty или NGINX с модулем lua-nginx-module).

Пример на Lua, где резолвинг делается через resty.dns.resolver:

local resolver = require "resty.dns.resolver"
local r, err = resolver:new{
nameservers = {"8.8.8.8", "1.1.1.1"},
retrans = 5,
timeout = 2000,
}

local answers, err = r:query("backend.example.com", { qtype = r.TYPE_A })
if not answers then
ngx.log(ngx.ERR, "failed to query: ", err)
return ngx.exit(500)
end

for _, ans in ipairs(answers) do
if ans.address then
ngx.var.backend_ip = ans.address
break
end
end

И потом в nginx.conf:

set_by_lua_block $backend_ip {
-- код выше
}

proxy_pass http://$backend_ip;

Плюсы:

  • Полный контроль над резолвингом.
  • Поддержка кастомных таймаутов, TCP, EDNS.
  • Можно писать retry‑логику, балансировку, SRV, даже DNSSEC.

Минусы:

  • Нужно собрать NGINX с поддержкой Lua.
  • Производительность чуть ниже, чем у встроенного резолвера.

Per-request DNS: резолвинг на каждый запрос

Встроенный механизм resolver + переменная в NGINX делает DNS‑запрос не на каждый запрос, а по истечении valid=X секунд.

Если нужно прям реально DNS на каждый HTTP‑запрос — нужно уходить в Lua или использовать внешние sidecar‑прокси, например Envoy или custom DNS‑клиент.

Вариант на Lua:

set_by_lua_block $backend_ip {
local resolver = require "resty.dns.resolver"
local r = resolver:new{ nameservers = {"1.1.1.1"}, timeout = 1000 }
local a, err = r:query("backend.example.com", { qtype = r.TYPE_A })
if not a or #a == 0 then return "127.0.0.1" end
return a[1].address
}

Код будет выполняться при каждом запросе (если блок set_by_lua_block указан в location), и делать прямой DNS‑запрос.

Итог: что стоит помнить

  • NGINX резолвит DNS сам, если вы используете переменные.
  • Без переменных — используется system resolver (glibc).
  • Указывайте resolver — всегда, если работаете с переменными.
  • Используйте resolver_timeout, чтобы не залипать.
  • valid=5s — хороший старт, но не забывайте про нагрузку на DNS.
  • Если у вас headless‑сервисы или SRV‑записи — придётся городить костыли или писать свой лоадер.

Если вы думали, что NGINX сам что‑то под капотом «подтягивает», то теперь знаете: он тупо кеширует результат и ничего больше. И этим он и хорош — прозрачность даёт предсказуемость.