Проект

Общее

Профиль

Построение отказоустойчивого кластера 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 Маршруты приведены для примера.

На этом установка завершена!