Построение отказоустойчивого кластера TEGU¶
Организация балансировки вычислительных нод Tegu потребуется вам в случае использования редакции Tegu Enterprise в мультисерверном (кластерном) исполнении. Такая балансировка не имеет прямого отношения к дистрибутиву Tegu, т.к. выполняется сетевым оборудованием пользователя.
Однако, нет ничего страшного в случае, если у пользователя нет сетевого оборудования, способного выполнять балансировку трафика, т.к. подобное решение можно реализовать стандартными средствами Linux и дополнительным ПО, отслеживающим падение нод.
Одно из таких решений Tiar мы предлагаем в данном разделе. Данный скрипт написан нами на Python для работы с nftables и может быть полезен как сам по себе, так и для понимания принципа балансировки трафика с помощью собственного оборудования пользователя.
#!/usr/bin/env python3
import signal
import socket
import nftables
import time
from systemd.journal import JournalHandler
import logging
log = logging.getLogger('nft_lb')
log.addHandler(JournalHandler())
log.setLevel(logging.INFO)
class ServiceDestination:
def __init__(self, name, dip):
self.name = name
self.dip = dip
self.online = True
class LbService:
def __init__(self, name, vip, vport):
self.name = name
self.vip = vip
self.vport = vport
self.dest = {}
class Event:
def __init__(self):
self.shutdown = False
ev = Event()
lb_map = {}
def create_rules():
nft = nftables.Nftables()
code, _, __ = nft.cmd('flush table ip lb')
if code != 0:
code, _, __ = nft.cmd('create table ip lb')
nft.cmd('add chain ip lb prerouting { type nat hook prerouting priority 0 ; }')
nft.cmd('add chain ip lb forward { type filter hook forward priority -10 ; }')
for svc_name in lb_map.keys():
nft.cmd(f'add chain ip lb dnat_{svc_name}')
nft.cmd(f'add rule ip lb prerouting ip daddr {lb_map[svc_name].vip} tcp dport {lb_map[svc_name].vport} counter jump dnat_{svc_name}')
dest_count = len(lb_map[svc_name].dest)
dest_list = []
for dest_num, dest_name in enumerate(lb_map[svc_name].dest.keys()):
dest_list.append(f'{dest_num} : {lb_map[svc_name].dest[dest_name].dip}')
nft.cmd(f'add rule ip lb forward ip daddr {lb_map[svc_name].dest[dest_name].dip} tcp dport {lb_map[svc_name].vport} counter mark set 333444555 accept comment "[{svc_name}] -> {dest_name}"')
nft.cmd(f'add rule ip lb dnat_{svc_name} counter dnat to numgen inc mod {dest_count} map {{ {", ".join(dest_list)} }}')
def recreate_service(svc_name):
nft = nftables.Nftables()
dest_list = []
online_exists = False
for dest_name in lb_map[svc_name].dest.keys():
if lb_map[svc_name].dest[dest_name].online:
dest_list.append(lb_map[svc_name].dest[dest_name].dip)
online_exists = True
dest_count = len(dest_list)
dest_map_list = []
for num, dest in enumerate(dest_list):
dest_map_list.append(f'{num} : {dest}')
nft.cmd(f'flush chain ip lb dnat_{svc_name}')
if online_exists:
nft.cmd(f'add rule ip lb dnat_{svc_name} counter dnat to numgen inc mod {dest_count} map {{ {", ".join(dest_map_list)} }}')
def shutdown_lb(sig_num, frame):
nft = nftables.Nftables()
nft.cmd('flush table ip lb')
nft.cmd('delete table ip lb')
ev.shutdown = True
def check_backends():
for svc_name in lb_map.keys():
need_recreate = False
for dest_name in lb_map[svc_name].dest.keys():
a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
a_socket.settimeout(1)
host = lb_map[svc_name].dest[dest_name].dip
port = lb_map[svc_name].vport
if a_socket.connect_ex((host, port)) != 0:
if lb_map[svc_name].dest[dest_name].online:
log.info(f'[{svc_name}] Node "{dest_name}" offline')
lb_map[svc_name].dest[dest_name].online = False
need_recreate = True
else:
if not lb_map[svc_name].dest[dest_name].online:
log.info(f'[{svc_name}] Node "{dest_name}" online')
lb_map[svc_name].dest[dest_name].online = True
need_recreate = True
a_socket.close()
if need_recreate:
recreate_service(svc_name)
def process_config():
with open('/opt/nft_lb_rules', 'r') as f:
rules_str = f.read()
for rule_line in rules_str.split('\n'):
if rule_line.strip() == '': continue
svc_name, vip, vport, dest_name, dip = rule_line.split('|')
if svc_name not in lb_map:
lb_map[svc_name] = LbService(svc_name, vip, int(vport))
lb_map[svc_name].dest[dest_name] = ServiceDestination(dest_name, dip)
create_rules()
if __name__ == '__main__':
signal.signal(signal.SIGINT, shutdown_lb)
signal.signal(signal.SIGTERM, shutdown_lb)
signal.signal(signal.SIGHUP, shutdown_lb)
process_config()
tik_count = 0
while not ev.shutdown:
if tik_count >= 20:
check_backends()
tik_count = 0
continue
time.sleep(1)
tik_count += 1
Данный скрипт использует список правил из файла /opt/nft_lb_rules в следующем формате:
smtp|1.2.3.4|25|node1|10.1.1.11 smtp|1.2.3.4|25|node2|10.1.1.12 smtp|1.2.3.4|25|node3|10.1.1.13 imap|1.2.3.4|993|node1|10.1.1.11 imap|1.2.3.4|993|node2|10.1.1.12 imap|1.2.3.4|993|node3|10.1.1.13 smtps|1.2.3.4|465|node1|10.1.1.11 smtps|1.2.3.4|465|node2|10.1.1.12 smtps|1.2.3.4|465|node3|10.1.1.13 webadm|1.2.3.4|9999|node1|10.1.1.11 webadm|1.2.3.4|9999|node2|10.1.1.12 webadm|1.2.3.4|9999|node3|10.1.1.13
Если в nftables в основной цепоче forward последним правилом настроено отбрасывание всех внешних пакетов, то необходимо исключить из этого правила пакеты с меткой 333444555:
iifname "eth1" mark != 333444555 counter drop
Для настройки автозапуска скрипта балансировщика, надо создать, включить и запустить сервис systemd:
/etc/systemd/system/nft_lb.service
[Unit] Description=Nftables python balancer After=multi-user.target [Service] Type=simple Restart=always ExecStart=/usr/bin/python3 /opt/nft_lb.py [Install] WantedBy=multi-user.target
Рассмотрим более подробно два варианта установки и настройки балансировщика на базе Debian:¶
1. Когда одна нога сетевого интерфейса балансировщика смотрит в интернет напрямую. 2. Когда одной ногой сетевого интерфейса балансировщик смотрит через VPN наружу из другой подсети.
отсюда разные настройки сетевых интерфейсов и разные настройки правил nftables.
В качестве подробного примера рассмотрим балансировщик построенный на Linux Debian 11.
При установке операционной системы в экспертном режиме создаем одну рутовую учетную запись.
Лучшим вариантом будет использованием контейнера в виртуализированной среде Proxmox.
Проверяем обновления пакетов.
apt update
Устанавливаем обновления на операционную систему.
apt-full upgrade
Удаляем iptables
apt purge iptables
Устанавливаем nftables
apt install nftables
Далее, необходимо настроить службу и автоматический старт:
systemctl enable nftables systemctl start nftables systemctl status nftables
Устанавливаем сетевые утилиты:
apt install tcpdump ethtool iftop net-tools procps
Вариант первый:¶
Одна нога балансировщика смотрит в интернет напрямую.¶
Настраиваем на балансировщике сетевые интерфейсы согласно схеме.¶
nano /etc/network/interfaces
auto lo iface lo inet loopback auto ens19 iface ens19 inet static address 75.137.210.126/24 gateway 75.137.210.1 auto ens18 iface ens18 inet static address 10.199.199.130/24
Устанавливаем bind9¶
apt install bind9
Прописываем следующие настройки.
nano /etc/default/bind9
# # run resolvconf? RESOLVCONF=no # startup options for the server OPTIONS="-4 -u bind"
Меняем DNS на 127.0.0.1
nano /etc/resolv.conf
Должно получится так:
search lan nameserver 127.0.0.1
Включаем форвардинг.
Необходимо раскоментировать строку net.ipv4.ip_forward=1
nano /etc/sysctl.d/99-sysctl.conf
Перезапускаем bind9
systemctl restart bind9
Прописываем настройки nftables согласно схеме.¶
nano /etc/nftables.conf
#!/usr/sbin/nft -f flush ruleset table ip nat { chain prerouting { type nat hook prerouting priority -150; policy accept; } chain postrouting { type nat hook postrouting priority 100; policy accept; oifname "ens19" ip saddr 10.199.199.0/24 counter masquerade } } table inet filter { chain input { type filter hook input priority 0; policy accept; ct state established,related counter accept icmp type echo-request counter accept iifname "ens19" tcp dport 2223 counter accept comment "SSH-Доступ" iifname "ens19" tcp dport http counter accept comment "Let's Encrypt auth" iifname "ens19" counter drop } chain forward { type filter hook forward priority 0; policy accept; ct state established,related counter accept iifname "ens19" mark != 333444555 counter drop } chain output { type filter hook output priority 0; policy accept; } }
P. S
Если в nftables в основной цепочке forward последним правилом настроено отбрасывание всех внешних пакетов, то необходимо исключить из этого правила пакеты с меткой 333444555:
iifname "eth1" mark != 333444555 counter drop
Разрешаем вход через SSH.¶
nano /etc/ssh/sshd_config
Находим строку
#PermitRootLogin prohibit-password
меняем значение на
PermitRootLogin yes
перезапускаем сервис SSH.
systemctl restart sshd
Добавляем в SSH дополнительный порт 2223¶
nano /etc/ssh/sshd_config
Port 22 Port 2223
systemctl restart sshd service nftables restart
Рестартуем procps и nftables
service procps restart service nftables restart
Устанавливаем fail2ban¶
apt install fail2ban
Приводим fail2ban в соответствии к следующим настройкам:¶
nano /etc/fail2ban/jail.d/defaults-debian.conf
[DEFAULT] bantime = 10800 findtime = 3600 ignoreip = 127.0.0.1/8 maxretry = 3 banaction = nftables-multiport [sshd] port = 2223 enabled = true
рестартуем nftables и fail2ban
service nftables restart && service fail2ban restart
Проверяем логи fail2ban на наличие ошибок.¶
tail -F /var/log/fail2ban.log
Установка и настройка скрипта балансировщика.¶
Установим дополнительные библиотеки:
apt install python3-nftables nmap python3-nmap python3-scapy
Прописываем скрипт.¶
(В самом скрипте менять ничего не нужно)
nano /opt/nft_lb.py
#!/usr/bin/env python3 import signal import socket import nftables import time from systemd.journal import JournalHandler import logging log = logging.getLogger('nft_lb') log.addHandler(JournalHandler()) log.setLevel(logging.INFO) class ServiceDestination: def __init__(self, name, dip): self.name = name self.dip = dip self.online = True class LbService: def __init__(self, name, vip, vport): self.name = name self.vip = vip self.vport = vport self.dest = {} class Event: def __init__(self): self.shutdown = False ev = Event() lb_map = {} def create_rules(): nft = nftables.Nftables() code, _, __ = nft.cmd('flush table ip lb') if code != 0: code, _, __ = nft.cmd('create table ip lb') nft.cmd('add chain ip lb prerouting { type nat hook prerouting priority 0 ; }') nft.cmd('add chain ip lb forward { type filter hook forward priority -10 ; }') for svc_name in lb_map.keys(): nft.cmd(f'add chain ip lb dnat_{svc_name}') nft.cmd(f'add rule ip lb prerouting ip daddr {lb_map[svc_name].vip} tcp dport {lb_map[svc_name].vport} counter jump dnat_{svc_name}') dest_count = len(lb_map[svc_name].dest) dest_list = [] for dest_num, dest_name in enumerate(lb_map[svc_name].dest.keys()): dest_list.append(f'{dest_num} : {lb_map[svc_name].dest[dest_name].dip}') nft.cmd(f'add rule ip lb forward ip daddr {lb_map[svc_name].dest[dest_name].dip} tcp dport {lb_map[svc_name].vport} counter mark set 333444555 accept comment "[{svc_name}] -> {dest_name}"') nft.cmd(f'add rule ip lb dnat_{svc_name} counter dnat to numgen inc mod {dest_count} map {{ {", ".join(dest_list)} }}') def recreate_service(svc_name): nft = nftables.Nftables() dest_list = [] online_exists = False for dest_name in lb_map[svc_name].dest.keys(): if lb_map[svc_name].dest[dest_name].online: dest_list.append(lb_map[svc_name].dest[dest_name].dip) online_exists = True dest_count = len(dest_list) dest_map_list = [] for num, dest in enumerate(dest_list): dest_map_list.append(f'{num} : {dest}') nft.cmd(f'flush chain ip lb dnat_{svc_name}') if online_exists: nft.cmd(f'add rule ip lb dnat_{svc_name} counter dnat to numgen inc mod {dest_count} map {{ {", ".join(dest_map_list)} }}') def shutdown_lb(sig_num, frame): nft = nftables.Nftables() nft.cmd('flush table ip lb') nft.cmd('delete table ip lb') ev.shutdown = True def check_backends(): for svc_name in lb_map.keys(): need_recreate = False for dest_name in lb_map[svc_name].dest.keys(): a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) a_socket.settimeout(1) host = lb_map[svc_name].dest[dest_name].dip port = lb_map[svc_name].vport if a_socket.connect_ex((host, port)) != 0: if lb_map[svc_name].dest[dest_name].online: log.info(f'[{svc_name}] Node "{dest_name}" offline') lb_map[svc_name].dest[dest_name].online = False need_recreate = True else: if not lb_map[svc_name].dest[dest_name].online: log.info(f'[{svc_name}] Node "{dest_name}" online') lb_map[svc_name].dest[dest_name].online = True need_recreate = True a_socket.close() if need_recreate: recreate_service(svc_name) def process_config(): with open('/opt/nft_lb_rules', 'r') as f: rules_str = f.read() for rule_line in rules_str.split('\n'): if rule_line.strip() == '': continue svc_name, vip, vport, dest_name, dip = rule_line.split('|') if svc_name not in lb_map: lb_map[svc_name] = LbService(svc_name, vip, int(vport)) lb_map[svc_name].dest[dest_name] = ServiceDestination(dest_name, dip) create_rules() if __name__ == '__main__': signal.signal(signal.SIGINT, shutdown_lb) signal.signal(signal.SIGTERM, shutdown_lb) signal.signal(signal.SIGHUP, shutdown_lb) process_config() tik_count = 0 while not ev.shutdown: if tik_count >= 20: check_backends() tik_count = 0 continue time.sleep(1) tik_count += 1
Делаем скрипт исполняемым.
chmod +x /opt/nft_lb.py
Данный скрипт использует список правил из файла /opt/nft_lb_rules в следующем формате:
smtp|1.2.3.4|25|node1|10.199.199.231 smtp|1.2.3.4|25|node2|10.199.199.232 smtp|1.2.3.4|25|node3|10.199.199.233 imap|1.2.3.4|993|node1|10.199.199.231 imap|1.2.3.4|993|node2|10.199.199.232 imap|1.2.3.4|993|node3|10.199.199.233 smtps|1.2.3.4|465|node1|10.199.199.231 smtps|1.2.3.4|465|node2|10.199.199.232 smtps|1.2.3.4|465|node3|10.199.199.233 webadm|1.2.3.4|9999|node1|10.199.199.231 webadm|1.2.3.4|9999|node2|10.199.199.232 webadm|1.2.3.4|9999|node3|10.199.199.233
Для настройки автозапуска скрипта балансировщика, надо создать, включить и запустить сервис systemd:
/etc/systemd/system/nft_lb.service
[Unit] Description=Nftables python balancer After=multi-user.target [Service] Type=simple Restart=always ExecStart=/usr/bin/python3 /opt/nft_lb.py [Install] WantedBy=multi-user.target
Команды для управления сервисом nft_lb.service
systemctl enable nft_lb.service systemctl start nft_lb.service systemctl status nft_lb.service systemctl stop nft_lb.service systemctl restart nftables.service
Ноды Tegu.¶
На стороне почтовых нод приводим сетевые настройки в соответствии схемы.¶
Не забываем о том, что шлюзом у нас является балансировщик.
nano /etc/network/interfaces
# The loopback network interface auto lo iface lo inet loopback # The primary network interface auto ens18 iface ens18 inet static address 10.199.199.131/24 gateway 10.199.199.130
Также не забываем про маршруты
nano /etc/network/routes
Маршруты прописываются в следующем формате:
# For example: # # 172.1.1.0 255.255.255.0 192.168.0.1 any
Вариант 2¶
Балансировщик одной ногой сетевого интерфейса смотрит через VPN наружу из другой подсети.¶
Отметим основные отличия:
На балансировщике настраиваем сеть
nano /etc/network/interfaces
auto lo iface lo inet loopback auto eth0 iface eth0 inet static address 10.44.44.14/24 gateway 10.44.44.254 auto eth1 iface eth1 inet static address 10.33.33.10/24
Настройки nftables¶
nano /etc/nftables.conf
#!/usr/sbin/nft -f flush ruleset table ip nat { chain prerouting { type nat hook prerouting priority -150; policy accept; } chain postrouting { type nat hook postrouting priority 100; policy accept; oifname eth1 ip saddr 10.44.44.0/24 ip daddr { 10.33.33.20, 10.33.33.21, 10.33.33.22 } counter masquerade oifname "eth0" ip saddr 10.33.33.0/24 counter masquerade } } table inet filter { chain input { type filter hook input priority 0; policy accept; ct state established,related counter accept iifname "eth0" tcp dport 80 counter accept comment "Let's Encrypt" iifname "eth0" tcp dport 2223 counter accept comment "SSH-Доступ" iifname "eth0" tcp dport http counter accept comment "Let's Encrypt auth" iifname "eth0" counter drop } chain forward { type filter hook forward priority 0; policy accept; ct state established,related counter accept iifname "eth0" mark != 333444555 counter drop } chain output { type filter hook output priority 0; policy accept; } }
Ноды Tegu.¶
На стороне почтовых нод приводим сетевые настройки в соответствии схемы.¶
nano /etc/network/interfaces
auto lo iface lo inet loopback auto eth0 iface eth0 inet static address 10.44.44.22/24 auto eth1 iface eth1 inet static address 10.33.33.20/24 gateway 10.33.33.10
и маршруты
nano /etc/network/routes
10.199.199.0 255.255.255.0 10.44.44.254 eth0 10.252.128.0 255.255.255.0 10.44.44.254 eth0
P/S Маршруты приведены для примера.
На этом установка завершена!¶