Перейти к основному содержимому

Балансировка DNS-трафика и активные health-check'и апстримов — как и почему мы начали использовать DNSdist

· 10 мин. чтения
Иван Давыдов
Иван Давыдов
SRE в Sberdevices

Коллеги, всем привет!

Долгое время в нашей внутренней сети для обработки DNS-трафика мы использовали только BIND, и нам с ним было хорошо. Но в какой-то момент его возможностей перестало хватать. В статье расскажу, что именно с BIND не так и почему теперь весь DNS-трафик у нас проходит через DNSdist. И что это вообще такое...

thumbnail

к сведению

Ссылка на статью на хабре – https://habr.com/ru/companies/sberdevices/articles/966638/

Жизнь с BIND

life-with-bind

Прежде чем говорить о DNSdist, нужно знать, почему мы вообще начали в его сторону думать. А для этого необходимо понимать, как была устроена наша DNS-инфраструктура до его прихода. Итак, начнём.

Мы живём в трех разных облаках. В каждом из них у нас поднято по два DNS-сервера BIND, которые реплицируются от «мастера», который также поднят в одном из этих облаков. Все сервера хранят одни и те же зоны, одну и ту же информацию.

Клиентами этих серверов у нас выступают все наши виртуальные машины, АРМ и K8S кластера. Для отказоустойчивости на всех ВМ мы указываем DNS-сервера из всех облаков. Например: ВМ из облака А будет в /etc/resolv.conf иметь DNS-сервера из облаков A, B и C. Никакого DNS-клиента (типа systemd-resolved) на этих ВМ не установлено (к сожалению), поэтому DNS-запросы летят вразнобой. Это станет нашей проблемой номер ноль.

В конфигурации самих BIND'ов у нас, помимо наших зон, в которые мы записываем адреса виртуальных машин, сервисов, балансиров и так далее, есть зоны, которые, по сути, обслуживаются не нами, а, к примеру, провайдерами облачных сервисов, которые мы используем.

zone zona IN {
type forward;
forward only;
forwarders { 172.17.20.22; 172.17.20.23;};
};

Тут у нас появляется одна, но серьёзная проблема – умершие апстримы не выпадают из балансировки. Это значит, что если один из серверов, который мы указали в списке forwarders, упадет, BIND продолжит пытаться слать ему запросы. Это нам не нравится, клиенты начинают получать лишние timeout'ы, а лучше бы не получали.

Помимо этого, у некоторых апстримов есть специфичная особенность – они по-хорошему должны обслуживать только DNS-запросы тех клиентов, которые находятся с ними в одном и том же облаке. К таким апстримам у нас, например, относятся Consul (решение от компании HashiCorp) сервера, которые возвращают клиенту адреса живых инстансов запрашиваемого сервиса. Всё, что нам нужно, это, по сути, редиректить DNS-запрос на нужный DNS-сервер (который указан в forwarders) в зависимости от ip-адреса клиента и при этом иметь возможность указать fallback апстримы. К сожалению, распределение запросов по форвардам, основываясь на source ip в BIND, невозможно, и это наша последняя проблема.

Выход на сцену DNSdist

Теперь давайте поговорим наконец-то о том, что же такое DNSdist. Если по-простому, то это Load Balancer для DNS-запросов. Он не содержит информации о зонах (кроме той, что закешировал), он просто берёт запрос и перенаправляет его самому подходящему, основываясь на политике балансировки, DNS-серверу (если предыдущий ответ на такой же запрос не закешировался или кеш «протух»).

Конфигурацию DNSdist принимает либо в формате Lua-скрипта, либо в формате обычной и всем знакомой YAML-конфигурации. В нашем понимании Lua намного гибче, чем YAML (так как это, блин, скриптовый язык), поэтому, если инструмент из коробки даёт возможность сконфигурировать себя чем-то таким, то надо хотя бы попробовать! Мы попробовали, и получилось очень даже неплохо :).

По конфигурации важно отметить один не очень приятный момент: DNSdist не может «на ходу» перечитать конфигурацию из файла, который вы ему указали. То есть нет никакого systemctl reload dnsdist, и вы не можете отправить ему SIGHUP, чтобы он что-то там перечитал в своей конфигурации (однако это не значит, что вы не можете заставить dnsdist переоткрыть файл, в который он пишет логи. Об этом будет ниже).

Но это не означает, что вы вообще не можете настраивать DNSdist без перезагрузки. DNSdist имеет встроенный веб-сервер, который помимо того, что может выводить метрики сервера в Prometheus формате и отображать их на простенькой веб-страничке (скриншот ниже), также позволяет частично манипулировать конфигурацией. Но под манипуляцией, к сожалению или к счастью, имеется в виду только сброс кеша и добавление адресов, которые могут обращаться к серверу.

dnsdist-webui

Ещё вы можете вносить изменения в конфигурацию через консольный доступ к DNSdist. Всё, что вам нужно сделать — это прописать вот эти две строчки в конфиге:

controlSocket('127.0.0.1:8899')
setKey("ENCODED KEY")

И дальше вот таким образом подключиться к консоли: dnsdist -k "ENCODED KEY" -c 127.0.0.1:8899, если вы не рут, и без указания ключа, если выполняете команду от рута. Это не очень удобно, и нормально такое автоматизировать будет достаточно геморройно, но это можно использовать, как ручник, когда вам ну вот вообще нельзя ребутать DNSdist, а внести изменения в конфигурацию надо. Подробнее об этом тут.

Если вам нужно менять конфигурацию периодически, и факторы, по которым вам нужно это делать, вы знаете, а такжесчитаете, что можете легко описать их в коде, то DNSDist предоставляет вам следующие возможности: создавать динамические блоки временных IP-адресов, на которые вы сможете «завязаться» в своих политиках, описывать какой-либо код в функции maintenance(), которая запускается автоматически раз в секунду, использовать блоки динамических правил и так далее. Вы можете написать почти любую логику для динамического изменения политик балансировки. Например, сделать так, чтобы в случае, если часть серверов в одном пуле вдруг начала периодически отдавать ServFail, перевести трафик на другой пул. Главное, чтобы ваши Lua-функции работали достаточно быстро и не блокировали выполнение других. Мы такими вещами у себя не пользуемся, наша конфигурация достаточно проста.

Наша конфигурация DNSdist

Давайте теперь посмотрим, на то как выглядит наша конфигурация DNSdist. Полностью, конечно, не покажем, но самые интересные моменты подсветим :).

Начинается всё с файла dnsdist.conf, который и указывается при запуске DNSdist.

dnsdist.conf
-- Импорт других файлов конфигурации
dofile("/etc/dnsdist/preps.lua")
dofile("/etc/dnsdist/secrets.lua")
dofile("/etc/dnsdist/pools.lua")
dofile("/etc/dnsdist/logging.lua")
dofile("/etc/dnsdist/pool_actions.lua")

-- Политика балансировки по умолчанию
setServerPolicy(roundrobin)

-- Сокет для DNS принятия запросов
setLocal('0.0.0.0:53')

-- Настройка сокета для изменения конфигурации из терминала
controlSocket('127.0.0.1:8899')
setKey(terminalPass)

-- Настройка веб сервера
webserver("0.0.0.0:8080")
setWebserverConfig({
password=webServerPass,
apiKey=webServerPass
})

Из интересного в этом файле:

  1. Возможность импорта других файлов конфигураций, которую предоставляет Lua.
  2. Переопределение дефолтной политики балансировки трафика.

По первому пункту, я думаю, всё понятно, а вот про второй давайте поподробнее. По дефолту для всех пулов апстримов используется политика leastOutstanding, которая отправляет запрос серверу либо с меньшим количеством запросов «в очереди», либо, если таких нет, то серверу с наименьшим порядковым значением, либо, если и таких нет, то с наименьшим средним latency. Нам такая стратегия не подходила, потому что при ней у нас в каждом пуле большая часть запросов летела в один сервер (потому что у него на 1-2мс latency был ниже, чем у других). Поэтому мы решили использовать для балансировки стандартный roundrobin. Подробнее про политики балансировки можно почитать тут.

Далее у нас идет preps.lua файл, в нём лежат константы и функция создания новых пулов.

preps.lua
geoProdZone = "geo.zona."

-- Селектор запросов определённой зоны
geoProdZoneSelector = QNameSuffixRule(geoProdZone)

-- Селектор запросов только от определённых source ip
cloud1SubnetsSelector = OrRule{
NetmaskGroupRule("172.18.0.0/16"),
NetmaskGroupRule("172.19.0.0/16"),
NetmaskGroupRule("172.20.0.0/16")
}

function newPool(options)
for i, v in ipairs(options.servers) do
local server = {
address=v.address,
name=v.name,
-- Основной пул для сервера
pool=options.primaryPool,
checkType="SOA",
--- Сервер обязан ответить что-то, что не NXDomain, ServFail или Refused чтобы пройти хелс чек
mustResolve=true,
--- Выводим сервер из балансировки после трех неудачных проверок
maxCheckFailures=3,
-- Две успешные проверки чтобы ввести сервер обратно в балансировку
rise=2
}

server.checkName = options.zone

local newServer = newServer(server)
-- Определяем вторичный пул для сервреа
if options.secondaryPool then
newServer:addPool(options.secondaryPool)
end

end
end

В целом для констант всё описано в комментариях, поэтому на них не будем заострять внимание. С функцией чуть поинтереснее. DNSdist из коробки даёт функцию на создание нового сервера (по сути, на определение апстрима), но использовать только её нам не очень понравилось из-за дублирования кода, поэтому пришлось смастерить свою небольшую функцию newPool, которая и определяет сервер с правильными хелсчеками, и добавляет его в основной пул и вторичный (если такой передан).

Теперь посмотрите, как удобно ей создавать пулы апстримов в файле pools.lua.

pools.lua
newPool({
servers = {
{address="172.23.1.2", name="cloud1_prod_consul1"},
{address="172.23.1.3", name="cloud1_prod_consul2"},
{address="172.23.1.4", name="cloud1_prod_consul3"}
},
-- Передаём основной пул для серверов
primaryPool = "consul_prod_cloud1",
-- Передаём вторичный пул для серверов
secondaryPool = "consul_prod_default",
-- Передаём зону для корректных хелсчеков
zone = consulProdZone
})

Далее у нас идет небольшой, но интересный файлик logging.lua.

logging.lua
function forLogsSelector(dq)
return not (
dq.qname:isPartOf(newDNSName("domain1.ru")) or
dq.qname:isPartOf(newDNSName("domain2.ru"))
) and dq.qname:countLabels() > 1
end

addAction(
LuaRule(forLogsSelector),
LogAction(
-- Имя файла, куда писать лог
"/var/log/dnsdist/queries.log",
-- Бинарный формат логов
false,
-- Добавлять (append) строчки логов к файлу или каждый раз очищать его
true,
-- Буферизация для логов перед записью в файл
false
)
)

В нём мы настраиваем логгирование запросов к DNSdist-серверу. Само логгирование включается через добавление действия LogAction, которое записывает логи в файл. Подробнее про действия поговорим в следующем файле. В этом же для нас основной интерес представляет функция forLogsSelector, которая по сути обозначает запросы каких доменов нужно логгировать, а каких нет. В наши DNS-сервера летит куча запросов доменов, о которых нам не интересно знать, что их кто-то когда-то запрашивал, и, соответственно забивать этим еластик не очень хочется. И наоборот, есть домены, про резолв которых интересно было бы знать (в основном безопасникам). Поэтому в этом файлике мы отсекаем лог запроса большого количества ненужных зон и оставляем только те, которые нам нужны.

Если вы ротейтите логфайл (меняете текущему имя и создаете рядом новый), вам надо сказать DNSdist, чтобы он файлик переоткрыл. Для этого нужно воспользоваться терминалом и следующим образом «релоаднуть» LogAction:

echo "getAction(0):reload()" | dnsdist -c 127.0.0.1:8899 

Только учтите, что такой вызов работает только в случае, если действие с LogAction идёт в вашем списке первым. О порядке выполнение действий чуть ниже.

Ну и последнее – это файлик с действиями: pool_actions.lua.

pool_actions.lua
-- consul cloud 1 action
addAction(AndRule{
cloudOneSubnetsSelector,
consulDevZoneSelector,
PoolAvailableRule("consul_dev_cloud1")
}, PoolAction("consul_dev_cloud1"))

-- consul fallback action
addAction(AndRule{
consulDevZoneSelector,
PoolAvailableRule("consul_dev_default")
}, PoolAction("consul_dev_default"))

-- default fallback
addAction(AllRule(), PoolAction("bind_default"))

Тут важно немного объяснить, как работают действия (actions).

  • Во-первых, синтаксис у них следующий: первым аргументом идет селектор, который выбирает, на какие запросы распространяется действие. Вторым идет само действие (есть ещё третий аргумент – хешмапа с опциями, но мы ей не пользуемся).
  • Во-вторых, последовательность, в которой вы эти действия в коде прописываете, имеет значение. То есть если вы самым первым определили действие с селектором AllRule, то никакие действия дальше работать не будут просто потому, что первое действие по итогу будет обрабатывать все запросы. Исключениями тут являются действия, которые выполняются и пропускают запрос дальше – например, LogAction, про который говорили выше.
  • В-третьих, вы можете менять последовательность действий на запущенном сервере DNSdist с помощью терминала, про который я говорил выше.

Полный список селекторов и действий можно найти тут и тут.

В нашем файле сверху у нас три действия.

  • Первое отбирает только запросы от клиентов из облака Cloud1 и «нацеленные» на DEV зону consul. В случае, если dev консулы в Cloud1 живы, мы перенаправим туда трафик. Если хотя бы одно из условий не истинно, действие не выполнится, и запрос пойдет к следующему.
  • Второе – это уже фолбек для консулов. Оно проверяет, что пользователь запрашивает DEV зону consul и в дефолтном пуле консулов (то есть где находятся все консулы из всех облаков) есть хотя бы один живой сервер.
  • Ну и последнее – это фолбек для всех действий: если ничего выше не сработало, то трафик пойдет на дефолтные BIND-сервера.

Итог

Подытожим: DNS-сервис, который мы предоставляем нашим клиентам, стал работать надёжнее после внедрения DNSdist за счёт его хелсчеков для апстрим DNS-серверов и гибкой настройки политик распределения DNS-трафика. Мы продолжим сопровождать DNSdist и в будущем, возможно, даже включим у него кеширование запросов на некоторых пулах для увеличения производительности.

Спасибо за чтение!