Привет, Хабр!
Сегодня рассмотрим, как NGINX работает с DNS и почему proxy_pass
не резолвит домены на ходу.
Как DNS вообще работает в Linux (и почему это важно для NGINX)
Когда мы говорим про proxy_pass
в NGINX, важно понимать, через какой именно механизм резолвятся доменные имена, потому что от этого зависят кеши, поведение при сбоях, реакция на смену IP и вообще предсказуемость всего маршрута запроса.
Первое, с чего надо начать: NGINX — не магический прокси. Он не абстрагирован от системы. Он работает в рамках ОС, и поведение его DNS‑запросов напрямую зависит от того, используете ли вы переменные в proxy_pass
и указываете ли resolver
.
Два режима резолвинга
Сценарий | Что делает NGINX | Через что резолвит |
---|---|---|
| Однократный резолвинг при старте (или reload) | системный стек, чаще всего |
| Резолвинг при каждом запросе или по | собственный асинхронный DNS‑клиент NGINX |
В первом случае NGINX использует системный резолвер, а значит:
читает
/etc/resolv.conf
следует
nsswitch.conf
и может обращаться к кастомным провайдерам (libnss_mdns
,libnss_resolve
)может получать IP из кеша
systemd-resolved
,nscd
или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 у домена — вам нужно:
Указать
resolver
;Использовать переменные в
proxy_pass
.
Без переменных — только однократный резолвинг при старте.
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 может отдавать большие ответы — поставьте перед ним dnsmasq
, CoreDNS
или 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 сам что‑то под капотом «подтягивает», то теперь знаете: он тупо кеширует результат и ничего больше. И этим он и хорош — прозрачность даёт предсказуемость.
Если вы работаете с NGINX не только «по документации», но и в продакшене — возможно, вам будет интересно углубиться в смежные темы. В Otus проходят открытые уроки, где разбирают инструменты, подходы и практики, которые часто идут рука об руку с тем, что обсуждалось в статье. Записывайтесь по ссылкам ниже:
— 24 апреля — Docker в действии: как контейнеризация меняет аналитику данных
О том, как контейнеры и микросервисы влияют на организацию сетевого взаимодействия и динамику DNS в инфраструктуре.
— 29 апреля — Release it: практические аспекты выпуска надёжного софта
Разбор типовых ошибок при работе с внешними сервисами, тайм-аутами, ретраями и тем самым «502 после резолвинга».
— 22 мая — Оптимизация NGINX и Angie под высокие нагрузки
Настройки, которые помогают выжать из NGINX максимум при работе в динамических окружениях. Разбор кейсов, где resolver и failover — не просто строчки в конфиге.