Привет, Хабр!
Сегодня рассмотрим, как 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? Посетите наш календарь открытых уроков — это отличная возможность бесплатно прокачать знания и задать вопросы экспертам.
А полный путь — в каталоге курсов: выберите направление и развивайтесь системно.
