Как стать автором
Обновить
588.3
OTUS
Цифровые навыки от ведущих экспертов

Коротко о DNS в NGINX

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров5.5K

Привет, Хабр!

Сегодня рассмотрим, как NGINX работает с DNS и почему proxy_pass не резолвит домены на ходу.

Как 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_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 у домена — вам нужно:

  1. Указать resolver;

  2. Использовать переменные в 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 — не просто строчки в конфиге.

Теги:
Хабы:
+18
Комментарии10

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS