Через реки, через лес прямо к PowerDNS

Всем привет! Меня зовут Максим, я руководитель одной из групп эксплуатации инфраструктурных сервисов в Ozon. Наша команда занимается поддержкой и развитием нескольких базовых сервисов компании, одним из которых, по историческим причинам, является сервис разрешения доменных имен (DNS).

b94aaf43270c9bbc03ba58d78b15eece.png

В Ozon много различных сервисов и систем. Они общаются друг с другом и внешним миром по доменным именам. DNS — центральное звено, без которого не обходится почти ни одна инфраструктура. Понятно, что когда DNS отдаёт некорректные данные, то это неприятно, когда таймаутит — плохо, когда прилёг — очень плохо, когда прилёг надолго — в принципе, можно расходиться. Значит, одна из основных задач команды инфраструктуры — обеспечить сервисам надёжное и, желательно, быстрое разрешение доменных имён. Об этом мы и поговорим. Также затронем вопросы управления ресурсными записями, жизнь в Multi DC-среде, обслуживание DNS, кеширование, журналирование запросов и возможные проблемы.

Статья может быть полезна коллегам, интересующимся эксплуатацией, архитектурой и высокой доступностью сервисов, да и просто может быть любопытна как история построения инфраструктурной единицы в крупной компании.

Небольшое примечание: потребителями нашего инфраструктурного DNS будут внутренние клиенты компании (любой сервис во внутренней сети). Внешние зоны у нас, конечно же, есть, но, исторически, ими занимается команда NOC (сетевые инженеры), и передавать их в инфру пока не стремится. Поэтому мы ориентируемся только на внутренних клиентов. По этой же причине в статье не будем затрагивать DNSSEC и IPv6.

Disclaimer

Любые совпадения имен машин и IP-адресов с реальными именами и ip случайны.

Основная часть

Так, а зачем нам вообще что-то менять, разве наш текущий инфраструктурный DNS ненадёжен? У меня вот есть три DNS-сервера в трёх ЦОДах, все они прописаны в resolv.conf каждого клиента, то есть если основной сервер прилёг, системный резолвер пойдёт во второй, а затем и в третий.

Дело в том, что в самом неудачном случае (когда остался доступен только третий ЦОД) и при дефолтных настройках резолвера (option timeout:5), последний nameserver ответит на запрос только по прошествии десяти секунд, что в микросервисной среде, когда один сервис взаимодействует с десятками других микросервисов и в коде нет кеширования DNS-запросов, просто недопустимо. Можно поиграть с уменьшенным таймаутом и другими опциями системного резолвера, но это не изменит картину принципиально. Тот же options rotate будет отправлять запросы на серверы из nameserver-списка по алгоритму round-robin (то есть по очереди), и 33% запросов будут обработаны без таймаутов… ТОЛЬКО 33%! Нам это не подходит.

Зафиксируем первое требование: Не хотим полагаться на системный resolver.

Идём дальше. Думаю, многие слышали и, возможно, нередко используют «smart DNS resolver». По сути, это легковесный DNS forwarder, который ставится на каждый хост. Его ключевой особенностью является функциональность отправки параллельных запросов сразу в несколько DNS-серверов и обработка ответа от самого быстрого из них. Достаточно интересно — получаем и отказоустойчивость, и максимальную скорость разрешения имён.

(Возможно, кому-то трафик x2,3, N может быть критичен, поэтому следует это учитывать в своей архитектуре.)

Для гомогенной среды (например, везде Linux) с единым центром управления (DNS контролирует одна команда инженеров) это вполне себе хорошее решение, и, что немаловажно, — простое.

Но, у нас не гомогенная среда (встречается Windows, немного, но есть), а также много инфраструктурных команд; придётся уговаривать каждую команду. К тому же при изменении адреса хотя бы одного DNS-сервера (например, при переезде машины в другой ЦОД), конфигурацию «smart DNS» нужно будет раскатить на тысячи машин… Тоже не подходит.

Вобщем, хочется более гибкого и универсального решения. Универсальное — это значит, что IP-адрес DNS-сервиса не должен меняться.

Второе требование: DNS-сервис должен быть отказоустойчив на сетевом (IP не меняется) уровне (L3 OSI).

Получается, за одним IP-адресом нашего DNS-сервиса должно скрываться два и более реальных DNS-сервера. Давайте для краткости такой IP называть VIP-адресом (Virtual IP), реальный DNS-сервер — real (сервером), а пачку реальных DNS-серверов за VIP — пулом (pool)

Также в Ozon есть общее требование:

Сервис должен переживать полную недоступность одного ЦОД на любой период времени (мы называем это требование DC-1).

Что делать, если за VIP один из real-серверов «задумался» или «приказал долго жить»? По сути, мы попадём в ту же ситуацию, что и с системным resolver, когда некая доля DNS-запросов будет гарантировано не обслужена. Именно поэтому необходим механизм проверок (healthchecks) real-серверов с выкидыванием из пула «больных» и закидыванием обратно «здоровых».

Следующее требование: Проверки (healthchecks) real-серверов со стороны VIP.

Мы будем успевать обрабатывать входящие запросы? Нагрузка на наш текущий DNS сервис весьма незначительна, всего 15–20 тыс. RPS, которую легко вывозит одна виртуалка, поэтому в эту сторону сильно не копаем. А с горизонтальным масштабированием и подавно вопрос «успевания» полностью закрыт. Оно у нас, скорее, для высокой доступности и возможности локализации трафика.

Пятое требование: Уметь горизонтально масштабироваться.

Раз уж у нас несколько ЦОД, стоит подумать и о сетевых задержках. Странно ходить в DNS из одного ЦОД в другой, когда под боком есть рабочий экземпляр DNS. Мы хотим, чтобы при наличии сервера в том же ЦОД сервисы ходили именно в него и получали ответ быстрее.

Шестое требование: Уметь локализовывать трафик в пределах ЦОД.

А что если мне захочется выполнить регламентное обслуживание сервера (обновить ОС, железо, другой деструктив)? Я могу остановить локальную DNS-службу, дождаться неудачных healthchecks, дождаться исключения сервера из пула и заняться обслуживанием. Но тогда все запросы после остановки локального DNS и вплоть до исключения сервера из пула завершатся ошибкой. Такое себе. Поэтому:

Седьмое требование: Плавно выводить трафик с сервера, на котором проводим регламентные работы.

DNS-сервер должен поддерживать динамическое обновление зон по протоколу DNS или API. Это нужно для различных систем наливки железных и виртуальных серверов (Foreman, MaaS, Terraform и т.д.).

Восьмое требование: Поддержка динамически обновляемых зон.

Далее, что насчёт IP-адреса клиента?

Очень желательно, я бы даже сказал, обязательно для поддержки и расследования инцидентов. А ещё это может пригодиться нам в будущем, например geo DNS прикрутить.

Девятое требование: Должны видеть source IP источника запроса.

Логирование DNS-запросов? Да, отдел информационной безопасности очень хочет. К тому же с логами debug куда проще и приятнее.

Десятое требование: Уметь логировать DNS-запросы.

В современном мире без мониторинга никуда, поэтому у DNS-сервиса должны иметься встроенные метрики. Прекрасно, если это будет формат Prometheus, потому что именно эту систему мониторинга мы используем в Ozon.

Последнее требование: Наличие встроенных в DNS-сервис метрик.

Прикинем архитектуру

По задумке путь прохождения клиентского DNS-запроса должен выглядеть так:

Client (DNS-query) → LoadBalancer → Caching DNS → Authoritative DNS 

Конечно, все компоненты DNS (LoadBalancer, Caching DNS, Authoritative DNS) — должны быть зарезервированы, поэтому немного усложним схему дублирующими узлами.

f4f034201f50923009cb9a5b0af026ba.png

Машины LoadBalancer (LB) являются точками входа клиентского трафика. Напомню, что у них должен быть VIP — единый IP-адрес, на который LB будет откликаться.

VIP (Virtual IP)

На ум приходят две технологии: CARP/VRRP и динамическая маршрутизация (BGP). Давайте сравним:

230e5d3d5e72bc2ca6fa3702a3edad01.png

LoadBalancer

Если мы озаботились вопросом о healthchecks, то должен быть компонент в нашей инфраструктуре, который эти проверки запускает. Обычно таким компонентом является балансировщик (LoadBalancer, или LB). Из названия понятно, что его главная задача — балансировка, то есть распределение трафика между real-серверами. Кроме того, он, как правило, проверяет доступность этих real-серверов, и при необходимости убирает и добавляет их из пула (балансировки). 

Тут мы выбирали из следующих вариантов:

9ad52899e9b439b9a9f20ead003f61fe.pngНемного про внутреннее устройство сети в Ozon:

Три ЦОД. В качестве физической сети, или underlay, выступает Layer 3 Network, в качестве overlay — BGP EVPN VXLAN. VLAN между ЦОД стараемся не тянуть, чтобы не увеличивать сложность обслуживания сети.

IPVS NAT mode обяжет нас возвращать ответ через тот же IPVS-шлюз, что уже непросто для real-сервера, если таких шлюзов больше одного. Усложняется debug.

IPVS DR mode завязан на L2-сегмент сети. И если не тянуть эти сегменты через VLAN, то «пошарить» real-серверы между ЦОД не получится.

IPVS IPIP — самый подходящий: благодаря GRE-туннелю трафик может быть проброшен в любую L3-сеть и отдан клиенту напрямую, минуя IPVS. Но, опять же, усложняется конфигурирование сети, debug и мониторинг.

Вывод: выбираем Userspace Layer7 LB как самый функциональный по возможностям и простой с точки зрения конфигурации сети.

Про source IP клиента

Обсудим способы, которыми можем выполнить требования записи IP-адреса каждого клиента в логи системы.

EDNS Client Subnet (ECS)

Принцип работы: модифицируем (если есть, и добавляем, если отсутствует) поле ECS в DNS-запросе, прописывая там source IP клиента. Поскольку записывается подсеть, для того, чтобы получить конкретный IP, нам нужна маска /32 (для IPv4). Добавлением поля ECS будет заниматься L7 LoadBalancer.

Недостаток в том, что это решение требует анализа и редактирования исходного запроса: балансировщик принимает DNS-запрос, разбирает его на Layer7 (OSI), принимает решение о модификации или добавлении поля ECS в запрос. 

X-Proxied-For (XPF)

По сути, мета-RR (resource record), которая добавляется к запросу. Имеет экспериментальный статус, с истёкшим драфтом.

Не рассматриваем это решение, потому что нам нужно надёжное и проверенное временем. 

Proxy Protocol (PROXYv2)

Информация о начальных адресах и портах источника и получателя добавляется в header в начале UDP-датаграммы или TCP-соединения, аналогично тому, как это реализовано в nginx, Haproxy и т.д. 

Недостаток в том, что требуется поддержки в lb, resursor и auth.

Поддержка в PowerDNS: Dnsdist начиная с 1.5.0, recursor — с 4.4.0, auth — с 4.6.0.

В результате на момент выбора механизма передачи source IP клиента самым универсальным решением показался ECS. И так как проблем с производительностью на наших небольших объемах нет (15–20 тыс. RPS), мы остановились на нём. 

Proxy protocol, честно говоря, пощупать тогда не удалось, не было нужных версий бинарей. Как бы там ни было, этот вариант выглядит вполне рабочим. Недавно руки дошли, протестировали, работает. Оставили его как вариант на будущее, если возможностей ECS перестанет хватать.

Подробнее про source ip от Dnsdist здесь и здесь. 

Control Plane

Для хранения RR раньше мы использовали PowerDNS + Generic PostgreSQL backend. 

Но после аварии (о которой я расскажу ниже) решили пересмотреть подход к хранению данных. Для динамически обновляемых зон (например, по протоколу DNS update) можно оставить SQL-совместимый backend (тот же PostgreSQL), а для относительно редко меняющихся RR взять что-нибудь более статичное, например, BIND zone backend. 

Для редактирования RR удобно пользоваться какой-нибудь готовой админкой, например, PowerDNS-Admin. Тяп-ляп и в production. К сожалению, использование таких инструментов несёт серьёзные риски, которые незаметны на первый взгляд. Мы начали с такого решения, что привело к плачевным последствиям: потере данных и простою на время восстановления. Подробнее об этом ниже.

Так или иначе, необходимо проработать более отказоустойчивое решение. 

В нашей компании для доставки кода мы используем Gitlab. Почему бы не доставлять через него статику bind-zone? Подумаем. 

Обслуживание (Maintenance)

При обслуживании любого компонента нашей архитектуры мы хотим плавно убирать с него нагрузку без влияния на клиентские запросы, и потом так же плавно возвращать. Другими словами, switchover. 

Нам нужен признак, по наличию которого auth, recursor и сервис BGP-маршрутизации поймут, что им следует снять нагрузку. По опыту, самым простым признаком может быть наличие обычного файла: если он отсутствует — нормальный режим работы, присутствует — режим обслуживания (убираем нагрузку).

Локализация трафика

Тут всё просто. Anycast BGP перенаправляет клиента на ближайший LoadBalancer. который всегда находится в том же ЦОД, что и клиент (если анонс VIP поднят и нет специфичных настроек маршрутизации). Далее LoadBalancer отправляет DNS-запрос на локальный, заранее прописанный в конфиге Caching/Authoritative DNS, и только в случае его недоступности — на Caching/Authoritative DNS, находящийся в одном из соседних ЦОД.

Подытожим нашу архитектуру 

Клиентский трафик заводится в три ЦОД через BGP anycast. Через ECMP попадает на несколько Layer7 LoadBalancer, далее перенаправляется на Caching DNS, и затем — на Authoritative DNS. 

LoadBalancer живут на отдельных машинах (в нашем случае — виртуальных). Caching DNS и Authoritative DNS живут на одной виртуалке (VM). 

С транспортным цехом (NOC, network operation center) договорились, что там, где будет настроена BGP-маршрутизация, будут жить только балансировщики. 

Caching DNS и Authoritative DNS на одной машине — такая конструкция достаточно автономна, легко горизонтально масштабируется, нет лишних сетевых хопов. 

Авторитативный DNS должен иметь два бэкенда: статичный, в котором нельзя удалить RR легким кликом мышки, и динамичный — для всякого рода DNS update (Foreman), редактирование через API (Terraform, MaaS). 

Также должно быть управляющее ПО, через которое народ сможет вносить изменения (в RR) в бэкенды, описанные выше. 

Нагрузка с машины LB снимается прекращением BGP-анонса VIP-адреса, с машины Caching/Authoritative DNS — неудачным healthcheck со стороны LB. 

Failover: в случае проблем на одной из LB-машин BGP-анонс VIP-адреса прекращается, таблицы маршрутизации перестраиваются, клиенты идут на оставшиеся LB-машины. 

В случае проблем с виртуальной машиной с Caching/Authoritative DNS балансировщик через healthchecks понимает это и переключает трафик на виртуалку Caching/Authoritative DNS в другом ЦОД. 

Switchover: аналогично failover, но без отказа в обслуживании для клиента — нагрузка с машины LB снимается с помощью прекращения BGP-анонса VIP-адреса, с машины Caching/Authoritative DNS — неудачным healthcheck со стороны LB. 

Мониторинг через Prometheus. 

790399c165b0f898fda2492f6fe765d0.png

По сути, DNS — это такой же сервис, и перед выкаткой в production новая конфигурация должна быть протестирована на более терпимом к сбоям окружении. Таким окружением для PowerDNS, как и для наших микросервисов, является staging. Для максимального приближения к условиям в prod на PowerDNS кластер в staging-окружении также подаётся нагрузка. Это запросы сервисов из development- и staging-окружений. Таким образом, и по конфигу, и по данным, и по наличию трафика кластеры в production и staging почти идентичны. Разница только в конфигурации «железной» зоны (h.lan). Исторически сложилось, что у нас нет отдельной зоны для production, отдельной для staging и т.д. Все реальные имена машин изо всех окружений находятся в h.lan. Очевидно, что вносить изменения в «железную» зону мы обязаны через единый эндпоинт, которым является production. При этом изменения, прилетевшие в production, мы должны видеть и в staging PowerDNS. Связывать эти окружения БД-репликацией не очень хорошая идея, потому что реплика из staging легко может повлиять на prod, а вот обычная master → slave DNS-репликация (через AXFR) — то, что нужно. 

d359565ced982a509e62b8834ffd9b68.png

Выбор программного обеспечения

Authoritative DNS и Caching DNS

В качестве authoritative и caching DNS у нас уже использовался PowerDNS, поэтому в первую очередь смотрели на него.

PowerDNS Authoritative Server (или просто pdns auth) поддерживает различные SQL-базы, файлы bind zone, кучу других бэкендов, Lua-записи (это нам чуть позже пригодится), встроенные метрики для Prometheus и SNMP, имеет API для менеджмента RR и управления демоном. 

PowerDNS Caching Server (или просто pdns recursor) — высокопроизводительный кеширующий DNS-сервер со встроенной поддержкой Lua-сценариев. Метрики тоже имеются. 

L7 LoadBalancer

Долго искать не пришлось: оказывается, в PowerDNS обо всём уже подумали и завезли Dnsdist — балансировщик с защитой от DNS- и DoS-атак. 

Справедливости ради попытались нагуглить другие альтернативы, но ничего даже отдалённо напоминающее Dnsdist не нашлось. 

BGP

Кроме, собственно, BGP-сервиса нам нужны healthcheks, чтобы снять или вернуть нагрузку (убрать или вернуть анонс /32 префикса VIP), если что-то не так с интерфейсом, dnsdist, или если мы решили вывести машину на обслуживание. 

Выбирали между:  

  • ExaBGP — демоном на Python с поддержкой healthchecks, но без BFD.

  • BIRD — можно сказать, стандартом де-факто в индустрии. Есть встроенный BFD, но нет healthchecks, то есть все проверки придётся костылить сбоку. 

Из-за большой применимости и наличия встроенного BFD остановились на BIRD. 

Database & database orchestrator

Для редактирования RR PowerDNS требует SQL-совместимую БД. В нашей компании большая экспертиза в PostgreSQL, поэтому выбрали её. Также в качестве оркестратора поверх PostgreSQL мы используем Patroni, здесь выбор тоже очевиден. 

Кеширование в PowerDNS

Если взять всю цепочку прохождения DNS-запроса — Dnsdist → PDNS Recursor → PDNS Auth, — то в сумме мы получим пять уровней, на которых запрос может быть закеширован. Пять, Карл!!! Понятно, что если мы включим их все, то навряд ли debug окажется лёгкой прогулкой. 

Dnsdist: packet cache. 

PDNS Recursor: packet cache, query cache.

PDNS Auth: packet cache, query cache.

Разница между кешами пакетов и запросов следующая.

В кеше пакетов лежат записи в виде уже сформированных ответов. То есть на идентичные запросы без какой-либо дополнительной обработки отдаём готовые ответы. Получается вроде кеша запросов в MySQL. 

Query cache или record cache, — кеш внутренних запросов, сюда попадают отдельные записи, то есть ответы от бэкенда. Некоторые запросы приводят к ряду внутренних запросов, которые также попадут в query cache. Самый очевидный пример — когда запрос A-типа приводит к дополнительному запросу CNAME-типа. 

TTL в ответе из packet cache не будет меняться, в то время как из query cache TTL честно будет уменьшаться на 1 с каждой секундой. 

Что еще… Packet cache для всех ответов проставляет одинаковый TTL (packetcache-ttl), в то время как query cache ориентируется на TTL записи и может быть разным, но не более max-cache-ttl. Если используются бэкенды на основе ОЗУ (не требующие переключений контекста), то packet cache может быть даже вреден и лучше его отключить. 

Оставляем максимум два кеша. Какие именно?  

Dnsdist для нас — проксирующий узел, который должен просто передать пакет дальше, поэтому кеш здесь не используем. 

PDNS Recursor — кеш запросов — да, кеш пакетов — нет. Простой синтетический тест показал, что packet cache на бэкенде bind-zone профита не дает. Поэтому выключим его в pdns, но включим с небольшим TTL query cache. Он нам нужен, чтобы не сильно мучить тяжелый SQL-бэкенд, но и слишком длительным по времени он тоже быть не должен (по крайней мере в нашем случае), чтобы healthcheck сквозь SQL-бэкенд был достаточно актуальным. 

Приступим к настройке

Dnsdist на LoadBalancer (LB) узле

/etc/dnsdist/dnsdist.conf 

Ключевые моменты

--- алгоритм выбора бэкенда для поступившего запроса
setServerPolicy(roundrobin)


--- backends
newServer({
    pool="current_dc",
      ...
})

newServer({
    pool="fallback_dc",
      ...
})

Полный конфиг

--- добавляем в dns запрос source ip клиента - /32 чтобы сохранить весь ipv4 адрес
setECSSourcePrefixV4(32)

--- чтобы можно было подключится к работающему демону и что-нибудь с ним поделать
controlSocket("127.0.0.1:5199")
--- для безопасности
setKey("секретный_ключ")

--- webui для просмотра статистики в realtime 
webserver("0.0.0.0:8080")
setWebserverConfig({
  password="секретный_ключ_2",
  apiKey="",
  statsRequireAuthentication=false,
  acl="127.0.0.1, ::1, 10.0.0.0/8"
})

--- только чтение, нам нужны только метрики
setAPIWritable(false)

--- принимаем запросы на 53 порту на всех интерфейсах
setLocal("0.0.0.0:53")

--- политика выбора бэкенда для поступившего запроса
setServerPolicy(roundrobin)

  
--- backends. Для простоты оставим по одному бэкенду на ЦОД (1 в current_dc пуле, 2 в fallback_dc пуле)
newServer({
  --- адрес бэкенда
  address="10.0.0.1",
  --- имя бэкенда
  name="powerdns1",
  --- один из бэкендов пула "fallback_dc"
  pool="fallback_dc",
  --- проверка живости бэкенда
  checkClass=DNSClass.IN,
  checkName="status.backend.powerdns.lan.", 
  checkType="TXT",
  checkTimeout=2000,
  --- проверяем 3 раза
  maxCheckFailures=3,
  --- 3 секунды между попытками
  checkInterval=3,
  --- должны явно получить ответ, без ошибок
  mustResolve=true,
  --- добавляем ECS
  useClientSubnet=true,
  --- ждем 2 удачные попытки перед вводом бэкенда обратно в строй
  rise=2
})

newServer({
  address="10.0.0.2",
  name="powerdns2",
  pool="fallback_dc",
  checkClass=DNSClass.IN,
  checkName="status.backend.powerdns.lan.",
  checkType="TXT",
  checkTimeout=2000,
  maxCheckFailures=3,
  checkInterval=3,
  mustResolve=true,
  useClientSubnet=true,
  rise=2
})

newServer({
  address="10.0.0.3",
  name="powerdns3",
  pool="current_dc",
  checkClass=DNSClass.IN,
  checkName="status.backend.powerdns.lan.",
  checkType="TXT",
  checkTimeout=2000,
  maxCheckFailures=3,
  checkInterval=3,
  mustResolve=true,
  useClientSubnet=true,
  rise=2
})


--- если "current_dc" доступен (хотя бы один из его бэкендов), отправляем запросы в него
addAction(PoolAvailableRule("current_dc"), PoolAction("current_dc"))
--- если нет – отправляем запросы в "fallback_dc"
addAction(AllRule(), PoolAction("fallback_dc"))

Если настроить webserver, то можно посмотреть статистику по http://127.0.0.1:8080:

344bb2197e8900dbc801a15196966f56.pngИли её можно посмотреть из CLI:

lbdns1:~# echo "showServers()" | dnsdist -c 127.0.0.1:5100
#   Name                 Address            State     Qps    Qlim Ord Wt    Queries      Drops     Drate   Lat Outstanding Pools
0   powerdns1     10.0.0.1:53         up       0.0       0   1  1          0          0       0.0   0.0           0 fallback_dc
1   powerdns2     10.0.0.2:53         up       0.0       0   1  1     163994       3917       0.0   1.2           0 fallback_dc
2   powerdns3     10.0.0.3:53         up     971.8       0   1  1 5209759184  512719881      80.0  40.3        9603 current_dc
3   powerdns4     10.0.0.4:53         up     972.1       0   1  1 5261742931      17716       0.0  75.2          57 current_dc
4   powerdns5     10.0.0.5:53         up       0.0       0   1  1      23001          0       0.0   0.8           0 fallback_dc
5   powerdns6     10.0.0.6:53         up       0.0       0   1  1      23435          0       0.0   1.1           0 fallback_dc
All                                         1943.0                10471712545 512741514 

Что ещё, кроме балансировки, умеет dnsdist?  

Отбрасывать неугодные по содержанию запросы, кешировать запросы, лимитировать клиентов по DNS query и по IP, использовать в качестве фильтра eBPF для максимальной производительности. 

Если захотите включить packet cache в dnsdist:

pc = newPacketCache( 
  10000, --- записей в кэше 
    { 
    --- максимальный ttl 
    maxTTL=86400, 
    --- минимальный ttl для включения в кэш 
    minTTL=0, 
    --- кэшируем Server Failure or a Refused 
    temporaryFailureTTL=60, 
    ---  
    staleTTL=60, 
    dontAge=false 
  } 
) 
getPool(""):setCache(pc)

Кеш включается на каждый пул.» — пул по умолчанию. А с помощью setStaleCacheEntriesTTL () можно временно отдавать клиенту устаревший кеш, пока бэкенд не придёт в норму. 

BIRD на LoadBalancer узле

Сообщаем наш VIP-адрес, а точнее маршрут к нему (обязательно с префиксом /32). Сам VIP будет висеть на dummy-интерфейсе, а анонс префикса /32 будет убираться и добавляться в BGP через удаление и добавление маршрута /32. Всё это будем делать скриптами под управлением monit. 

/etc/bird/bird.conf

log syslog all;
router id 10.1.0.1;

filter z00_p32 {
  if net ~ [ 10.10.0.0/23{32,32} ] then accept;
  reject;
}

protocol device {
  debug { states,routes,filters,interfaces,events,packets };
}

protocol direct {
  disabled;
}

protocol kernel {
  learn;             # Learn all alien routes from the kernel
  persist;           # Don't remove routes on bird shutdown
  scan time 10;       # Scan kernel routing table every 2 seconds
  import filter z00_p32;
  export none;      # Export to protocol. default is export none
  graceful restart;  # Turn on graceful restart to reduce potential flaps in
}

template bgp bgp_template {
  debug { states,routes,filters,interfaces,events,packets };
  description "Connection to BGP peer";
  local as 4200000002;
  multihop;
  gateway recursive; # This should be the default, but just in case.
  add paths on;
  graceful restart;  # See comment in kernel section about graceful restart.
  connect delay time 2;
  connect retry time 5;
  error wait time 5,30;
  import none;
  # самое главное место
  export filter z00_p32;
  bfd on;
}
protocol bgp Node_10_1_1_1 from bgp_template {
  neighbor 10.1.1.1 as 4200000001;
  source address 10.1.0.1;
}
# bfd, чтобы быстрее детектить проблемы на сети
protocol bfd {
  interface "eth*" {
    min rx interval 100 ms;
    min tx interval 100 ms;
    idle tx interval 300 ms;
    multiplier 10;
  };
  multihop {
    interval 200 ms;
    multiplier 10;
  };
  import none;
  export filter z00_p32;
  neighbor 10.1.1.1 local 10.1.0.1 multihop;
}

Monit на LoadBalancer узле

Давайте настроим систему так, чтобы трафик снимался с машины, если мы не можем получить успешный ответ от локального Dnsdist, или когда инженер выполняет регламентные работы. 

Алгоритм простой:  

  1. Выполняем healthcheck (обычный DNS-запрос к локальному Dnsdist) и проверяем наличие файла /var/lib/vip0.down.

  2. Снимаем анонс (удаляем маршрут к vip0), если результат healthcheck неуспешен или существует /var/lib/vip0.down.

  3. Добавляем анонс (добавляем маршрут к vip0), если healthcheck успешен и отсутствует файл /var/lib/vip0.down.

Скрипт проверки живости:   

/usr/local/bin/dns_healthcheck_vip0.sh 

#!/bin/bash

dummy_iface=vip0
announce_ip=10.50.0.1

check_record=status.backend.powerdns.lan.
check_type=TXT
check_content=\"up\"

# check1
if [ -e "/var/lib/${dummy_iface}.down" ]; then
  echo "file /var/lib/${dummy_iface}.down is exist"
  exit 1
fi

# check2
_result=$(dig +short +timeout=3 +tries=2 @127.0.0.1 -t ${check_type} ${check_record})
if [ "${_result}" != "${check_content}" ]; then
  echo "failed to check record ${check_record}"
  exit 22
fi

echo OK

Снимаем анонс маршрута /32:  

/usr/local/bin/bgp_withdraw_vip0.sh

#!/bin/bash

dummy_iface=vip0
announce_ip=10.50.0.1

_result=$(ip route show ${announce_ip})
if [ -n "${_result}" ]; then
  ip route delete ${announce_ip}/32 && echo OK
else
  echo "NOTHING TO DO: route ${announce_ip}/32 not exists"
fi

Возвращаем анонс маршрута /32:

/usr/local/bin/bgp_announce_vip0.sh

#!/bin/bash

check_vip () {
  iface=${1}
  ip=${2}
  if [[ "${ip}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
    ip -brief a | grep "^${iface} " | grep " ${ip}/32 " | grep -q -v " DOWN "
    e_code=$?
    if (( e_code != 0 )); then
      echo "Interface \"${iface}\" is not UP or IP \"${ip}\" is not match ip format"
      exit 0
    fi
  fi
}

check_maintenance () {
  if [ -e "/var/lib/${dummy_iface}.down" ]; then
    echo "file /var/lib/${dummy_iface}.down is exist"
    exit 0
  fi
}


dummy_iface=vip0
announce_ip=10.50.0.1

check_maintenance
check_vip ${dummy_iface} ${announce_ip}

_result=$(ip route show ${announce_ip})
if [ -z "${_result}" ]; then
  ip route add ${announce_ip}/32 dev ${dummy_iface} && echo OK
else
  echo "NOTHING TO DO: route ${announce_ip}/32 already exists"
fi

Осталось передать это всё под управление какого-нибудь супервизора. Старый добрый monit должен с этим справиться. 

/etc/monit/monitrc

set daemon 10
set log /var/log/monit.log
set idfile /var/lib/monit/id
set statefile /var/lib/monit/state
set eventqueue
basedir /var/lib/monit/events
slots 100 
check program dns_healthcheck_vip0.sh
    with path /usr/local/bin/dns_healthcheck_vip0.sh
    with timeout 8 seconds
    if status != 0 for 3 cycles then
        exec /usr/local/bin/bgp_withdraw_vip0.sh
        repeat every 5 cycles
    if status == 0 for 2 cycles then
        exec /usr/local/bin/bgp_announce_vip0.sh
        repeat every 5 cycles

PDNS Recursor на узле PowerDNS

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

/etc/powerdns/recursor.conf

allow-from=0.0.0.0/0, 127.0.0.0/8, 10.0.0.0/8, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, ::1/128, fc00::/7, fe80::/10
api-key=секретный_апи_кей
config-dir=/etc/powerdns
forward-zones-file=/etc/powerdns/recursor-forward.zones
hint-file=/usr/share/dns/root.hints
include-dir=/etc/powerdns/recursor.d
local-address=0.0.0.0
local-port=53
disable-packetcache=yes
#packetcache-ttl=5
#packetcache-servfail-ttl=5
max-cache-ttl=86400
max-negative-ttl=10
quiet=no
security-poll-suffix=
setgid=pdns
setuid=pdns
webserver=yes
webserver-address=0.0.0.0
webserver-password=вебсервер_пароль
webserver-port=8082
dnssec=off
# а здесь будем делать всякие интересные вещи, типа контролиовать maintenance режим и беспардонно модифицировать пролетающие запросы и ответы 
lua-dns-script=/etc/powerdns/recursor.lua.d/main.lua
lua-maintenance-interval=5
trace=fail
loglevel=5
ecs-ipv4-bits=32
use-incoming-edns-subnet=yes

Для удобства объединяем все Lua-вставки в один файл:

/etc/powerdns/recursor.lua.d/main.lua

function fileExists(file)
  local f = io.open(file, "rb")
  if f then
    f:close()
    return true
  end
  return false
end


dofile("/etc/powerdns/recursor.lua.d/maintenance.lua")
dofile("/etc/powerdns/recursor.lua.d/dns.lua")

Далее рассмотрим код процедуры maintenance. Наличие файла включает режим обслуживания, отсутствие — выключает. Физически это заключается в проверке наличия файла и присвоении булевой переменной maintenanceState значения true или false. 

/etc/powerdns/recursor.lua.d/dns.lua

healthcheckSet = newDS()
healthcheckSet:add{"status.backend.powerdns.lan"}

function preresolve(dq)
  if healthcheckSet:check(dq.qname) then
    dq.variable = true                      -- disable packet cache. Это нужно если у вас ключен packet cache в recursor
    if maintenanceState == true then
      dq.rcode = pdns.NXDOMAIN
      return true;
    end
  end

  return false;
end

Полный конфиг

healthcheckSet = newDS()
healthcheckSet:add{"status.backend.powerdns.lan"}

backendname_request = "name.backend.powerdns.lan"
backendname_response = "\"powerdns1.lan\""

function preresolve(dq)
  if healthcheckSet:check(dq.qname) then
    dq.variable = true                      -- disable packet cache. Это нужно если у вас ключен packet cache в recursor
    if maintenanceState == true then
      dq.rcode = pdns.NXDOMAIN
      return true;
    end
  end

  if dq.qname:equal(backendname_request) then
    dq.variable = true                      -- disable packet cache
    dq.rcode=0                              -- make it a normal answer
    dq:addAnswer(pdns.TXT, backendname_response, 1)     -- ttl 1s
    return true
  end

  return false;
end

Файл форварда зон:

/etc/powerdns/recursor-forward.zones

# forward zones
h.lan=127.0.0.1:5300
s.lan=127.0.0.1:5300
powerdns.lan=127.0.0.1:5300 

# forward zones recurse
+k8s.lan=10.2.0.1, 10.2.0.2
+example.net=9.9.9.9 , 8.8.8.8:53, 8.8.4.4 
+.=10.9.0.1

# Где 127.0.0.1:5300 это pdns auth

PDNS Auth на узле PowerDNS

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

/etc/powerdns/pdns.conf

allow-axfr-ips=127.0.0.0/8, ::1
api=yes
api-key=секрет_ари_кей
include-dir=/etc/powerdns/pdns.d  # подключаем BIND и PG бэкенды
launch=
local-address=127.0.0.1,10.1.2.1
local-port=5300
cache-ttl=0
negquery-cache-ttl=10
query-cache-ttl=3
security-poll-suffix=
setgid=pdns
setuid=pdns
webserver=yes
webserver-address=0.0.0.0
webserver-allow-from=127.0.0.1,10.0.0.0/8
webserver-password=вебсервер_пароль
webserver-port=8081
enable-lua-records=yes  # это чтобы работали LUA RR в бэкенде
primary=no              # дальше будет понятно почему
secondary=no            # дальше будет понятно почему
xfr-cycle-interval=60
only-notify=
also-notify=
loglevel=5

Подключение плагинов bind и gpgsql:

/etc/powerdns/pdns.d/40_bind.conf

launch+=bind
bind-config=/etc/powerdns/backend_bind/zone_bind.conf
bind-supermaster-config=/var/lib/powerdns/supermaster.conf
bind-supermaster-destdir=/var/lib/powerdns/zones.slave.d

/etc/powerdns/pdns.d/20_gpgsql.conf

launch+=gpgsql
gpgsql-host=127.0.0.1
gpgsql-port=5432
gpgsql-dbname=powerdns
gpgsql-user=powerdns
gpgsql-password=пароль_к_бд
gpgsql-dnssec=yes

Здесь мы подключили плагины bind и gpgsql. 

Может показаться хорошей идеей прописать одну и ту же зону с разным набором записей в разных бэкендах и получить выгоду от использования обоих. Например, нам было бы удобно работать с простым и понятным всем по формату bind-файлом, но в некоторых случаях всё-таки подключать записи из GeoIP-бэка (то есть отдавать разный набор записей в зависимости от source IP клиента, просто потому что там это делать очень удобно. 

Так вот, забудьте про это. Архитектура бэкендов на это не рассчитана. Да, технически вы можете разместить одну и ту же зону с разным или идентичным набором записей в разных бэкендах, и PDNS Auth это проглотит без ошибок, но порядок просмотра бэкендов в этом случае неочевиден. Зону из какого бэка PDNS Auth возьмёт в качестве эталонной? Ответ знает только сам PDNS Auth. 

Может поиграться с опцией launch=bind в gpgsql, меняя очерёдность подгрузки бэкендов. В этом случае бэки действительно загрузятся в том порядке, в котором они перечислены в опции launch, но, опять же, это никак не повлияет на то, в какой бэк PDNS Auth пойдёт за зоной. 

Пример:  

  1. берём bind- и geoip-бэки;

  2. заполняем одинаковым по количеству набором записей, но с немного отличающимся содержимым (чтобы понимать, из какого бэка пришёл ответ);

  3. делаем запрос;

  4. получаем содержимое из geoip-бэка;

  5. меняем местами бэки в launch;

  6. опять получаем содержимое из geoip-бэка;

  7. меняем serial в большую и в меньшую сторону;

  8. снова получаем контент из geoip-бэка;

  9. добавляем новую запись только в bind-бэк;

  10. ищем по ней

  11. получаем NXDOMAIN;

  12. удаляем SOA из geoip;

  13. получаем REFUSED. 

А вот с парой bind + gpgsql было чуть иначе: при именовании конфигурационных файлов 40_gpgsql.conf и 20_bind.conf (которые подключались через include-dir=/etc/powersns/pdns.d).

Вывод: каждая зона должна быть определена только один раз в одном из бэкендов PDNS Auth. 

Теперь донастроим оба бэка и добавим пару ресурсных записей.

Настроим кластер PostgreSQL. Процесс настройки связки master –> replica тривиален, поэтому останавливаться здесь не будем. Гораздо интереснее подъём полноценного HighAvailability (HA)решения, например Patroni. В Ozon по нему отличная компетенция, поэтому сюрпризов в эксплуатации быть не должно. Однако есть небольшое отличие от стандартных кластеров под микросервисы: все необходимые для HA компоненты (PostgreSQL, Patroni, etcd) будут располагаться на тех же машинах, что и PDNS Auth. Сделано это для того, чтобы запросы к БД были максимально быстрыми, а отдельные компоненты, такие как etcd, не использовались совместно с другими сервисами, никак не связанными с DNS, дабы избежать влияния от них.

Небольшой оффтоп. Наш текущий кластер etcd-Patroni (тот, который под микросервисы, а не тот, который под DNS), обслуживает уже 3,7 тыс. Patroni-кластеров и 11 тыс. отдельных экземпляров PostgreSQL в production.

Конфиг Patroni будет примерно такой:

/etc/patroni/patroni.yml

---
name: stgpowerdns1.h.lan
namespace: /service/
scope: powerdns_multidc
restapi:
  listen: 0.0.0.0:8008
  connect_address: 10.40.0.11:8008
  authentication:
    username: patroni
    password: "somepassword"
bootstrap:
  dcs:
    ttl: 60
    loop_wait: 10
    retry_timeout: 20
    maximum_lag_on_failover: 1048576
    master_start_timeout: 60
    synchronous_mode: True
    postgresql:
      use_pg_rewind: True
      use_slots: True
      parameters:
        wal_level: logical
        hot_standby: 'on'
        wal_keep_segments: 100
        max_wal_senders: 24
        max_replication_slots: 30
        wal_log_hints: 'on'
        max_connections: '100'
        max_locks_per_transaction: '64'
  initdb: ['encoding=UTF8', 'data-checksums', 'auth-local=trust', 'locale=en_US.UTF-8', 'auth-host=md5', 'auth-local=trust']
  pg_hba:
    - 'local all all trust'
    - 'local replication all trust'
    - 'host replication replication 127.0.0.1/32 trust'
    - 'host replication replication 127.0.0.1/32 md5'
    - 'host all all 127.0.0.0/8 md5'
    - 'local all all trust'
    - 'host replication replication 10.40.0.11/32 trust'
    - 'host replication replication 10.40.0.12/32 trust'
    - 'host replication replication 10.40.0.13/32 trust'
    - 'host replication replication 10.40.0.14/32 trust'
    - 'host replication replication 10.40.0.15/32 trust'
postgresql:
  basebackup:
    {'max-rate': '1000M', 'checkpoint': 'fast'}
  listen: 0.0.0.0:5432
  connect_address: 10.40.0.11:5432
  data_dir: "/data/postgresql"
  config_dir: "/etc/postgresql/12/main"
  bin_dir: "/usr/lib/postgresql/12/bin"
  use_unix_socket: True
  parameters:
    unix_socket_directories: "/var/run/postgresql"
  authentication:
    replication:
      username: replication
      password: "somepassword2"
    superuser:
      username: postgres
      password: "somepassword3"
etcd: # отказоустойчивое хранилище для хранения состояния кластера 
  protocol: https
  cert: "/etc/patroni/ssl/patroni.pem"
  key: "/etc/patroni/ssl/patroni-key.pem"
  cacert: "/etc/patroni/ssl/ca.pem"
  hosts: "10.40.0.11:2379,10.40.0.12:2379,10.40.0.13:2379,10.40.0.14:2379,10.40.0.15:2379"
watchdog:
  mode: "off"
  device: "/dev/watchdog"
  safety_margin: 5
tags:
  noloadbalance: False
  nofailover: False
  clonefrom: False
  nosync: False

После сетапа БД…

Импортируем схему данных (таблицы, индексы и т.д.). Для PostgreSQL: https://doc.powerdns.com/authoritative/backends/generic-postgresql.html#default-schema 

Теперь можно создать пару зон и наполнить их записями.

Зона powerdns.lan

Нужна для правильной работы механизма healthchecks. Dnsdist постоянно запрашивает у бэкенда status.backend.powerdns.lan текстовую запись и тем самым прозрачно проверяет работоспособность базы данных на каждом PowerDNS узле. Чтобы healthcheck был более-менее актуален, отключаем paсket cache на рекурсоре, выставляем TTL записи в 1 секунду и кеш запроса в 3 секунды (чтобы не сильно мучить PostgreSQL).

Поскольку в зону powerdns.lan мы никогда никакие изменения не вносим, то вероятность накосячить, удалив или изменив ключевую запись status.backend.powerdns.lan, существенно снижается.

Создаём зону в БД: pdnsutil create-zone powerdns.lan ns1.powerdns.lan.

Загляните под 

© Habrahabr.ru