- PVSM.RU - https://www.pvsm.ru -
Сегодня я расскажу о том, как быстро собрать отказоустойчивый кластер с балансировкой нагрузки с помощью keepalived на примере DNS-серверов.
Итак, предположим, что у нас есть сервер, который должен работать без перебоев. Его нельзя просто так взять и выключить посреди рабочего дня — клиенты или пользователи не поймут. Тем не менее любой сервер время от времени надо обслуживать, ставить обновления, менять аппаратную конфигурацию или сетевые настройки. Кроме того, нужно быть готовым к росту нагрузки, когда мощности сервера перестанет хватать. У сервера должна быть предусмотрена возможность быстрого масштабирования. Если вы озаботились этими проблемами, то вам нужно организовать отказоустойчивый кластер.
Чаще всего такие задачи приходится делать с HTTP-серверами. Я сегодня покажу сборку кластера на примере DNS, потому что опробовал технологию именно на нем, но с минимальными изменениями то же самое можно сделать и с серверами, работающими по TLS или HTTP.
В принципе, DNS задуман так, чтобы необходимости городить огород с кластерами не возникало. В каждой зоне можно прописать множество NS-записей, в каждой сети можно раздать список DNS-серверов с помощью DHCP. DNS-сервера умеют реплицировать зоны, поэтому они хорошо масштабируются. Однако на практике это плохо работает. Когда я попробовал добавить 2-й DNS-сервер, то обнаружил, что
Если долгое время в сети используется DNS-сервер с определенным IP, то он оказывается прописан в десятках разных мест. Например:
Никакими плейбуками и DHCP-декларациями это все обновить просто нереально. Поэтому было бы очень хорошо, если бы собираемый кластер можно было повесить на 1 уже существующий IP. Тогда для пользователей ничего не изменится, и софт перенастраивать не понадобится.
Поэтому я выбрал решение с keepalived и протоколом VRRP [1].
Собирая кластер из нескольких серверов, желательно уметь их различать на стороне клиента, чтобы не бегать искать по логам серверов, какой запрос, куда попал. Я для этого в bind9 завел вот такую зону.
$ORIGIN load.balance.
$TTL 1h
load.balance. 86400 IN SOA ns.mydomain.ru. dnsmaster.mydoamin.ru. (
1603065098 3600 1800 604800 30
)
load.balance. IN NS ns.mydomain.ru.
health IN TXT =nameserver-1=
На 1-м сервере ресурсная запись TXT для health.load.balance содержит текст =nameserver-1=, на 2-м сервере — =nameserver-2=, и т. д. Таким образом, отправляя запрос в кластер, я по ответу могу определить, какой сервер мне ответил, что очень удобно для отладки.
Если у вас HTTP-сервер, то поместите эту информацию в HTTP-заголовок. Например, для nginx я использую вот такую директиву
add_header serega-trace "$hostname" always;
Убедитесь, что ваши сетевые файерволлы не блокируют IP-трафик протокола 112 по адресу 224.0.0.18. Этот адрес будут использовать ваши сервера, чтобы договариваться между собой о том, кто из них MASTER, а кто BACKUP.
iptables -t filter -I INPUT -p vrrp -d 224.0.0.18 -j ACCEPT
iptables -t filter -I OUTPUT -p vrrp -d 224.0.0.18 -j ACCEPT
При организации DNS нужно выделить мастер-сервер, на который не будут идти клиентские запросы. DNS-мастер используется только для управления. Он реплицирует свои зоны на slave-сервера, которые уже видны пользователям, и именно на них идет нагрузка. Далее речь будет идти о кластеризации именно DNS-slave.
Если вам просто достаточно резервирования на случай аварии, то рекомендую рассмотреть самый быстрый и простой вариант. 2 одинаковых сервера разделяют между собой общий виртуальный IP (далее буду называть его VIP). У кого в данный момент в сетевом интерфейсе прописан VIP, тот сервер и работает. 2-й сервер следит за мастером (имеется в виду мастер VRRP, а не DNS), и как только обнаруживает, что он перестал вещать, сразу объявляет мастером себя и поднимает VIP на своем сетевом интерфейсе.
Меня приятно удивило то, как быстро удалось поднять кластер по такому варианту. Несмотря на то, что раньше я никогда не имел дело с keepalived, уже через час все работало. Так что если вам нужно решить задачу быстро, то уровень 1 вам подойдет в самый раз.
Для успешного развертывания вам понадобится собрать следующую информацию. Здесь я привожу значения для примера. У вас эти значения должны быть свои.
Параметр | Возможное значение | Описание |
---|---|---|
vip | 10.2.1.5 | виртуальный IP, на который шлют запросы клиенты |
dev0 | eth0 | 1-й сетевой интерфейс на узлах кластера |
ip01 | 10.2.1.2 | IP 1-го узла кластера на 1-м сетевом интерфейсе |
ip02 | 10.2.1.3 | IP 2-го узла кластера на 1-м сетевом интерфейсе |
net0 | 10.2.1.0/24 | подсеть, которой принадлежат ip01 и ip02 |
Установите keepalived и snmpd. SNMP ставить необязательно, но мне кажется, что, если серверов с виртуальными IP будет много, он будет полезен.
setenforce 0 # если вдруг у вас selinux
dnf install -y keepalived nmap-ncat net-snmp net-snmp-utils
systemctl enable keepalived
systemctl enable snmpd
Netcat нужен для диагностики и healthcheck-ов.
В файл /etc/sysconfig/keepavlied добавьте опцию -x. Она нужна для взаимодействия keepalived с snmpd. Если вы не собираетесь поднимать SNMP, то этот шаг можете пропустить.
Отредактируйте файл /etc/snmp/snmpd.conf следующим образом:
master agentx
rocommunity public
В /etc/keepalived положите вот такой скрипт keepalived-notify.sh:
#!/bin/sh
umask -S u=rwx,g=rx,o=rx
exec echo "[$(date -Iseconds)]" "$0" "$@" >>"/var/run/keepalived.$1.$2.state"
Этот скрипт будет вызываться демоном keepalived при изменении состояния кластера. Он запишет в каталог /var/run статусный файл, который удобно использовать для диагностики.
Отредактируйте основной конфигурационный файл keepalived.conf следующим образом:
global_defs {
enable_script_security
}
vrrp_script myhealth {
script "/bin/nc -z -w 2 127.0.0.1 53"
interval 10
user nobody
}
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 5
priority 100
advert_int 1
nopreempt
notify /etc/keepalived/keepalived-notify.sh root
authentication {
auth_type PASS
auth_pass KPSjXfRG
}
virtual_ipaddress {
10.2.1.5
}
track_script {
myhealth
}
}
Блок global_defs содержит единственную необходимую настройку enable_script_security, которая по умолчанию отключена.
Блок vrrp_script описывает скрипт, который демон keepalived будет использовать для определения работоспособности своего сервера. Если этот скрипт вернет ошибку, то демон перейдет в состояние FAIL, и не будет претендовать на роль MASTER. В этом же блоке описывается периодичность выполнения healthchek-ов и указывается пользователь, от имени которого запускается скрипт. В нашем случае используется утилита netcat, которая устанавливает соединение c локальным DNS-сервером по TCP-порту 53. Можно использовать разные проверки, например, прозвонить UDP-порт 53 утилитой dig.
В блоке VRRP_INSTANCE задаются настройки 1 экземпляра сервера с виртуальным IP.
Выполните указанные настройки на обоих серверах кластера и запустите сервисы:
systemctl start snmpd
systemctl start keepalived
Проверьте логи и статусные файлы на наличие ошибок.
journalctl -u snmpd
journalctl -u keepalived
tail /var/run/keepalived.INSTANCE.VI_1.state
Если у вас используется selinux, не забудьте по данным audit.log обновить политики и вернуть enforcing mode командой set enforce 1.
Если в логах ошибок нет, можно проверять. Поскольку мы кластеризовали DNS, то будем использовать dig. Для проверки HTTP-сервера подойдет curl.
Запустите на клиенте такой скрипт:
while true; do
dig -4 +short +notcp +norecurse +tries=1 +timeout=1
-q health.load.balance. -t txt @10.2.1.5;
sleep 1;
done
Этот скрипт будет раз в секунду выдавать ресурсную запись health.load.balance/IN/TXT, которая содержит идентификатор сервера. В нашей конфигурации это будет тот сервер, который сейчас VRRP-мастер.
Зайдите на этот сервер и убедитесь, что в файле /var/run/keepalived.INSTANCE.VI_1.state в последней строке указано MASTER.
Перезапустите на мастере сервис keepalived. Если у вас все сделано правильно, то мастер поменяется. На 1-м сервере в файле keepalived.INSTANCE.VI_1.state появятся строки STOP и BACKUP, на втором сервере в этом же файле появится строка MASTER, а клиентский скрипт станет выдавать ресурсную запись с идентификатором нового мастера.
[2020-10-13T10:48:00+00:00] /etc/keepalived/keepalived-notify.sh INSTANCE VI_1 BACKUP 100
[2020-10-13T11:26:29+00:00] /etc/keepalived/keepalived-notify.sh INSTANCE VI_1 MASTER 100
"=nameserver-1="
"=nameserver-1="
"=nameserver-1="
"=nameserver-2="
"=nameserver-2="
Все, кластер готов. Теперь можно перезагружать сервера по одному в любое время, не нужно откладывать обновления на выходные, не нужно работать по ночам, не нужно рассылать пользователям предупреждения о том, что возможны перебои в связи с регламентными работами.
Если у вас все получилось, значит вы успешно прошли 1-й уровень кластеризации. Он обладает следующими плюсами:
Однако есть у данного варианта развертывания и существенный минус. В один момент времени в кластере работает только один сервер, а остальные, что называется, курят бамбук. Во-первых, это неэффективно с точки зрения использования вычислительных ресурсов. Во-вторых, если ваш сервер перестанет справляться с нагрузкой, то такой простенький кластер вам не поможет.
Чтобы обеспечить не только отказоустойчивость, но и масштабирование, переходите на следующий уровень кластеризации, о котором я расскажу прямо сейчас.
Чтобы задействовать все сервера кластера, нужно научить VRRP-мастер, на сетевом интерфейсе которого прописан VIP, не только принимать трафик для своих локальных сервисов, но и направлять часть трафика на остальные сервера. Демон keepalived как раз умеет это делать.
Отредактируйте конфиг keepalived.conf, приведя его к следующему виду:
global_defs {
enable_script_security
}
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 5
priority 100
advert_int 1
nopreempt
notify /etc/keepalived/keepalived-notify.sh root
authentication {
auth_type PASS
auth_pass KPSjXfRG
}
virtual_ipaddress {
10.2.1.5
}
}
virtual_server 10.2.1.5 53 {
protocol UDP
delay_loop 10
lvs_sched rr
lvs_method NAT
real_server 10.2.1.2 53 {
DNS_CHECK {
type txt
name health.load.balance.
}
}
real_server 10.2.1.3 53 {
DNS_CHECK {
type txt
name health.load.balance.
}
}
}
virtual_server 10.2.1.5 53 {
protocol TCP
delay_loop 10
lvs_sched rr
lvs_method NAT
real_server 10.2.1.2 53 {
TCP_CHECK {
connect_timeout 3
}
}
real_server 10.2.1.3 53 {
TCP_CHECK {
connect_timeout 3
}
}
}
Начало конфигурации аналогично предыдущему варианту без балансировки, только из блока vrrp_instance исчез track_script, соответственно за ненадобностью был удален блок vrrp_script.
Главное отличие в новой конфигурации заключается в блоках virtual_server. Для DNS требуется 2 виртуальных сервера, для 53-го порта TCP и для 53-го порта UDP. В случае HTTP-сервера аналогично потребуются сервера для 80-го и 443-го портов TCP.
Каждый виртуальный сервер идентифицируется 3 значениями: IP, порт и протокол. IP и порт через пробел указываются в заголовке блока virtual_server, а протокол определяется параметром protocol внутри блока. Допустимые протоколы TCP и UDP. В случае DNS как раз нужны оба.
Параметр delay_loop задает периодичность, с которой балансировщик опрашивает бэкенды для определения их состояния.
Самые важные параметры в виртуальном сервере — это lvs_sched и lvs_method. lvs_sched задает алгоритм, по которому балансировщик определяет, куда отправить очередной IP-пакет. lvs_method задает механизм, который использует балансировщик для направления пакетов в выбранный пункт назначения.
В приведенном примере lvs_sched равен rr, что означает round robin, т. е. балансировка равномерно по очереди. lvs_method используется NAT. Кроме NAT доступны также механизмы DR (direct routing) и TUN (tunneling). На мой взгляд, NAT — единственный рабочий вариант. Остальные методы работают только в очень специфических условиях.
Каждый виртуальный сервер состоит из нескольких реальных серверов, что отражено в соответствующих вложенных блоках real_server. Внутри блока real_server надо описать способ опроса его состояния, т. е. healthcheck.
После такой настройки keepalived работает следующим образом:
Только для работы этого недостаточно. Выбранный реальный сервер отправит ответный IP-пакет, в котором адрес отправителя будет равен его собственному IP-адресу. Клиент дропнет такой пакет, потому что он отправлял запрос на VIP и ожидает ответы от VIP, а не с IP-адреса реального сервера.
Чтобы решить эту проблему, нужно заставить реальный сервер отправить ответ не на IP клиента, а на IP мастера, чтобы тот выполнил обратную подстановку реального IP на виртуальный.
Для этого понадобится itpables и некоторые настройки ядра.
dnf -y install iptables iptables-services
systemctl enable iptables
systemctl start iptables
echo "net.ipv4.ip_forward=1" >>/etc/sysctl.d/99-sysctl.conf
echo "net.ipv4.vs.conntrack=1" >>/etc/sysctl.d/99-sysctl.conf
sysctl -w net.ipv4.ip_forward=1
sysctl -w net.ipv4.vs.conntrack=1
Добавьте в iptables следующее правило:
iptables -t nat -I POSTROUTING 1 -d 10.2.1.0/24 -j SNAT --to-source 10.2.1.5
service iptables save
Действие SNAT означает, что после маршрутизации в IP-пакете IP-адрес источника будет заменен на IP-адрес to-source. Вместо SNAT можно также использовать действие MASQUERADE, которое делает то же самое, только определяет исходящий IP автоматически.
Поскольку были внесены изменения в такие вещи как iptables и параметры ядра, рекомендуется перезагрузить сервер и убедиться, что ничего из этого не потерялось.
Выполните описанные действия на обоих серверах кластера. Проверьте логи и статусные файлы. Если ошибок нет, то выполните проверку работы кластера способом, описанным выше. Теперь должен получиться вот такой вывод:
"=nameserver-1="
"=nameserver-2="
"=nameserver-1="
"=nameserver-2="
"=nameserver-1="
Поскольку в балансировщике задан алгоритм round robin, то сервера отвечают строго по очереди друг за другом.
Остановите named на 2-м сервере, и получите:
"=nameserver-1="
"=nameserver-1="
"=nameserver-1="
"=nameserver-1="
"=nameserver-1="
Снова запустите на 2-м сервере named, и на клиенте снова начнется чередование ответов.
Не забудьте про корректировку политик selinux, о которой рассказано выше.
Поздравляю. Только что мы с вами прошли 2-й уровень кластеризации.
Теперь наш кластер не только отказоустойчивый, но и масштабируемый.
Тем не менее, данная конструкция имеет и большой минус. Из-за трансляции адресов на подчиненные сервера запросы приходят с VIP, и невозможно по логам определить, какой запрос от какого клиента пришел. Возможность вычислить клиента по IP очень полезна для решения различных проблем. Поэтому, мой совет — не останавливаться на 2-м уровне и, по возможности, перейти на 3-й.
3-й уровень кластеризации подразумевает, что вы уже находитесь на 2-м, и все у вас работает как надо. Необходимо только обеспечить проброс клиентского IP-адреса до реальных серверов.
Принцип проброса следующий: все пакеты, приходящие на сервер, содержат IP реального клиента, а ответы на них отправляются на VIP. Т. е. VIP для реального сервера является шлюзом по умолчанию. Причем на этот шлюз направляются даже те пакеты, получатель которых находится в локальной подсети.
Но у серверов уже есть шлюз по умолчанию, и заменить его нельзя. Если убрать стандартный шлюз по умолчанию, то балансировка может быть и будет работать, однако все остальное сетевое взаимодействие у сервера сломается, и на него нельзя будет даже зайти по SSH.
Решение заключается в создании на сервере 2-го сетевого интерфейса. На 1-м сетевом интерфейсе будет производиться стандартный сетевой обмен, а на 2-м как раз и будет настроен VIP в качестве шлюза по умолчанию. Соответственно, у серверов вместе с дополнительным сетевым интерфейсом должен будет появиться и дополнительный IP-адрес.
Итак, добавьте на сервера кластера дополнительные сетевые интерфейсы и назначьте им IP-адреса.
Соберите информацию, необходимую для дальнейшей настройки. В нашем примере будут использоваться следующие значения:
Параметр | Возможное значение | Описание |
---|---|---|
vip | 10.2.1.5 | виртуальный IP, на который шлют запросы клиенты |
dev0 | eth0 | 1-й сетевой интерфейс на узлах кластера |
dev1 | eth1 | 2-й сетевой интерфейс на узлах кластера |
ip01 | 10.2.1.2 | IP 1-го узла кластера на 1-м сетевом интерфейсе |
ip02 | 10.2.1.3 | IP 2-го узла кластера на 1-м сетевом интерфейсе |
ip11 | 10.2.1.6 | IP 1-го узла кластера на 2-м сетевом интерфейсе |
ip12 | 10.2.1.7 | IP 2-го узла кластера на 2-м сетевом интерфейсе |
net0 | 10.2.1.0/24 | подсеть, которой принадлежат ip01 и ip02 |
Скорректируйте конфигурацию keepalived по указанному образцу.
global_defs {
enable_script_security
}
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 5
priority 100
advert_int 1
nopreempt
notify /etc/keepalived/keepalived-notify.sh root
authentication {
auth_type PASS
auth_pass KPSjXfRG
}
virtual_ipaddress {
10.2.1.5
}
}
virtual_server 10.2.1.5 53 {
protocol UDP
delay_loop 10
lvs_sched rr
lvs_method NAT
real_server 10.2.1.6 53 {
DNS_CHECK {
connect_ip 10.2.1.2
type txt
name health.load.balance.
}
}
real_server 10.2.1.7 53 {
DNS_CHECK {
connect_ip 10.2.1.3
type txt
name health.load.balance.
}
}
}
virtual_server 10.2.1.5 53 {
protocol TCP
delay_loop 10
lvs_sched rr
lvs_method NAT
real_server 10.2.1.6 53 {
TCP_CHECK {
connect_ip 10.2.1.2
connect_timeout 3
}
}
real_server 10.2.1.7 53 {
TCP_CHECK {
connect_ip 10.2.1.3
connect_timeout 3
}
}
}
От предыдущей конфигурации новая отличается тем, что для реальных серверов указаны IP с дополнительных сетевых интерфейсов, но healthcheck-и отправляются на IP основных сетевых интерфейсов. Обратите внимание на этот момент. Все сервера кластера опрашивают друг друга, но на дополнительных сетевых интерфейсах будут настроены маршруты на VIP, поэтому узлы, которые сейчас в состоянии BACKUP, не смогут получить ответ от новых IP-адресов.
Теперь необходимо прописать правильные маршруты для дополнительных сетевых интерфейсов.
Добавьте в файл /etc/iproute2/rt_tables 2 новых таблицы маршрутизации. В примере ниже добавлены таблицы table0 и table1.
#
# reserved values
#
255 local
254 main
253 default
0 unspec
#
# local
#
#1 inr.ruhep
20 table0
21 table1
По документации к NetworkManager и CentOS 8 статические маршруты и правила следует помещать в файлы /etc/syconfig/network-scripts/route-eth0 и rule-eth0. На многих моих серверах именно так и сделано. Только почему-то на серверах, поднятых из одного и того же образа, формат этих файлов оказался разным. На большинстве серверов route-eth0 выглядит так:
192.168.1.0/24 via 192.168.1.1
172.10.1.0/24 via 172.10.1.1
но почему-то на моих серверах DNS эти же файлы содержат вот это:
ADDRESS0=192.168.1.0
NETMASK0=255.255.255.0
GATEWAY0=192.168.1.1
ADDRESS1=172.10.1.0
NETMASK1=255.255.255.0
GATEWAY1=172.10.1.1
Формат понятен, только совершенно непонятно, как в него поместить имя таблицы маршрутизации. Я не смог разобраться в этом вопросе. Если кто-то знает, в чем дело, поделитесь, пожалуйста, информацией в комментариях.
Поскольку сохранить маршруты в специальных системных файлах не удалось, пришлось сделать костыль.
#!/bin/sh
# https://tldp.org/HOWTO/Adv-Routing-HOWTO/lartc.rpdb.multiple-links.html
vip="10.2.1.5"
dev0="eth0"
ip0="10.2.1.2" # "10.2.1.3"
dev1="eth1"
ip1="10.2.1.6" # "10.2.1.7"
ip route add 10.2.1.0/24 dev "$dev0" src "$ip0" table table0
ip route add default via 10.2.1.1 table table0
ip rule add from "$ip0" table table0
ip route add "$vip/32" dev "$dev1" src "$ip1"
ip route add default via "$vip" table table1
ip route add "$vip/32" dev "$dev1" src "$ip1" table table1
ip rule add from "$ip1" table table1
В комментарии в скрипте указаны значения, которые надо скорректировать при переносе скрипта на другой сервер.
Данный скрипт я добавил в автозапуск вместе с сервисом keepalived.
systemctl edit keepalived
[Service]
ExecStartPre=/etc/keepalived/routes.sh
Не самое элегантное решение, но работает нормально, поэтому можете спокойно использовать, если не знаете, как можно сделать лучше.
Принципы задания правил маршрутизации описаны в Linux Advanced Routing & Traffic Control HOWTO [2]. Идея заключается в том, что создается 2 независимые таблицы маршрутизации, в каждой свой шлюз по умолчанию. В зависимости от того, с какого сетевого интерфейса отправляется пакет, с помощью правил ip rule выбирается либо одна таблица, либо другая.
Удалите из iptables правило SNAT в цепочке POSTROUTING, добавленное при прохождении 2-го уровня. Сохраните состояние iptables.
iptables -t nat -D POSTROUTING -d 10.2.1.0/24 -j SNAT --to-source 10.2.1.5
service iptables save
Проверьте результат. Клиент должен получать ответы от каждого сервера поочередно, в логах на серверах должны писаться реальные IP клиентов.
Теперь имеется отказоустойчивый кластер, в котором все живые узлы принимают нагрузку и видят IP-адреса своих клиентов. Единственный небольшой минус у этой конфигурации заключается в том, что есть 1 скрипт, который на разных серверах должен иметь разные параметры. Небольшое усложнение администрирования, которое с лихвой окупается полученными преимуществами.
Сложность | Масштабирование | Единые настройки | IP клиента | |
---|---|---|---|---|
Уровень 1 | Easy | Нет | Да | Да |
Уровень 2 | Normal | Да | Да | Нет |
Уровень 3 | Expert | Да | Нет | Да |
Автор: Сергей Б.
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/news/358221
Ссылки в тексте:
[1] VRRP: https://en.wikipedia.org/wiki/Virtual_Router_Redundancy_Protocol
[2] Linux Advanced Routing & Traffic Control HOWTO: https://tldp.org/HOWTO/Adv-Routing-HOWTO/lartc.rpdb.multiple-links.html
[3] Источник: https://habr.com/ru/post/524688/?utm_source=habrahabr&utm_medium=rss&utm_campaign=524688
Нажмите здесь для печати.