Разворачиваем PowerDNS для корпоративного DNS: 10 000 запросов в секунду без потерь

Исходная ситуация

Телеком-провайдер «МегаНет» обратился к нам с классической проблемой: корпоративный DNS на связке BIND9 + ручные zone-файлы перестал справляться с ростом инфраструктуры. В сети более 800 серверов, три дата-центра, 45 внутренних доменных зон — и всё это обслуживалось двумя серверами BIND без какого-либо failover.

Конкретные проблемы:

  • Производительность — пиковая нагрузка достигала 10 000 запросов/сек, и серверы начинали терять пакеты после 7 000 RPS.
  • Управление зонами — каждое изменение требовало ручного редактирования zone-файлов, инкремента serial, перезагрузки BIND. Ошибка синтаксиса ломала всю зону.
  • Отказоустойчивость — при падении мастер-сервера slave-зоны устаревали, и через 2 часа клиенты теряли резолвинг.
  • Безопасность — DNSSEC не был настроен, cache poisoning был реальной угрозой.
  • Мониторинг — понять, кто и что запрашивает, было невозможно без разбора tcpdump.

Почему PowerDNS, а не BIND или Unbound

Мы рассмотрели три варианта: оставить BIND9, перейти на Unbound или развернуть PowerDNS. Выбор пал на PowerDNS по нескольким причинам:

  • Разделение ролей — PowerDNS предлагает отдельные компоненты: Authoritative Server для авторитативных зон и Recursor для кеширующего резолвинга. Это позволяет масштабировать каждую роль независимо.
  • Database backend — зоны хранятся в PostgreSQL, а не в текстовых файлах. Изменения вступают в силу мгновенно, без перезагрузки демона.
  • REST API — встроенный HTTP API для управления зонами и записями. Идеально для автоматизации через Ansible и CI/CD.
  • Prometheus-метрики — из коробки экспортирует более 80 метрик без сторонних экспортёров.
  • Производительность — при правильной настройке Authoritative Server обрабатывает до 100 000 QPS на одном ядре.

Unbound хорош как рекурсор, но не поддерживает авторитативные зоны с SQL-бэкендом. BIND9 умеет всё, но управление зонами через файлы не масштабируется при 45+ зонах с частыми изменениями.

Архитектура решения

Мы спроектировали трёхуровневую архитектуру DNS:

Клиент → dnsdist (L7 балансировщик) → PowerDNS Recursor → PowerDNS Authoritative
                                            ↓
                                    Внешние DNS (8.8.8.8, 1.1.1.1)
                                    
PowerDNS Authoritative → PostgreSQL 15 (Patroni HA-кластер)

В каждом из трёх ЦОД развёрнуто:

  • 2 × dnsdist — принимают все DNS-запросы на VIP-адрес через BGP anycast. Dnsdist умеет балансировать, кешировать, фильтровать и логировать запросы на L7.
  • 2 × PowerDNS Recursor — кеширующие резолверы. Если запись есть в кеше — отвечают сами. Если нет — идут к Authoritative или к внешним серверам.
  • 2 × PowerDNS Authoritative — обслуживают внутренние зоны. Подключены к общему PostgreSQL-кластеру через Patroni.

Для маршрутизации используем BGP anycast: все три площадки анонсируют один и тот же IP-адрес DNS-сервиса. BIRD daemon на каждом узле dnsdist управляет BGP-сессиями. При отказе площадки трафик автоматически перенаправляется на ближайшую живую.

# /etc/bird/bird.conf — анонс DNS VIP
protocol bgp upstream {
    local as 65100;
    neighbor 10.0.0.1 as 65000;
    source address 10.0.0.10;

    ipv4 {
        import none;
        export filter {
            if net = 10.255.0.53/32 then accept;
            reject;
        };
    };
}

protocol static dns_vip {
    ipv4 { };
    route 10.255.0.53/32 blackhole;
}

Настройка PowerDNS Authoritative с PostgreSQL

Начинаем с установки и настройки авторитативного сервера. PostgreSQL уже развёрнут в HA-конфигурации через Patroni — три ноды с автоматическим failover.

# Установка PowerDNS Authoritative на Ubuntu 22.04
sudo apt install pdns-server pdns-backend-pgsql

# Создаём базу и загружаем схему
sudo -u postgres createuser pdns_auth
sudo -u postgres createdb -O pdns_auth pdns_zones
psql -U pdns_auth -d pdns_zones < /usr/share/pdns-backend-pgsql/schema/schema.pgsql.sql

Конфигурация авторитативного сервера:

# /etc/powerdns/pdns.conf
setuid=pdns
setgid=pdns

# Слушаем только localhost — доступ через Recursor
local-address=127.0.0.1
local-port=5300

# PostgreSQL backend
launch=gpgsql
gpgsql-host=/var/run/postgresql
gpgsql-port=5432
gpgsql-dbname=pdns_zones
gpgsql-user=pdns_auth
gpgsql-password=<пароль_из_vault>
gpgsql-dnssec=yes

# API для управления зонами
api=yes
api-key=
webserver=yes
webserver-address=127.0.0.1
webserver-port=8081
webserver-allow-from=127.0.0.1,10.0.0.0/8

# Кеширование
query-cache-ttl=20
negative-query-cache-ttl=60

# Производительность
receiver-threads=4
distributor-threads=4

Создаём первую зону через API:

# Создание зоны meganet.internal
curl -s -X POST http://127.0.0.1:8081/api/v1/servers/localhost/zones \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "meganet.internal.",
    "kind": "Native",
    "nameservers": ["ns1.meganet.internal.", "ns2.meganet.internal."],
    "soa_edit_api": "INCEPTION-INCREMENT"
  }'

# Добавление A-записи
curl -s -X PATCH http://127.0.0.1:8081/api/v1/servers/localhost/zones/meganet.internal. \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rrsets": [{
      "name": "db-master.meganet.internal.",
      "type": "A",
      "ttl": 300,
      "changetype": "REPLACE",
      "records": [{"content": "10.10.5.20", "disabled": false}]
    }]
  }'

Благодаря soa_edit_api: INCEPTION-INCREMENT serial зоны обновляется автоматически при каждом изменении через API — больше никакого ручного инкремента.

Dnsdist как фронтенд и балансировщик

Dnsdist — это специализированный DNS-балансировщик от разработчиков PowerDNS. Он принимает запросы от клиентов и распределяет их между бэкендами с учётом health checks, политик маршрутизации и ACL.

-- /etc/dnsdist/dnsdist.conf

-- Принимаем запросы на VIP
addLocal('10.255.0.53:53')
setACL({'10.0.0.0/8', '172.16.0.0/12'})

-- Бэкенды Recursor текущего ЦОД
newServer({address='127.0.0.1:5301', name='recursor-local-1', pool='local'})
newServer({address='10.10.1.12:5301', name='recursor-local-2', pool='local'})

-- Бэкенды Recursor соседних ЦОД (fallback)
newServer({address='10.20.1.11:5301', name='recursor-dc2-1', pool='remote'})
newServer({address='10.30.1.11:5301', name='recursor-dc3-1', pool='remote'})

-- Политика: сначала локальный пул, при отказе — remote
setPoolServerPolicy(roundrobin, 'local')
setPoolServerPolicy(roundrobin, 'remote')

-- Маршрутизация: всё идёт в local pool
addAction(AllRule(), PoolAction('local'))

-- Health checks каждые 2 секунды
setServerPolicyLuaFFI('failover', function(servers, dq)
    for i = 1, #servers do
        local s = servers[i]
        if s.pool == 'local' and s:isUp() then
            return s
        end
    end
    for i = 1, #servers do
        local s = servers[i]
        if s.pool == 'remote' and s:isUp() then
            return s
        end
    end
    return servers[1]
end)

-- EDNS Client Subnet для передачи source IP
setECSSourcePrefixV4(32)
setECSSourcePrefixV6(128)

-- Rate limiting: не более 100 QPS с одного IP
addAction(MaxQPSIPRule(100), DropAction())

-- Блокируем запросы ANY (amplification attack)
addAction(QTypeRule(DNSQType.ANY), RCodeAction(DNSRCode.REFUSED))

-- Метрики для Prometheus
webserver('127.0.0.1:8083')
setWebserverConfig({apiKey=''})

Ключевой момент — EDNS Client Subnet с маской /32. Без неё PowerDNS видит только IP dnsdist, а не реального клиента. С ECS мы сохраняем полный адрес клиента для логирования и ACL в авторитативном сервере.

DNSSEC: подписываем зоны

DNSSEC защищает от подделки DNS-ответов (cache poisoning). PowerDNS делает настройку DNSSEC тривиальной благодаря встроенной поддержке:

# Активируем DNSSEC для зоны
pdnsutil secure-zone meganet.internal

# Проверяем ключи
pdnsutil show-zone meganet.internal
# Zone is actively secured
# Zone has following keys:
# ID = 1 (CSK), flags = 257, tag = 12345, algo = 13 (ECDSAP256SHA256)
# CSK DNSKEY = meganet.internal. IN DNSKEY 257 3 13 ...

# Проверяем корректность подписей
pdnsutil check-zone meganet.internal
# Checked 847 records of 'meganet.internal', 0 errors, 0 warnings.

# Ректифицируем зону (обновляем NSEC3 записи)
pdnsutil rectify-zone meganet.internal

Мы используем алгоритм ECDSAP256SHA256 (algo 13) вместо RSA — подписи в 4 раза меньше, что сокращает размер DNS-ответов и снижает нагрузку на сеть.

Для автоматической ротации ключей настроили cron-задачу:

# /etc/cron.d/pdns-dnssec-rotate
0 3 1 */3 * root pdnsutil activate-zone-key meganet.internal $(pdnsutil add-zone-key meganet.internal ksk active 257 ecdsap256sha256) && pdnsutil rectify-zone meganet.internal

Ротация KSK каждые три месяца. CSK (Combined Signing Key) позволяет использовать один ключ вместо связки KSK+ZSK, что упрощает управление для внутренних зон.

Автоматизация и CI/CD для DNS-зон

Ручное управление DNS-записями через API или веб-интерфейс не масштабируется при 45 зонах. Мы реализовали GitOps-подход: все записи описываются в YAML-файлах, изменения проходят через merge request и деплоятся автоматически.

Структура репозитория:

dns-zones/
├── zones/
│   ├── meganet.internal.yaml
│   ├── dc1.meganet.internal.yaml
│   ├── dc2.meganet.internal.yaml
│   └── 10.in-addr.arpa.yaml
├── scripts/
│   ├── sync_zones.py
│   └── validate_zones.py
└── .gitlab-ci.yml

Формат описания зоны:

# zones/meganet.internal.yaml
zone: meganet.internal.
records:
  - name: db-master
    type: A
    ttl: 300
    content: 10.10.5.20
  - name: db-replica-1
    type: A
    ttl: 300
    content: 10.10.5.21
  - name: db-replica-2
    type: A
    ttl: 300
    content: 10.10.5.22
  - name: db
    type: CNAME
    ttl: 60
    content: db-master.meganet.internal.
  - name: _postgresql._tcp
    type: SRV
    ttl: 300
    content: "0 5432 db-master.meganet.internal."
    priority: 10

Скрипт синхронизации сравнивает YAML с текущим состоянием зоны через API и применяет только diff:

#!/usr/bin/env python3
# scripts/sync_zones.py
import yaml, requests, sys

API = 'http://127.0.0.1:8081/api/v1/servers/localhost'
HEADERS = {'X-API-Key': os.environ['PDNS_API_KEY']}

def sync_zone(zone_file):
    with open(zone_file) as f:
        desired = yaml.safe_load(f)

    zone_name = desired['zone']
    current = requests.get(f'{API}/zones/{zone_name}', headers=HEADERS).json()
    current_rrsets = {(r['name'], r['type']): r for r in current['rrsets']}

    patch_rrsets = []
    for record in desired['records']:
        fqdn = f"{record['name']}.{zone_name}" if record['name'] != '@' else zone_name
        key = (fqdn, record['type'])
        rrset = {
            'name': fqdn,
            'type': record['type'],
            'ttl': record.get('ttl', 3600),
            'changetype': 'REPLACE',
            'records': [{'content': record['content'], 'disabled': False}]
        }
        if key not in current_rrsets or current_rrsets[key] != rrset:
            patch_rrsets.append(rrset)

    if patch_rrsets:
        resp = requests.patch(
            f'{API}/zones/{zone_name}',
            headers=HEADERS,
            json={'rrsets': patch_rrsets}
        )
        resp.raise_for_status()
        print(f'Updated {len(patch_rrsets)} records in {zone_name}')
    else:
        print(f'Zone {zone_name} is up to date')

CI/CD пайплайн: на merge request — валидация YAML и dry-run, на merge в main — деплой через sync_zones.py. При ошибке синтаксиса пайплайн падает до деплоя, и зона не ломается.

Мониторинг и результаты

Все компоненты стека экспортируют метрики в Prometheus. Мы создали Grafana-дашборд с ключевыми панелями:

  • QPS по типам запросов — A, AAAA, SRV, PTR. Позволяет видеть аномалии (внезапный рост ANY — признак DDoS).
  • Латентность — P50, P95, P99 времени ответа. Целевой показатель: P99 < 5 мс для внутренних зон.
  • Cache hit ratio — на Recursor должен быть >80% для нормальной работы.
  • Доступность бэкендов — health check статус каждого сервера в dnsdist.
  • Ошибки — SERVFAIL, NXDOMAIN, timeout по каждой зоне.

Для логирования DNS-запросов используем DNStap — бинарный протокол, который PowerDNS отправляет в go-dns-collector. Коллектор парсит protobuf-сообщения и складывает в Elasticsearch:

# /etc/dnsdist/dnsdist.conf — включаем DNStap
fsul = newFrameStreamUnixLogger('/var/run/dnsdist/dnstap.sock')
addAction(AllRule(), DnstapLogAction('dnsdist-dc1', fsul))
addResponseAction(AllRule(), DnstapLogResponseAction('dnsdist-dc1', fsul))

Результаты после миграции:

МетрикаДо (BIND9)После (PowerDNS)
Максимальный QPS7 000 (с потерями)25 000 (без потерь)
P99 латентность45 мс3.2 мс
Время добавления записи5–15 минут (ручное)2 секунды (API)
Время failover2+ часа (manual)3 секунды (BGP)
DNSSECНе настроенВсе 45 зон подписаны
Видимость запросовНет (tcpdump)Полная (DNStap + ELK)

Клиент «МегаНет» получил отказоустойчивый, масштабируемый DNS с полной автоматизацией и мониторингом. Инфраструктура выдерживает рост до 100 000 QPS без изменения архитектуры — достаточно добавить ноды в dnsdist-пул.

Часто задаваемые вопросы

Authoritative Server обслуживает зоны, за которые он ответственен — отвечает на запросы из своей базы данных. Recursor — это кеширующий рекурсивный резолвер, который ищет ответы у других DNS-серверов и кеширует результаты. В продакшене они работают вместе: Recursor обращается к Authoritative для внутренних зон и к внешним DNS для всего остального.
Да, PowerDNS поддерживает MySQL/MariaDB, SQLite, LDAP и другие бэкенды. Мы выбрали PostgreSQL из-за зрелой поддержки HA через Patroni и более надёжной работы с DNSSEC. MySQL-бэкенд также стабилен и подходит для большинства инсталляций.
Для простых инсталляций с одним ЦОД dnsdist не обязателен. Но при нескольких площадках он незаменим: BGP anycast, rate limiting, ACL, health checks, логирование и переключение между пулами бэкендов. Dnsdist добавляет менее 0.1 мс латентности, что незаметно.
С PostgreSQL-бэкендом PowerDNS обрабатывает зоны любого размера благодаря индексам в БД. Для зон с сотнями тысяч записей рекомендуется увеличить query-cache-ttl до 60 секунд и настроить pgbouncer для пулинга соединений. В наших тестах зона с 200 000 A-записей отвечала за 1.5 мс.
Поэтапно: сначала разверните PowerDNS рядом с BIND и импортируйте зоны командой zone2sql (утилита из комплекта PowerDNS). Затем переключите часть клиентов на новый DNS через split-horizon в dhcpd или изменение resolv.conf через Ansible. Убедитесь, что все записи резолвятся корректно, и только после этого отключайте BIND.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#powerdns#dns сервер#authoritative dns#recursor#dnsdist#postgresql backend#dnssec#bgp anycast
Комментарии 0

Оставить комментарий

загрузка...